场景:
业务逻辑:第三方登录情况下,获取到用户的实名信息。之后判断该用户在用户表中是否存在,如果不存在或非实名,那么将其实名;如果已经实名,那么不做处理,直接登录。ORM使用的是spring data jpa,用户表在mobile字段上有唯一索引idx_mobile
发现不定期的发生业务报错:Deadlock found when trying to get lock; try restarting transaction
原因:
分析死锁日志
通过SHOW ENGINE INNODB STATUS;来查看死锁日志:
日志类似
注意:SHOW ENGINE INNODB STATUS\G 看到的DEADLOCK相关信息,只会返回最后的2个事务的信息,而其实有可能有更多的事务才最终导致的死锁
日志的上半部分说明事务1在等待什么锁
ip1 dbuser update
这个用户在执行下面这条sql语句
insert into 用户表 值1
其在申请idx_mobile索引的
RECORD LOCKS space id 3251 page no 14336 n bits 704 index `idx_mobile` of table 用户表 trx id 306872608 lock_mode X locks gap before rec insert intention waiting
这条插入记录的事务等待中,等待获得插入意向锁
日志的下半部分说明了事务2当前持有的锁以及等待的锁:
ip2 dbuser update
这个用户在执行下面这条sql语句
insert into 用户表 也是值1
HOLDS THE LOCK(S):
事务2持有S gap lock
lock mode S locks gap before rec
至于为什么加S Gap-Lock ,是因为在插入之前还需要多一步检查:如果记录中有唯一约束,判断存在一条记录等于当前插入的记录时,则需要在这个记录加上S Gap-Lock
也就是说事务1的insert intention lock等待事务2的s gap-lock释放
从日志的WAITING FOR THIS LOCK TO BE GRANTED块中我们可以看到事务2正在申请插入意向锁
那是什么原因造成这个dead lock呢?
是事务0的回滚导致事务1和事务2的deadlock
为什么是事务0呢,看后面的参考就知道了,事务1和事务2的死锁是由于事务0的rollback导致的
参考:
还原整个过程
第三方登录的情况下,前后端没有做重复提交的避免策略,这样会造成一个用户可以多次执行第三方登录的请求,当用户短时间内连续3次(或以上)执行第三方登录的请求,导致会起3个transaction(事务0 事务1 事务2)去执行insert操作
此时如果事务0由于业务代码问题rollback,会导致事务1和事务2 deadlock,直到mysql锁超时,报deadlock错误
即发生:当有3个(或以上)事务对相同的表进行insert操作,如果insert对应的字段上有uniq key约束并且第一个事务rollback了,那其中一个将返回死锁错误信息。
解决方案
避免此DEADLOCK;我们都知道死锁的问题通常都是业务处理的逻辑造成的,既然是uniq key,同时多台不同服务器上的相同程序对其insert一模一样的value,这本身逻辑就不太完美。故解决此问题:
思路1:
保证业务程序别在同一时间点并发的插入相同的值到相同的uniq key的表中
前端可以通过
1.提交数据之前判断当前提交按钮是否存在lock锁
2.在ajax提交之前给提交按钮上锁
3.ajax成功之后或者失败之后解锁
后端可以通过redis+aop来做
参考:redis防表单重复提交
思路2:
由于是事务0 rollback了才产生的deadlock,查明rollback的原因
我们的解决方法
我们现在是前端做重复提交的去重。
后端修改了可能产生rollback的逻辑