delete+insert 引发的死锁问题
问题
当上层业务会短时间内调用两次接口,导致线上报错死锁,报错信息如下:
屏幕快照 2020-07-06 上午10.56.05.png死锁日志:
屏幕快照 2020-07-06 下午2.11.23.png
db结构
唯一索引 userid+userKey
普通索引 userkey+uservalue
场景还原
updateUniqueClientIdAndUserValue方法里面,一共有三个db操作:①根据userId和userKeys、userValues查询②根据userkey和value删除数据③批量保存或更新key-value信息
可直接定位到删除+insert这部操作的问题;
下面的表格是两个事务中的db操作:
(红色字体为db提示)
![ 屏幕快照 2020-07-06 下午2.14.12.png]
屏幕快照 2020-07-06 下午2.14.12.png
至此,死锁问题复现
分析原因
首先,死锁为什么会产生?死锁的产生需要相互等待资源,相互等待的资源是什么?
了解这个之前,先了解一下delete操作的时候需要获取什么锁。
屏幕快照 2020-07-06 下午2.14.17.png以及锁之间的兼容关系:
由此,可以分析出,结合死锁日志,事务1执行delete操作的时候,获取了索引区间(1086,415097555)的gap锁;
事务2执行的时候,也获取了索引区间(1086,415097555)的gap锁;由于gap锁之间相互兼容,到这一步为止是正常执行的;
事务1insert的时候,需要先获取一个插入意向锁(insert intention),由于官方文档解释,插入意向锁被认为是一种gap锁,这两个锁之间不兼容,事务1需要等待事务2释放索引区间(1086,415097555)的gap锁,此时db在等待事务2释放资源,也没有产生死锁;当事务2也执行insert的时候,事务2也需要获取插入意向锁,也要等待事务1释放索引区间(1086,415097555)的gap锁,事务2发生死锁,事务回滚,gap锁资源释放;事务1获取到这个锁,执行成功。
解决办法
可以看出来,主要是因为删除了一条不存在的数据导致的,再删除之前先查询,再删除;
我们可以来走一下先查询再删除的场景:
事务1删除一条数据,受影响1行,或者到了这条记录的锁以及这个索引之前的区间gap锁;事务2也要删除这条数据,需要先获得这条记录的行锁,等待事务1执行insert之后事务提交,释放锁资源;
思考
- 线上出现这个死锁的问题时,线下并没有马上复现,因为删除的数据是存在;只复现除了等待锁资源超时
- 死锁日志确实也提到了当insert操作时,获取的(1086,415097555)的插入意向锁那边有问题