问题来源
“mysql RC 隔离级别也会出现gap 锁吗?我们生产环境是RC 隔离级别,出现了gap 锁导致死锁咯 ”? 上一周同事突如其来的问题彻底把我问懵了。
其实一直以来我也是认为只有RR隔离级别才会出现的,甚至我找到曾有人就这个问题给mysql 团队提了个bug ,并且mysql 团队也认为是个bug。那为啥现在RC 也会出现,甚至RU 级别也会出现呢。。我来盘下到底是怎么回事
问题背景
事情经过是这样子的,上周我们生产环境报了个死锁的问题,是并发执行insert into ... on duplicate key update 的时候报的一个死锁。死锁的日志是这样子的:
trx id 679250 lock_mode X locks gap before rec insert intention waiting
gap lock 阻塞了各自的的insert intention lock 。一眼看去这个是老生常谈的死锁现象了。慢着,我们生产环境是RC 隔离级别,一连串的疑问冒出来:RC 隔离级别怎么会出现gap lock 呢?gap lock 不是在RR隔离级别下为了解决幻读而存在的吗?直接颠覆了我的认知,不死心查几次生产环境的事务隔离级别确认都是RC 隔离级别。
难道我记错了吗?登录mysql官网,下面是官网的描述:
虽然说gap lock 可以通过改变为RC 进行禁用,但是依然在外键和唯一键的时候会用到gap lock。 um um 确实是官方说是会产生的,但是依然不明白为啥会用gap lock 呢?
网上找到一个网友跟我一样,因为这个问题曾经给mysql 提了一个bug bug73170
神奇的是mysql 当时还当成bug 修复了,但是导致二级索引的唯一键失效,又revert 掉这个fix bug 68021
似乎看起来,mysql 团队也曾经认为这是一个bug ,只是由于实现难度解决不了还是继续使用了gap lock
Anway 先重现死锁,再一步一步分析为啥唯一键的检查需要用到gap lock,为什么mysql 团队没有去掉它
问题重现
根据我同事提供的重现sql.
第一步:
CREATE TABLE `test2` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` int(11) NOT NULL,
`other` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `code` (`code`)
) ENGINE=InnoDB ;
TRUNCATE table test2;
insert ignore into test2 (id,code, other) values(1,1,1),(3,3,3),(5,5,5);
现在数据库的数据是这样子的
id | code | other |
---|---|---|
1 | 1 | 1 |
3 | 3 | 3 |
5 | 5 | 5 |
第二步session1 :
begin;
insert into test2(code, other) values (3, 4) on duplicate key update other = VALUES(other);
数据库已经存在一个3的值,所以这里会冲突并且执行更新(这里的冲突很重要,是重现问题的关键)
第三步session2:
begin;
insert into test2(code, other) values (5, 6) on duplicate key update other = VALUES(other);
数据库已经存在一个5的值,所以这里会冲突并且执行更新(这里的冲突很重要,是重现问题的关键)
以上正常执行,但是问题来了,当我在session2 继续插入(2,2)的时候,被锁阻塞住了,why? 数据库中有没有这条记录和我冲突,为啥要给我锁住,而且我是RC 隔离级别,可以允许幻读的存在,为啥给我加个gap lock?
session2:
insert into test2(code, other) values (2, 2) on duplicate key update other = VALUES(other);
这个时候这个语句被阻塞了
第四步session1:
insert into test2(code, other) values (4, 4) on duplicate key update other = VALUES(other)
这个时候死锁产生了,是不是觉得很奇怪,不管是session 2 插入的(2,2)还是session1 插入的(4,4)他们在数据库中都不存在,理论上都应该可以正常执行,但实际上却阻塞了,这不是导致并发性能下降了吗?锁粒度过大了呢?
执行show engine innodb status; 看下死锁日志
从死锁日志可以看到插入意向锁是被对方的gap lock 阻塞住导致的死锁
问题分析
二级唯一索引的
俗话说的好,你要解决一个问题,你得先去了解它,我们看看二级索引的唯一键是怎么实现的
find the B-tree page in the secondary index you want to insert the value to
assert the B-tree page is latched
equal-range = the range of records in the secondary index which conflict with your value
if(equal-range is not empty){
release the latches on the B-tree and start a new mini-transaction
for each record in equal-range
lock gap before it, and the record itself (this is what LOCK_S does)
also lock the gap after the last(equal-range)
also (before Bug #32617942 was fixed) lock the record after last(equal-range)
once you are done with all of the above, find the B-tree page again and latch it again
}
insert the record into the page and release the latch on the B-tree page.
可以看到在二级唯一索引插入record 的时候, 分成了两个阶段
- 判断当前的物理记录上是否有冲突的record(delete mark 为不冲突)
- 如果没有冲突, 那么可以执行插入操作
这里在第一步 和 第二步 之间必须有锁来保证, 否则第一步 判断没有冲突可以插入的时候, 但是在第一步和第二步 之间另外一个事务插入了一个冲突的record, 那么第二步 再插入的时候其实是冲突了.
所以当前的实现如果gap 上存在至少一个相同的record(包括删除但是还没被回收的记录,因为删除只是做了个删除的墓碑标识,后面再回收), 那么需要给整个range 都加上gap X lock, 加了gap X lock 以后就可以禁止其他事务在这个gap 区间插入数据, 也就是通过lock 来保证第一步和第二步的原子性.
假设在code 这个唯一索引的数据是这样子的,总共有两个数据页,page1通过point 指针指向下一个数据页page2。红色带有delete mark 是代表这个数据已经被删除了,由于还没有给purge线程回收,因此还是在page 上,只是做了个删除的墓碑标识。绿色代表是正常的数据
现在我们有两个线程分别执行以下语句
sesson1:
insert into test2(id,code, other) values (4,3, 4) on duplicate key update other = VALUES(other);
sesson2:
insert into test2(id,code, other) values (12,3, 12) on duplicate key update other = VALUES(other);
session 1 执行第一个步判断code 唯一索引上找到有相同value 的记录 <3,3 delete mark>,<3,10 delete mark>,<3,11 delete mark>,<3,18 delete mark>,然后分别给他们加上next-key 锁。
最后还得再<5,5> 上增加gap 锁(假如这个5,5变成很大,那么意味着锁的gap 会非常大,影响并发性能),以防止<3,19>之后的数据被插入。到这一步是没有冲突的,因为这些值都是已经被删除的,插进去后不会违背唯一索引
如果在session 1 在第一阶段和第二阶段中间,session2 并发执行,那么第一阶段也会跟session 1 一样执行,都加上gap lock。因此后面他们两个人的插入都会失败,成功避免他们都成功来导致最终的数据不一致的问题。当然也就引入了死锁的
这个时候,你可能会说,如果我只是加数据上加锁S的行锁不是更好吗?这样就可以避免锁的范围太大,导致并发低下的问题。
session 1 执行第一个步判断code 唯一索引上找到有相同value 的记录 <3,3 delete mark>,<3,10 delete mark>,<3,11 delete mark>,<3,18 delete mark>,然后分别给他们加上s锁。
session 2 并发执行,也分别给他们加上s 锁,这个时候是不冲突的。他们两个都认为数据库没有这个记录,最后他们都插入成功。最终就违反了数据库的唯一键规则,这也就是mysql 团队修复了之前说bug 后带来的问题,因此有立马revert 了,直到今天都一直保留这个RC 隔离级别依然出现gap lock 的方式来保证唯一的正确性
看完两件事
如果你觉得这篇内容对你挺有启发,我想邀请你帮我2个小忙:
- 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
- 关注公众号「面试bat」,免费和大牛咨询技术,在这个寒冬一起报团取暖吧。
参考: