RC隔离级别下,死锁案例分析
文章的产生是因为生产上遇到一个死锁案例,根据此案例分析引申出比较多的内容,故总结一下
死锁分析
- 数据库版本: MySQL 5.6.39社区版
- 事务隔离级别: RC
- 死锁日志
这个死锁日志中,可以得到上锁信息如下:
- 事务28608410 , 申请X类型记录锁时,发生了锁等待,对应的记录就是user_id=195578这条(16进制的2fbfa就是195578)
- 事务28608409持有user_id=195578这条记录上的X类型记录锁时
- 事务28608409, 申请S类型的Next-Key Lock时发生了所等待,对应user_id=195578这条记录
RC隔离级别下理论上不应该存在Nexy-Lock的,为什么这里会出现S类型的Next-Lock呢?我们先不考虑这问题,先单纯的分析这个死锁。
上面的死锁对应操作如下:
| 事务28608409 | 事务28608410 |
|---|---|
| start transaction | |
| delete from user_photo_info where user_id = 195578; | |
| delete from user_photo_info where user_id = 195578; | |
| INSERT INTO user_photo_info (user_id, user_photo) VALUES (195578, ‘省略值’); | |
| ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
针对上面这个死锁,有些奇怪:
为什么事务28608409申请S类型的Next-Key Lock时会发生死锁呢?是和谁冲突的呢?感觉不像是和事务28608410发生的冲突,因为28608409还没有结束,那么X类型的记录锁也申请不到。感觉这种情况下不应该发生死锁的,并且发现在8.0.18版本中进行同样的测试,就不会发现死锁问题。
查询了最近8.0版本的Release Notes,在8.0.18版本中有一个bug修复:InnoDB: A deadlock was possible when a transaction tries to upgrade a record lock to a next key lock. (Bug #23755664, Bug #82127)
对应bug地址:
大概意思就是在某些情况下,锁升级问题导致了死锁。现象来看是bug导致,但通过show engine innodb status\G,查看加锁情况,就会发现还是有S类型的Next-Key Lock加锁成功的:
这里会发现在supremum添加S类型的Next-Key Lock加锁成功的,还要继续分析为何会添加这个S类型的Next-Key Lock。
- 注意:
下面的分析就与此bug无关,因为RC隔离级别下某些情况确实会加S类型的Next-Key Lock。主要是看下为何会加S Next-Key Lock和一些延伸。
为何要添加S类型的间隙锁
MySQL中间隙锁是在RR隔离级别下才会有,为何RC情况下也会出现?官方文档中也有说过,如果insert时候,有唯一性索引检测到冲突,会添加S类型Nexy-Key Lock 锁。看一个例子:
- 测试环境MySQL-8.0.19session1|session2|session3
1
2
3create table t1 (id int );
create unique index idx_uni_id on t1 (id);
insert into t1 values(1);
——–|———|———–
begin;|||
DELETE FROM t1 WHERE id = 1;||
|begin;
|insert into t1 values(1);//等待|
||begin;
||insert into t1 values(1); //等待
commit;||
这里还需要两个概念:
唯一约束检测原则:
- 当发生唯一索引约束冲突时,会对当前记录和当前记录的下一条添加S类型Next-Key Lock
插入意向锁:
- 文档中关于插入意向锁的描述,简单说就是:插入意向锁直接是不冲突的,插入意向锁也是一种间隙锁,提高并发插入。
- 但还有一点就是申请插入意向锁时,会检查插入记录位置的下一条记录上是否持有锁,如果有,则判断是否与插入意向锁冲突
如果这里添加的不是S类型Next-Key Lock锁,会出现主键失效的情况, 假设添加的是S NOT GAP锁,情况如下:
session1 添加X类型记录锁
session2 添加S NOT GAP锁 //等待
session3 添加S NOT GAP锁 //等待
由于session1还没有提交,所以会做唯一性约束检查,申请S NOT GAP锁(假设)。当session1执行commit后,session2和session3获得S NOT GAP 锁(假设), 并且session2和session3同时会对下一条记录添加S NOT GAP 锁(假设),这时session2检查插入记录的下一条时发现有S NOT GAP锁,不与插入意向锁冲突,则插入成功,同理session3也会做相应的检查,但也不会发生冲突,所以两条记录都会插入成功。
所以这里需要添加S类型的Next-Key Lock,这样插入意向锁就会发生冲突,在一些场景下就会触发死锁,例如这个例子,死锁日志如下:
这里事务120495申请插入supremum上的插入意向锁时发生了锁等待,因为事务120496在supremum上添加了S类型的Next-Key Lock,并且事务120496申请supremum上的插入意向锁时发生了冲突,这样就造成了死锁。
这里为何说是一些场景情况下, 因为我发现测试中也有不会发生死锁现象的时候,分别看下出现死锁和没有出现死锁时performance_schema.data_locks上的加锁情况:
当发生锁等待时:
session2,session3都会申请S类型的Next-Key Lock,与X类型的记录锁发生锁冲突,等待。
出现死锁时的情况
由于出现了死锁,这里看到的就是留下来的事务加锁的信息,这里能看到个(S,GAP),这个后面讲锁继承和锁迁移时会说到。
没有出现死锁情况
没有出现死锁情况从上锁信息来看,像是session2和session3中,有一个执行的很快,首先在id=1这条记录上添加了X类型的记录锁,导致另外一个会话申请S类型的Next-Key Lock时发生了锁锁等待,从而没有导致死锁发生。但当中还有一点没有搞明白的是,为何S类型的GAP锁会发生锁等待,并且thread_id看起来也很奇怪。
代码上的补充说明
在做插入时首先会做唯一性约束检查,在函数row_ins_scan_sec_index_for_duplicate中,大致内容如下:
1 | do { |
这里还有个问题就是为何还要获取下一条记录呢?这个可以阅读下文章最后面的参考连接(4.5.6),网易温正湖老师的三篇文章。
冲突检测后,会进入lock_rec_insert_check_and_lock函数,主要作用就是检测,插入记录的下一条记录是否存在锁,如果存在是为与插入意向锁冲突:
1 | ulint heap_no = page_rec_get_heap_no(next_rec); //读取当前记录的下一条,获取heap_no |
隐式锁、锁继承、锁分裂
先看一些后面文章中会用到的内容, 涉及到锁类型和锁模式,还有一些数字,其定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 enum lock_mode {
LOCK_IS = 0, /* intention shared */
LOCK_IX, /* intention exclusive */
LOCK_S, /* shared */
LOCK_X, /* exclusive */
LOCK_AUTO_INC, /* locks the auto-inc counter of a table
in an exclusive mode */
LOCK_NONE, /* this is used elsewhere to note consistent read */
LOCK_NUM = LOCK_NONE, /* number of lock modes */
LOCK_NONE_UNSET = 255
};
例如文中提到的,546= LOCK_GAP|LOCK_REC|LOCK_S(512+32+2) = S,GAP
heap_no是记录在物理文件中的位置编号,是物理位置,例如有可能是这样存储:
heap_no : 2 3
存储的值: 2 1
heap_no = 1代表supermum
heap_no = 0 代表infimum
我们插入的数据都是从heap_no = 2开始计算
上面的例子中,我们会看到有S类型的GAP锁出现,这里面涉及到了锁继承和锁分裂,这里我们解释一下:
正常的插入时,不会添加锁的,除非发生有唯一性冲突检测时会添加S类型的Next-Key Lock,通过一个例子来感受下:
session1 开启会话,执行insert语句,这时查看performance_schema.data_locks表,只能看到一个表上的意向锁(IX),但如果session 2 开启会话,执行同样的insert语句,就会看到如下结果:
这里看到表上有了X,REC_NOT_GAP也就是记录锁,但是仔细看会发现thread_id是81,81是session2的线程ID,这就是隐式锁,因为这里的X,REC_NOT_GAP记录锁是session2会话构建的。
还有两个圈红的地方是数值3,代表的heap_no,下面会用到。
涉及到GAP锁时,会有锁继承和锁分裂现象,看下面这个例子:
1 | create table t1 (id int ); |
| session1 | session2 |
|---|---|
| begin; | |
| insert into t1 values(1); | |
| begin; | |
| insert into t1 values(1);//等待 | |
| rollback; |
查看performance_schema.data_locks表:
当执行rollback之前的加锁情况,这时能看到申请S类型Next-Key Lock发生了锁冲突
执行rollback之后情况如下:

这里的锁模式都是S类型的间隙锁,这是如何来的呢?看thread_id是80,那就是session1创建的。执行rollback时候,gdb查看到信息如下:
这里主要看这里: lock_rec_inherit_to_gap(heir_block=0x000000012b837a80,block=0x000000012b837a80, heir_heap_no=2, heap_no=3)
将heap_no=3继承给heap_no=2, type_mode=546(S,GAP) , heap_no=3就是插入id=1这条记录- 锁继承是当原有记录被删除时,需要将原记录上的GAP属性继承给下一条记录。例如:表中有两条记录(1,2),原有的GAP锁加在(-oo,1)上,当记录1被删除后,要保证GAP锁能继续起到锁住这段范围的作用,就会将GAP锁继承给记录2,也就是变成了(-oo,2)。
所以这里session1执行了rollback后,会将原有记录上申请S类型的Next-Key Lock的GAP属性继承给下一条记录。
1
2
3
4
5
6
7
8
9
10thread #42, stop reason = breakpoint 56.2
* frame #0: 0x000000010c2e94f0 mysqld`lock_rec_set_nth_bit(lock=0x00007fb758839778, i=2) at lock0priv.ic:83:3
frame #1: 0x000000010c2e91ac mysqld`RecLock::lock_alloc(trx=0x000000012b2bf060, index=0x00007fb7587c6c88, mode=546, rec_id=0x00007000071ab1a8, size=9) at lock0lock.cc:1033:3
frame #2: 0x000000010c2ea030 mysqld`RecLock::create(this=0x00007000071ab180, trx=0x000000012b2bf060, add_to_hash=true, prdt=0x0000000000000000) at lock0lock.cc:1308:18
frame #3: 0x000000010c2ecc72 mysqld`lock_rec_add_to_queue(type_mode=546, block=0x000000012b837a80, heap_no=2, index=0x00007fb7587c6c88, trx=0x000000012b2bf060, we_own_trx_mutex=false) at lock0lock.cc:1551:12
frame #4: 0x000000010c2edd3c mysqld`lock_rec_inherit_to_gap(heir_block=0x000000012b837a80, block=0x000000012b837a80, heir_heap_no=2, heap_no=3) at lock0lock.cc:2625:7
frame #5: 0x000000010c2eead3 mysqld`lock_update_delete(block=0x000000012b837a80, rec="\x80") at lock0lock.cc:3443:3
frame #6: 0x000000010be4b211 mysqld`btr_cur_optimistic_delete_func(cursor=0x00007000071ab8a8, flags=0, mtr=0x00007000071ab9e0) at btr0cur.cc:4616:5
frame #7: 0x000000010c5560c5 mysqld`row_undo_ins_remove_sec_low(mode=16386, index=0x00007fb7587c6c88, entry=0x00007fb75603f2b8, thr=0x00007fb75a3af210, node=0x00007fb75916c2b8) at row0uins.cc:245:11
省略 ......- 下面执行insert插入时候,会将原有的一个间隙锁,分裂成两个(锁分裂),例如原有的GAP是加在了(1,5)上,现在插入一条记录3,则会变成(-oo,3),(3,5)这两个GAP锁:
heap_no=3上的锁从heap_no=2上分裂过来 , heap_no=3也就是session2中插入id=1这条记录 lock_rec_inherit_to_gap_if_gap_lock(block=0x000000012b837a80, heir_heap_no=3, heap_no=2)
1
2
3
4
5
6
7
8
9
10thread #39, stop reason = breakpoint 56.2
* frame #0: 0x000000010c2e94f0 mysqld`lock_rec_set_nth_bit(lock=0x00007fb758839778, i=3) at lock0priv.ic:83:3
frame #1: 0x000000010c2ecbd1 mysqld`lock_rec_add_to_queue(type_mode=546, block=0x000000012b837a80, heap_no=3, index=0x00007fb7587c6c88, trx=0x000000012b2bf060, we_own_trx_mutex=false) at lock0lock.cc:1538:9
frame #2: 0x000000010c2ee956 mysqld`lock_rec_inherit_to_gap_if_gap_lock(block=0x000000012b837a80, heir_heap_no=3, heap_no=2) at lock0lock.cc:2656:7
frame #3: 0x000000010c2ee856 mysqld`lock_update_insert(block=0x000000012b837a80, rec="\x80") at lock0lock.cc:3417:3
frame #4: 0x000000010be41bb2 mysqld`btr_cur_optimistic_insert(flags=0, cursor=0x00007000070ce478, offsets=0x00007000070ce448, heap=0x00007000070ce558, entry=0x00007fb7587d22c8, rec=0x00007000070cdcc8, big_rec=0x00007000070cdcc0, n_ext=0, thr=0x00007fb75797d350, mtr=0x00007000070ce8a8) at btr0cur.cc:2928:5
frame #5: 0x000000010c46f606 mysqld`row_ins_sec_index_entry_low(flags=0, mode=2, index=0x00007fb7587c6c88, offsets_heap=0x00007fb75680fc18, heap=0x00007fb75681d218, entry=0x00007fb7587d22c8, trx_id=0, thr=0x00007fb75797d350, dup_chk_only=false) at row0ins.cc:3004:11
frame #6: 0x000000010c471fb9 mysqld`row_ins_sec_index_entry(index=0x00007fb7587c6c88, entry=0x00007fb7587d22c8, thr=0x00007fb75797d350, dup_chk_only=false) at row0ins.cc:3200:9
frame #7: 0x000000010c47ce56 mysqld`row_ins_index_entry(index=0x00007fb7587c6c88, entry=0x00007fb7587d22c8, multi_val_pos=0x00007fb75797d130, thr=0x00007fb75797d350) at row0ins.cc:3300:13
省略 ......这样就成了我们上面看到的结果了。
- 锁继承是当原有记录被删除时,需要将原记录上的GAP属性继承给下一条记录。例如:表中有两条记录(1,2),原有的GAP锁加在(-oo,1)上,当记录1被删除后,要保证GAP锁能继续起到锁住这段范围的作用,就会将GAP锁继承给记录2,也就是变成了(-oo,2)。
结语
- 这块内容也是看了很多大牛的文章和资料,根据自己对这块知识存在的疑问做了个总结。当然其中还是有很多地方也不是很明白,还需要多看多试验才可以。由于水平有限文章必然也会存在错误,还望大家能够指出问题。
参考文章
- http://mysql.taobao.org/monthly/2017/12/02/ –MySQL · 引擎特性 · Innodb 锁子系统浅析
- http://mysql.taobao.org/monthly/2016/01/01/ –MySQL · 引擎特性 · InnoDB 事务锁系统简介
- http://mysql.taobao.org/monthly/2016/06/01/ – MySQL · 特性分析 · innodb 锁分裂继承与迁移
- https://zhuanlan.zhihu.com/p/52098868 –MySQL RC级别下并发insert锁超时问题 - 现象分析和解释
- https://zhuanlan.zhihu.com/p/52100378 –MySQL RC级别下并发insert锁超时问题 - 源码分析
- https://zhuanlan.zhihu.com/p/52234835 –MySQL RC级别下并发insert锁超时问题 - 案例验证
- http://mysql.taobao.org/monthly/2015/06/02/ – MySQL · 捉虫动态 · 唯一键约束失效
- https://www.jianshu.com/p/1e1e13f8ec27 –MySQL:一个死锁分析 (未分析出来的死锁)