MySQL间隙锁(幻读解决原理)

专栏持续更新中:MySQL详解

一、间隙锁概念

  • 当我们用范围条件而不是相等条件检索数据, 并请求共享或排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做"间隙(GAP)" ,InnoDB 也会对这个"间隙"加锁,这种锁机制就是所谓的间隙锁
  • 举例来说, 假如 user 表中只有 101 条记录, 其userid 的值分别是 1,2,...,100,101, 下面的 SQL: select * from user where userid > 100 for update;是一个范围条件的检索,InnoDB 不仅会对符合条件的 userid 值为 101 的记录加锁,也会对userid 大 于 101(但是这些记录并不存在)的"间隙"加锁,防止其它事务在表的末尾增加数据
  • InnoDB 使用间隙锁的目的,是为了防止幻读,以满足串行化隔离级别的要求 ,对于上面的例子,要是不使用间隙锁,如果其他事务插入了 userid 大于 100 的任何记录,那么本事务如果再次执行上述语句,就会发生幻读

InnoDB串行化隔离级别使用间隙锁(gap lock)解决幻读(事务并发情况下两次查询的数据量不同)问题

间隙锁专用于串行化隔离级别,可解决幻读问题,幻读问题表现为:当前事务没做操作,前后两次相同的查询语句,显示的数据量不一致

我们把事务2 select的指定的条件分为2类:范围查询、等值查询

record lock:记录锁,就是行锁

gap lock:间隙锁,不包含记录本身

next-key lock:record lock(记录本身) + gap lock(不包括记录本身)

二、测试间隙锁范围加锁

设置事务为手动提交,然后把隔离级别设置成串行化

查看表结构,id、age、name都有索引

场景1:用不可重复的主键id测试间隙锁

范围查询

事务2的select操作只给三行数据加了共享锁,为什么插入id为24的数据也不行呢?

这是因为在串行化隔离级别中,不仅仅是获取了满足条件的这3行的行锁,而且把表记录之间以及后边空洞的地方也加上了间隙锁

图中红色线的地方都上了next-key锁,上锁范围(左开右闭)为:( 11 , 12 ] ∪ ( 12 , 22 ] ∪ ( 22 , 23 ] ∪ ( 23 , + ∞ ]

上述select不仅仅获取了12,22,23的共享行锁(record-lock),还把间隙加了间隙锁,其实就是给间隙加上共享锁或者排他锁,由于我们这里是select,所以是给间隙加上了共享锁(我们select id>11还是可以的,不能update、insert、delete id>11的数据)

也就是说,我们可以在id小于11的地方update/delete,加排它锁。但是操作了id<=11的部分,不影响相同的select * from stu where id>11所获取的数据量,这样就能防止幻读发生

串行化隔离级别通过给select的部分加间隙锁,防止其他事务在加了间隙锁的区间进行增加或删除数据,就能防止幻读

场景2:用可重复的age(有索引)测试间隙锁

测试辅助索引树上,间隙锁的范围

我们先查看一下表结构、表数据,然后回滚

开启事务进行测试

很明显,由于age>20的区间都被事务1加上了间隙锁(这里加的是共享锁),所以事务2插入age=22和age=21都失败了

幻读就是同一事务两次用相同的条件查询数据,下一次查出的数据量和上一次的数据量不一样,就算事务1把age=20的数据插入表,事务2再用age>20查询,得到的数据量也不会改变。

那事务1插入age=20的数据能否成功呢?

依然不能成功,这是因为我们插入的数据id是自增的,所以这条数据(age=20,id=24),位于辅助索引树中(age=20,id=12)的右边,由于(age=20,id=12)右边都被上了锁,(age=20,id=24)自然无法插入

也就是说,如果我们指定age=20,id合法且<12,则可插入

很显然,事务1插入的age=18和age=19都不在事务2上锁的范围,所以可以插入

如果只是在辅助索引树上查找,不回表,那么主键索引树上不会加锁

select id from stu where age>14后,辅助索引树加锁区间如下:

场景3:实际情况需要具体分析用的到底是行锁还是表锁

回滚,重新开启事务

开始测试

我们发现事务1无论是插入age>18范围内的数据,还是范围外的数据,都无法成功

这时我们就要分析了,这应该没有用到索引,因为我们用索引,过滤出的数据占了整张表的一大半,MySQL server没使用索引。

没有加行锁,只能加表锁(这时加的是共享锁),所以事务1无论插入什么数据都不行

果然,没有用到索引

age>20用到了索引,所以可以用行锁

三、测试等值间隙锁

查看表结构和表数据

设置成手动提交,设置串行化隔离级别。回滚,然后启动事务

1. 测试不能重复的主键索引

此时事务2 select,由于是等值查询,相当于给这条数据加上了共享锁

事务1现在插入新的数据是可以成功的,因为主键id不能重复,我们不能再插入主键id=9的数据

在这种情况下,由于id=9的数据已经存在,主键和唯一键是不能重复的,事务2进行等值查询时,事务1插入一个新的数据,不用担心这条新插入的数据和查询条件是一样的,如果主键一样,SQL语句执行失败,所以肯定能成功

2. 测试能重复的辅助索引

回滚并重启事务

事务2等值查询,给age=18这行数据加上共享锁(record-lock)

事务1插入age=18,这是不能允许的,否则事务2再查询age=18就有两条记录了(幻读)

奇怪的是,我们插入age=17、16、15都被阻塞了,而14、13成功了

这是因为,为了防止幻读,除了age=18这条数据加了共享锁,其两侧也被加上了间隙锁,因为在这种情况下,插入(age=18,id=10)和(age=18,id=8)是会发生幻读的,所以在一切会影响select * from user where age=18查询结果的地方都加上了间隙锁,但这也会导致一些本不影响查询结果的语句也执行失败,比如插入(age=17,id=24)虽然不影响上述SQL执行结果,由于在间隙锁范围内,依然无法插入

如果插入(age=15,id=1)就可以成功,根据辅助索引值相同,按照主键值升序排列,(age=15,id=1)应该放在(age=15,id=23)前面,不在间隙锁范围内

间隙锁是给不存在的数据记录的范围加锁:

  • 对于辅助索引,若值允许重复,在串行隔离级别中如果进行等值查询,InnoDB会给数据加上record-lock和gap-lock(防止别的事务插入索引值重复的数据,造成幻读)
  • 对于主键索引,或者唯一键索引,值不允许重复,那只需要加行锁就够了,不需要再加间隙锁(对于唯一键索引,不可能发生插入索引值重复的数据)

串行化隔离级别通过排它锁和共享锁解决脏读、不可重复读(两次查询的数据内容不同),通过间隙锁解决幻读(两次查询的数据量不同)

相关推荐
正儿八经的少年5 分钟前
redis 的大 key 和热 key 详解
数据库·redis·缓存
AI砖家9 分钟前
多智能体系统实战:架构设计、数据库表设计与 Skill 体系
数据库·多智能体·skill·agent架构设计·agengt
止语Lab30 分钟前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活30 分钟前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困35 分钟前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn36 分钟前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
夜雪一千41 分钟前
MySQL查询条件的顺序是否影响查询效率
数据库·mysql
用户2986985301442 分钟前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户772831049084044 分钟前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端