当前位置: 首页 > news >正文

MySQL RC隔离级别惊现间隙锁:是bug吗?

在MySQL的默认事务隔离级别——读已提交(Read Committed, RC)中,开发者普遍认为不会出现间隙锁(Gap Lock)。这一认知源于RC级别的设计原则:仅通过行锁确保已提交数据的可见性,而将幻读问题交由应用层处理。然而,近期多个生产环境却报告了反常现象——RC级别下竟触发了Gap Lock!这不仅导致事务意外阻塞,更动摇了我们对隔离级别底层行为的理解。

问题来源

“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官网,下面是官网的描述:

image.png

虽然说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);

现在数据库的数据是这样子的

idcodeother
111
333
555

第二步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; 看下死锁日志

image.png

从死锁日志可以看到插入意向锁是被对方的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 的时候, 分成了两个阶段

  1. 判断当前的物理记录上是否有冲突的record(delete mark 为不冲突)
  2. 如果没有冲突, 那么可以执行插入操作

这里在第一步 和 第二步 之间必须有锁来保证, 否则第一步 判断没有冲突可以插入的时候, 但是在第一步和第二步 之间另外一个事务插入了一个冲突的record, 那么第二步 再插入的时候其实是冲突了.

所以当前的实现如果gap 上存在至少一个相同的record(包括删除但是还没被回收的记录,因为删除只是做了个删除的墓碑标识,后面再回收), 那么需要给整个range 都加上gap X lock, 加了gap X lock 以后就可以禁止其他事务在这个gap 区间插入数据, 也就是通过lock 来保证第一步和第二步的原子性.

假设在code 这个唯一索引的数据是这样子的,总共有两个数据页,page1通过point 指针指向下一个数据页page2。红色带有delete mark 是代表这个数据已经被删除了,由于还没有给purge线程回收,因此还是在page 上,只是做了个删除的墓碑标识。绿色代表是正常的数据

image.png

现在我们有两个线程分别执行以下语句

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 的方式来保证唯一的正确性

image.png

http://www.lryc.cn/news/572186.html

相关文章:

  • 如何在中将网络改为桥接模式并配置固定IP地址
  • openLayers切换基于高德、天地图切换矢量、影像、地形图层
  • Zabbix监控系统安装部署(图文)
  • Linux简单了解以及VM虚拟机的安装使用(后端程序员)
  • 探秘阿里云EBS存储:云计算的存储基石
  • LINUX 619 NFS rsync
  • 深度学习-164-MCP技术之开发本地MCP服务器和异步客户端
  • LTC3130EMSE#TRPBF ADI电子元器件深度解析 物联网/工业传感器首选!
  • AWS GuardDuty邮件推送设置
  • 刘波卸任OPPO法定代表人、经理等职务,段要辉“接棒”
  • C++ 互斥量
  • 【Python】python系列之函数作用域
  • 微信小程序获取指定元素,滚动页面到指定位置
  • Maven镜像
  • ssh配置sftp,实现上传下载文件
  • uni-app总结4-项目配置+HBuilder插件使用+uni插件使用
  • 正则表达式一些例子
  • 视频续播功能实现 - 断点续看从前端到 Spring Boot 后端
  • 【Bug:docker】--Docker同时部署Dify和RAGFlow出现错误
  • web3 浏览器注入 (如 MetaMask)
  • 无人机电调技术要点与突破解析!
  • 插值与模板字符串
  • 宇鹿家政服务系统小程序ThinkPHP+UniApp
  • Spring Boot 工程启动以后,我希望将数据库中已有的固定内容,打入到 Redis 缓存中,请问如何处理?
  • WEB安全--WAF的绕过思路
  • Flutter中FutureBuilder和StreamBuilder
  • 对gateway和nocas的理解
  • DeepSeek 助力 Vue3 开发:打造丝滑的日历(Calendar),日历_项目里程碑示例(CalendarView01_22)
  • 局域网即时通讯软件评测:4款支持私有化部署的通讯软件对比
  • 2025 Java EasyExcel 基于Excel模板填充数据 SpringBoot+Mybatis-Flex