并发插入引发的死锁问题排查( 二 )


a. 事务一
HOLDS THE LOCK(S) …… lock_mode X 持有X锁
WAITING FOR THIS LOCK TO BE GRANTED …… X locks gap before rec insert intention waiting 等待insert intention lock
b.事务二
HOLDS THE LOCK(S) …… lock_mode X 持有X锁
WAITING FOR THIS LOCK TO BE GRANTED …… X locks gap before rec insert intention waiting 等待insert intention lock
2. 补充关于一些锁方面的知识
当InnoDB在判断行锁是否冲突的时候 , 除了最基本的IS IX S X锁的冲突判断意外 , 还有一套更精确的判断逻辑 。除了上面说到的锁类型 , InnoDB还将锁细分为如下几种子类型:
record lock(RK)
锁直接加在索引记录上面 , 锁住的是key
gap lock(GK)
间隙锁 , 锁定一个范围 , 但不包括记录本身 。GAP锁的目的 , 是为了防止同一事务的两次当前读 , 出现幻读的情况
next key lock(NK)
行锁和间隙锁组合起来就叫Next-Key Lock
insert intention lock(IK)
如果插入前 , 该间隙已经由gap锁 , 那么Insert会申请插入意向锁 。因为了避免幻读 , 当其他事务持有该间隙的间隔锁 , 插入意向锁就会被阻塞(不用直接用gap锁 , 是因为gap锁不互斥) 。
下面画的就是“精确模式”锁兼容矩阵
列相加行已有RKGKIKNK
RK0110
GK1111
IK1010
NK0110
insert中对唯一索引的加锁逻辑
先做UK冲突检测 , 如果存在目标行 , 先对目标行加S NK(S lock中的next key lock , 下同) , 这个锁如果最终插入成功(该记录在等待期间被其他事务删除 , 此锁被同时删除)
如果1成功 , 对对应行加X IK
如果2成功 , 插入记录 , 并对记录加X RK(有可能是隐式锁)
3.锁的细节
1. 前文已分析 , 一个insert SQL需要加的锁依次为 S NK, X IK, X RK、那么加XIK前需要GK或NK 。而insert不需要加GK , 因此两个事务X IK被申请等待的原因是在申请S NK的过程受到阻塞了 。
2. insert完成之后 , 只会残留X RK锁 , 这就是两个事务都有X RK的原因 , 说明它们刚插入完某几条记录 。
3. 由1,2可以推测 , 死锁是事务1 的S NK被事务2的 X RK所阻塞 , 说明事务2插入的记录在事务1 S NK的范围内 。而事务2的 S NK被 事务1 阻塞的申请S NK给阻塞 , 说明事务1 S NK的范围要大于事务2 S NK的范围 。
4. 由第3点推断 , 可以证明出事务2所有的记录范围 REC2 是要在 事务1所有的记录范围 REC1之后的,既REC2 < REC1
而插入的业务场景的数据是:
事务1
('10076','150686','2017-06-11 08:39:15.866') ,
('10111','150686','2017-06-11 08:39:15.866') ,
('10133','214563','2017-06-11 08:39:15.866') ,
('10171','150686','2017-06-11 08:39:15.866')
事务2
('15186','150686','2017-06-11 08:39:15.866') ,
('15186','151509','2017-06-11 08:39:15.866') ,
('15186','207522','2017-06-11 08:39:15.866') ,
('15187','151509','2017-06-11 08:39:15.866')
 
实际的插入数据符合我们的预期
5.由上面的结论 , 我们可以得到一张死锁循环图
四.预防死锁
死锁发生的条件:
1、资源不能共享 , 需要只能由一个进程或者线程使用
2、请求且保持 , 已经锁定的资源自给保持着不释放
3、不剥夺 , 自给申请到的资源不能被别人剥夺
4、循环等待
防止死锁的途径就是避免满足死锁条件的情况发生 , 适合这个问题解决的方案有:
1、保持事务简短并在一个批处理中
在同一数据库中并发执行多个需要长时间运行的事务时通常发生死锁 。事务运行时间越长 , 其持有排它锁或更新锁的时间也就越长 , 从而堵塞了其它活动并可能导致死锁 。保持事务在一个批处理中 , 可以最小化事务的网络通信往返量 , 减少完成事务可能的延迟并释放锁 。
2、使用低隔离级别
确定事务是否能在更低的隔离级别上运行 。执行提交读允许事务读取另一个事务已读取(未修改)的数据 , 而不必等待第一个事务完成 。使用较低的隔离级别(例如提交读)而不使用较高的隔离级别(例如可串行读)可以缩短持有共享锁的时间 , 从而降低了锁定争夺(比如这次的S NK和X IK 是InnoDB引擎Repeatable Read级别才有的) 。


推荐阅读