前言有一天,突然有人问我MySQL的下一键锁,我的第一反应是:这是什么???我在这个截图中什么都看不到?仔细看,似曾相识,这不是《MySQL 45 讲》里的内容吗?1什么是next-keylocknext-keylock是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合。官网上的解释大概意思就是:anext-keylock是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合。先给自己打一连串的小问号???对主键、唯一索引、普通索引、普通字段加锁,哪些索引加锁了?不同查询条件锁定哪些范围的数据?forshareandforupdateequivalencequery和rangequery的锁范围是多少?当查询等价值不存在时,锁定范围是多少?查询条件为主键、唯一索引、普通索引有什么区别?既然你什么都不懂,那我只好从头开始练习了!来看看《MySQL 45 讲》中丁奇老师的结语:看了这个结语,大部分的问题应该都有答案了,但是有一个非常非常重要的点需要注意:后面的版本MySQL的锁策略可能会改变,所以这个规则仅限于目前最新的版本,即5.x系列<=5.7.24,8.0系列<=8.0.13所以上面的规则不一定适用对当前版本适用,我以MySQL8.0.25版本为例,多角度验证next-keylock锁定范围。2环境准备MySQL版本:8.0.25隔离级别:RepeatableRead(RR)存储引擎:InnoDBmysql>select@@global.transaction_isolation,@@transaction_isolation\Gmysql>showcreatetablet\G如何使用Docker安装MySQL,可以参考另一篇文章3条主键索引首先要验证主键索引的next-key锁的范围。此时数据库中的数据如图所示。对于主键索引,此时的数据差距如下:主键等效查询-数据存在于mysql>begin;select*fromtwhereid=10forupdate;这条SQL锁id=10。大家可以先想想加的是什么锁?哪些数据被锁定?可以通过data_locks查看锁信息。SQL如下:#mysql>select*fromperformance_schema.data_locks;mysql>select*fromperformance_schema.data_locks\G具体字段含义参考官方文档[1]结果主要包括引擎、库、表等信息.我们需要关注以下字段:INDEX_NAME:锁定索引的名称LOCK_TYPE:锁的类型。对于InnoDB,允许的值为RECORD行级锁和TABLE表级锁。LOCK_MODE:锁的类型:S、X、IS、IX和间隙锁LOCK_DATA:与锁关联的数据。对于InnoDB,当LOCK_TYPE为RECORD(行锁)时,显示该值。当锁定在主键索引上时,该值是锁定记录的主键值。当锁定在二级索引上时,将显示二级索引的值,并附加主键值。结果很明显,这里就是给表加IX锁,给主键索引id=10的记录加X,REC_NOT_GAP锁,也就是只给记录加锁。同理,forshare对表加IS锁,对主键索引id=10的记录加S锁。可以得出,当主键的等价值被加锁,且该值存在时,一个表上加意向锁,同时主键索引上加行锁。主键等值查询——数据不存在mysql>select*fromtwhereid=11forupdate;如果数据不存在,加什么锁?锁的范围是什么?在验证之前,分析数据差距。id=11肯定不存在。但是添加forupdate,则需要添加next-keylock,id=11属于开闭前的(10,15]区间;因为是等价查询,所以不需要对id为记录的加锁=15,next-keylock会退化为gaplock;开启前后最终区间为(10,15)使用data_locks分析锁信息:看锁信息X,GAP表示是gaplock补充一下,其中LOCK_DATA=15,表示锁是主键索引id=15之前的间隙。此时在另一个Session中执行SQL,答案很明显,id=12不能插入,但是id=15可以更新,可以得出结论,当数据不存在时,主键等效查询会锁定主键查询条件所在的间隙,主键范围查询(重点)>begin;select*fromtwhereid>=10andid<11forupdate;根据《MySQL 45 讲》的分析,得到如下结果:id>=10定位到更新的区间e10位于(10,+∞);因为有一个>=的等价判断,所以需要包含10的值,变成[10,+∞)再闭合再闭合区间;id<11限制后续区间,则根据11判断下一区间为15的前开后收区间;组合是[10,15]。(不完全正确)先看data_locks,可以看到除了表锁,还有id=10的行锁(X,REC_NOT_GAP)和主键索引id=之前的间隙锁(X,GAP)15.所以其实id=15是可以更新的。也就是说前开后关区间有问题。个人认为应该以id<11的条件来判断,这样就不用加锁lock15的行了。结果验证也正确,id=12插入阻塞,id=15更新成功。当范围的右侧包含等效查询时?mysql>开始;select*fromtwhereid>10andid<=15forupdate;我们来分析一下这条SQL:id>10定位到10所在的区间(10,+∞);id<=15的定位是(-∞,15];组合是(10,15)。同样先看data_locks,可以看到只加了一个主键索引id=15的X锁。验证id=15是否可以更新?验证id=16是否可以插入?结果证明没有问题!当然这里也有小伙伴会说《MySQL 45 讲》有bug,会锁下next-key。《MySQL 45 讲》第21号原来这个bug已经被修复了,修复的版本是MySQL8.0.18。但是还没有完全修复!!!参考链接地址:https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-18.html#mysqld-8-0-18-bug搜索关键词:Bug#29508068)我们可以使用8.0.17重现:MySQL8.0.17在8.0.17id<=15也会锁住id=20的数据,但是在8.0.25版本就不会了。所以这个bug已经修复了,看看是不是前开后开-closingorfront-openingandback-opening,说得更准确些,用8.0.17和8.0.18做对比。MySQL8.0.17MySQL8.0.18现在我估计大概率是8.0.18版本修复Bug#29508068时,前开后关优化为Openbeforeandafter。对比data_locks数据:注意红色下划线部分。8.0.17版本,id<17时,LOCK_MODE为X,8.0.25版本为X,GAP。4小结本文主要是通过实际操作,验证主键加锁时下一键加锁的范围,并查阅资料,通过版本对比得出不同的结论。结论一:在加锁的时候,会先给表加一个意向锁,IX或者IS;如果有多个范围加锁,则分别加多把锁,每个范围有一把锁;(这个可以在id<20下练习)主键等效查询,当数据存在时,会在主键索引的值上加一个行锁X,REC_NOT_GAP;主键等效查询,当数据不存在时,会在查询条件的主键值所在的间隙上加一个间隙锁X,GAP;主键等值查询,范围查询更复杂:8.0.17版本是前开后闭,而8.0.18及以后的版本优化了主键不等的判断,后面不会锁定闭区间。当critical<=query时,8.0.17会锁定下一个next-key的前开后闭区间,8.0.18及之后的版本修复了这个bug。优化后导致打开较晚。不知道是因为优化后,后面会直接打开主键的范围,还是因为是bug。具体的朋友可以试试。结论2通过使用select*fromperformance_schema.data_locks;以及操作时间,可以看出LOCK_MODE和LOCK_DATE的关系:LOCK_MODELOCK_DATAlockrangeX,REC_NOT_GAP1515该数据的rowlockX,GAP1515该数据之前的gap,不包括15X1515的数据gap,其中15LOCK_MODE=X是先开后关再关的间隔;X,GAP为开仓前开仓再开仓(gaplock)的区间;X,REC_NOT_GAP是行锁。基本上,主键的next-keylockrange已经算出来了。请注意,使用的版本是8.0.25。唯一索引的下一键锁定范围是多少?覆盖索引时,锁和锁定索引的范围是多少?为什么说这个bug没有完全修复,而且这个bug在非主键唯一索引中也重现了。文章篇幅有限,大家可以先自己想一想,尽量自己尝试操作,实践出真知。至于具体的答案,还需要在下一篇文章中去验证和总结。参考链接:[1]data_locks表:https://dev.mysql.com/doc/mysql-perfschema-excerpt/8.0/en/performance-schema-data-locks-table.html本文转载自微信公众号“程序员小航”,可以通过以下二维码关注。转载本文请联系程序员小航公众号。
