MySQL 加行锁的规则
实验环境
MySQL 的版本是 8.0.30
这里说下这是我原来测试其它功能的表,如果觉得数据太多不好分析可以把数据弄个 4-5 条就可以了,初始化数据的 sql 如下:
1 | CREATE TABLE `student` ( |
主键索引等值查询存在的记录
执行SELECT * FROM student WHERE id = 36 LOCK IN SHARE MODE;
,之后执行 SELECT * FROM performance_schema.data_locks;
查看加锁情况(后面不会再重复查看加锁的语句):
这里得出的结论是:使用主键索引等值查询,并且这条记录存在,只会对主键索引上的数据加 record 锁
主键索引等值查询不存在的记录
执行SELECT * FROM student WHERE id = 33 LOCK IN SHARE MODE;
加锁的情况如下:
这里得出的结论是:使用主键索引等值查询,并且这条记录不存在时,会在所在条件所在的间隙加 gap 锁(例子里面锁的区间是
(33,36)
)
这里有个特殊情况就是如果数据落在表中最大值和 supremum
之间,比如执行SELECT * FROM student WHERE id = 37 LOCK IN SHARE MODE;
,那么加锁的范围会是(36,supremum]
,个人认为这里不用 gap 锁而是用 next-key 锁的原因是加锁的基本单位是 next-key 锁,只是根据情况降级到 record 锁和 gap 锁,但是在这里给 supremum
加不加 record 锁没区别,所以就不需要锁降级
主键索引范围查询并且范围内存在记录
执行SELECT * FROM student WHERE id > 29 AND id < 33 LOCK IN SHARE MODE;
,加锁情况如下:
执行SELECT * FROM student WHERE id >= 29 AND id < 33 LOCK IN SHARE MODE;
,加锁情况如下:
执行SELECT * FROM student WHERE id > 29 AND id <= 33 LOCK IN SHARE MODE;
,加锁情况如下:
执行SELECT * FROM student WHERE id >= 29 AND id <= 33 LOCK IN SHARE MODE;
,加锁情况如下:
这里得出的结论是:使用主键索引范围查询,并且范围内存在记录,会对符合条件的记录加 next-key 或者 record 锁,对剩余未加锁的间隙的下一条记录加 gap 锁
不过有一种特殊情况,执行SELECT * FROM student WHERE id > 29 AND id <= 36 LOCK IN SHARE MODE;
,加锁情况如下:
按理来说不应该给(36,supremum]
加锁,
在《MySQL 实战 45 讲》中作者给行锁加锁规则总结了“两个原则”、“两个优化”和“一个 bug”(《MySQL 实战 45 讲》的作者说的是截止到当时最新的 MySQL 版本:5.x 系列<=5.7.24,8.0 系列 <=8.0.13),其中的一个 bug 就是唯一索引上的范围查询会访问到不满足条件的第一个值为止
,也就是在上面的例子中虽然扫描到了id=36
的索引,还要继续向后扫描,所以还要对supremum
加 next-key 锁(虽然 MySQL 说在 8.0.18 版本已经修复这个bug(关键词 Bug #29508068),但是对于一部分的查询还是有这个 bug,就如上面的例子的之后的非主键唯一索引和非唯一索引范围查询)。
主键索引范围查询并且范围内不存在记录
执行SELECT * FROM student WHERE id > 33 AND id < 36 LOCK IN SHARE MODE;
,加锁情况如下:
这里得出的结论是:使用主键索引范围查询,并且范围内不存在记录,会对查询条件所在范围的下一条记录加 gap 锁
有一种特殊情况,执行SELECT * FROM student WHERE id > 36 LOCK IN SHARE MODE;
,加锁情况如下:
这里可以也是上面说的不需要降级所以给加的是 next-key 锁
非主键唯一索引等值查询存在的记录
执行SELECT * FROM student WHERE name = '刘备4' LOCK IN SHARE MODE;
,加锁情况如下:
这里得出的结论是:使用非主键唯一索引等值查询,并且这条记录存在时,会对非主键唯一索引上查到的记录加 record 锁,还会给对应的主键索引的记录加上 record 锁
还有特殊情况,执行SELECT id, name FROM student WHERE name = '刘备4' LOCK IN SHARE MODE;
,加锁情况如下:
执行SELECT id, name FROM student WHERE name = '刘备4' FOR UPDATE;
,加锁情况如下:
发现如果只是使用共享锁并且是索引覆盖查询是不需要对主键索引对于的记录加 record 锁的,但是排它锁查询是不管是否索引覆盖都给对应的主键索引加 record 锁
非主键唯一索引等值查询不存在的记录
执行SELECT * FROM student WHERE name = '刘备3' LOCK IN SHARE MODE;
,加锁情况如下:
这里得出的结论是:使用非主键唯一索引等值查询,并且这条记录不存在时,会对非主键唯一索引上查询条件所在间隙的下一条记录加 gap 锁
又是锁不需要降级所以是加 next-key 锁的特殊情况,执行SELECT * FROM student WHERE name = '鲁班1' LOCK IN SHARE MODE;
非主键唯一索引范围查询并且范围内存在记录
执行SELECT * FROM student WHERE name > '刘备' AND name < '刘备4' LOCK IN SHARE MODE;
,加锁情况如下:
执行SELECT * FROM student WHERE name >= '刘备' AND name < '刘备4' LOCK IN SHARE MODE;
,加锁情况如下:
执行SELECT * FROM student WHERE name > '刘备' AND name < '刘备4' LOCK IN SHARE MODE;
,加锁情况如下:
执行SELECT * FROM student WHERE name >= '刘备' AND name <= '刘备4' LOCK IN SHARE MODE;
,加锁情况如下:
如果只是看非主键唯一索引加锁的情况会发现 bug
name < '刘备4'
原本应该锁的是(刘备2,刘备4)
,但是现在多出了一个 record 锁,变成了(刘备2,刘备4]
name <= '刘备4
原本应该锁的是(刘备2,刘备4]
,但是现在多出来一个(刘备4,刘永]
name > '刘备'
原本应该就只是给刘备
这条记录加 record 锁,但是多出来了一个 gap 锁,变成了(关羽,刘备]
这就是前面主键索引范围查询并且范围内存在记录中说的那个 bug 导致的
然后再看主键索引上的加锁情况发现主键索引上加锁是正常的
name < '刘备4'
在非主键唯一索引上加锁的范围是(刘备2,刘备4]
,但是主键索引上并未对刘备4
对应的记录 record 锁name <= '刘备4
在非主键唯一索引上加锁的范围是(刘备4,刘永]
,但是主键索引上并未对刘永
对应的记录 record 锁
这里得出的结论是:使用非主键唯一索引范围查询,并且范围内存在记录,会对符合条件的记录加 next-key(上面的
name > '刘备'
的 bug),会对不满足条件的第一个值记录加上 next-key 锁(上面的name < '刘备4'
和name <= '刘备4
),在主键索引上,会对在查询范围内记录对应的主键索引上加 record 锁
而且上面的这个现象还和你索引的排序有关,如果你执行把索引idx_name
改成降序,再执行SELECT * FROM student WHERE name >= '刘备' AND name <= '刘备4' LOCK IN SHARE MODE;
,加锁就会变成下面这种情况:
非主键唯一索引范围查询并且范围内不存在记录
执行SELECT * FROM student WHERE name > '刘备1' AND name < '刘备2' LOCK IN SHARE MODE;
,加锁情况如下:
这里面加锁逻辑其实和上面的非主键唯一索引范围查询并且范围内存在记录
加锁的一样的,都有那个 bug 的问题,区别就是查不到记录所以不会给主键索引加锁
这里得出的结论是:使用非主键唯一索引范围查询,并且范围内不存在记录,会对查询条件所在间隙的下一条记录加 next-key 锁
非唯一索引等值查询存在的记录
执行SELECT * FROM student WHERE age = 12 LOCK IN SHARE MODE;
,加锁情况如下:
这里得出的结论是:使用非唯一索引等值查询,并且这条记录存在时,在非唯一索引上,会对符合查询条件的记录加 next-key 锁,会对不满足条件的第一个值记录加上 gap 锁,在主键索引上,对符合查询条件的记录在主键索引对应的位置上加 reocrd 锁
非唯一索引等值查询不存在的记录
执行SELECT * FROM student WHERE age = 18 LOCK IN SHARE MODE;
,加锁情况如下:
这里得出的结论是:使用非唯一索引等值查询,并且这条记录不存在时,在非唯一索引上,会对符合查询条件所在间隙的下一条记录加 gap 锁
这里也有那个锁没降级的情况,不在赘述了
非唯一索引范围查询并且范围内存在记录
执行SELECT * FROM student WHERE age >= 12 AND age < 13 LOCK IN SHARE MODE;
,加锁情况如下:
执行SELECT * FROM student WHERE age > 12 AND age <= 13 LOCK IN SHARE MODE;
,加锁情况如下:
执行SELECT * FROM student WHERE age >= 12 AND age <= 13 LOCK IN SHARE MODE;
,加锁情况如下:
如果只是看非唯一索引加锁的情况会发现 bug
age < 13
原本应该锁的是(12,13)
,但是现在多出了一个 record 锁变成了(12,13]
age <= 13
原本应该锁的是(12,13]
,但是现在多出了一个 next-key 锁(13,17]
这就是前面主键索引范围查询并且范围内存在记录中说的那个 bug 导致的
然后再看主键索引上的加锁情况发现主键索引上加锁是正常的
age < 13'
在非唯一索引上加锁的范围是(12,13]
,但是主键索引上并未对13
对应的记录 record 锁age <= 13
在非唯一索引上加锁的范围是(13,17]
,但是主键索引上并未对17
对应的记录 record 锁
这里得出的结论是:使用非唯一索引范围查询,并且范围内存在记录,会对符合条件的记录加 next-key,会对不满足条件的第一个值记录加上 next-key 锁(上面的
age < 13'
和age <= 13
),在主键索引上,会对在查询范围内记录对应的主键索引上加 record 锁
不过这里要说一下age > 12
的情况,因为 age 是 12 的有多条,但是这里(12,13]
的 12 这条记录对应的是 id 最大的那条,这里对应的就是 id=28 的这条,原因也很简单因为我的索引是升序排序的,如果相同的情况下会根据 id 再升序排序,如果你这里把索引的排序改成降序,那么索引值相同的情况下还是会根据 id 升序排序,下面把 age 的排序改成降序,执行SELECT * FROM student WHERE age > 12 AND age <= 13 LOCK IN SHARE MODE;
后加锁情况如下:
如果是 age 相同的情况下是按照 id 降序的话,这里应该锁显示的 LOCK_DATA 显示的是12,28
所以根据上面的情况可以发现age > 12
并且索引升序的情况下可以插入 age=12 的记录只是要求 id 小于最大的值就行,在这里就是 id 小于 28,age < 13
也是一样的逻辑只是变成 id 要大于最小值
非唯一索引范围查询并且范围内不存在记录
执行SELECT * FROM student WHERE age > 12 AND age < 13 LOCK IN SHARE MODE;
,加锁情况如下:
这里面加锁逻辑其实和上面的非唯一索引范围查询并且范围内存在记录
加锁的一样的,都有那个 bug 的问题,区别就是查不到记录所以不会给主键索引加锁
这里得出的结论是:使用非唯一索引范围查询,并且范围内不存在记录,会对查询条件所在间隙的下一条记录加 next-key 锁
不使用索引查询
分别执行
SELECT id,name FROM student WHERE test1 = 2 LOCK IN SHARE MODE;
SELECT id,name FROM student WHERE test1 > 2 LOCK IN SHARE MODE;
加锁情况如下:
这里图片截不全,表中每一条记录包括最大界限伪记录 supremum 都加了锁 next-key 锁
这里得出的结论是:不使用索引查询时,无论是等值查询还是范围查询,无论是否存在查询结果,都会对表中所有的记录加 next-key 锁,也就是我们常说的“锁表”