在上一篇文章中,我们学习了间隙锁和下一键锁,但是不知道它们是如何加锁的,加锁的规则是什么。间隙锁的概念不太好理解,尤其是与上行锁结合使用时,在判断是否会发生锁等待时很容易出错。
今天我们来学习锁定规则。
学习前请注意以下规则仅限于版本范围:5.x系列<=5.7.24、8.0系列<=8.0.13。
锁定规则
这个锁定规则包含两个“原理”、两个“优化”和一个“bug”。
- 原理1:锁定的基本单位是下一键锁定。我希望你还记得下一键锁是一个开闭区间。
- 原则2:只有在搜索过程中访问过的对象才会被锁定。
- 优化一:对于索引的等价查询,锁定唯一索引时,下一键锁退化为行锁。
- 优化2:对于索引的等价查询,当向右遍历且最后一个值不满足相等条件时,next-key锁退化为间隙锁。
- 一个bug:唯一索引上的范围查询将访问第一个不满足条件的值。
下面以表t为例介绍这些规则。表t的建表语句和初始化语句如下。
创建表 `t` (
`id` int(11) NOT NULL,
`c` int(11) 默认为 NULL,
`d` int(11) 默认为 NULL,
主键(`id`),
键“c”(“c”)
) 引擎=InnoDB;
插入 t 值(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
情况1:等效查询间隙锁
图1 等效查询间隙锁
分析:
- 第1步:根据原理1,添加锁(5, 10]
- 第2步:根据优化2,id=10不满足查询条件,因此降级为间隙锁(5, 10)
结论:
- 会话 B 被阻止,因为 id=8 处于间隙锁 (5, 10)
- 会话C可以执行,因为id=10的线路没有锁定
情况 2:非唯一索引相等锁
图2 非唯一索引等价锁
分析:
- 第一步:根据原理1,锁定(0, 5]
- 第2步:由于c是普通索引,所以需要继续遍历,直到找到c=10,不满足条件。根据原则2,访问的对象必须被锁定,所以必须加上(5, 10]。
- 第3步:同时根据优化2,这是一个等价查询。第一个不满足向右遍历条件的值10,(5, 10]会退化为间隙锁(5, 10)
- 所以锁定是索引c 的下一键锁(0,5]和间隙锁(5,10)
结论:
- 会话B可以执行,因为索引c被锁定,而不是主键索引
- 会话 C 被阻塞,因为 c 的插入值为 7,该值在间隙锁 (5, 10) 范围内
情况3:主键索引范围锁定
图3 主键索引范围锁
分析:
- 第1步:根据原理1,添加锁(5, 10]
- 第2步:根据优化1,退化为id=10的行锁
- 第3步:继续遍历,找到不满足id<11的值id=15,并锁定(10, 15]
因此,锁定 id=15 的行锁和 id (10, 15]的下一键锁
结论:
- 插入 id=13 被阻止:下一键锁定 (10, 15]
- 更新 id=15 被阻止:下一键锁定 (10, 15]
情况4:非唯一索引范围锁定
图4 非唯一索引范围锁
分析:
- 第1步:根据原理1,添加锁(5, 10]
- 第2步:继续遍历,找到不满足c<11的值c=15。根据原则2,锁定(10, 15]
因此锁定索引 c (5, 10] 和 (10, 15]
结论:
- 插入 c=8 并被 (5, 10] 阻挡
- 更新 c=15,被 (10, 15] 阻止
案例 5:独特的索引范围锁定错误
图5 唯一索引范围锁定bug
分析:
- 第1步:根据原理1,加锁(10, 15],然后根据优化1,退化为id=15的行锁
- 第 2 步:向右移动并锁定 (10, 15]
- 第3步:根据BUG,需要访问第一个不满足条件的值,即id=20,并锁定(15,20]。
所以锁是 (10, 15] 和 (15, 20]
结论:
- 更新 id=20 被阻止,被 (15, 20] 锁定
- 插入 id=16 被阻止,被 (15, 20] 锁定
案例 6:非唯一索引上的“等价”示例
mysql>插入t值(30,10,30);
图 6 非唯一索引上的“等价”示例
分析:
- 第1步:根据原理1,(c=5,id=5)到(c=10,id=10)这个下一键锁
- 第2步:向右搜索,直到遇到行(c=15,id=15),循环结束。根据优化 2,这是一个等效查询。不满足条件的行都在右边找到,所以会退化为间隙锁,从(c=10,id=10)到(c=15,id=15)
因此将 (c=5,id=5) 锁定到 (c=10,id=10) 这个下一键锁定,将 (c=10,id=10) 锁定到 (c=15,id=15) 这个间隙锁
结论:
- 插入c=12被间隙锁阻止并锁定,从(c=10, id=10)到(c=15, id=15)
- 更新c=15成功,c=15未锁定
情况 7:限制语句锁定
先插入一条记录。
mysql>插入t值(30,10,30);
图7 limit语句加锁
分析:
- 第1步:根据原理1,(5, 10],因为c=10有两行,所以遍历到这里结束
- 第二步:因为是删除,所以添加两个行锁(id=10和id=30)
因此锁 c (5, 10) 和两个行锁(id=10 和 id=30)
结论:
- c=12插入成功,因为c=12没有锁定
说明:这个例子对我们实践的指导意义是尝试在删除数据时添加限制。
案例8:死锁示例
这个案例的目的是为了说明:下一键锁实际上是间隙锁和行锁之和的结果。
图8 死锁示例
分析:
- 第1步:根据原理1,添加锁(5, 10]
- 第2步:继续遍历,直到c=15不满足条件,锁定(10, 15],根据优化2,退化为(10, 15)
结论:
- 会话 B 正在等待锁定。此时Session B已经添加了间隙锁(5, 10),正在等待行锁c=10。
- 会话A插入c=8,并且也在等待锁,导致死锁
说明:具体执行next-key lock时,分为两个阶段:间隙锁和行锁。
情况9:非唯一索引排序范围锁定
图9 非唯一索引排序范围锁
分析:
- 第一步:首先执行c=20并锁定(15, 20]
- 第2步:根据优化2,添加间隙锁(20, 25)
- 第3步:再次执行c=15并锁定(10, 15]
- 第四步:继续向左遍历,直到找到id=10的记录,锁定(5, 10]
扫描过程中,c=20、c=15、c=10这三行存在数值。由于是select *,所以会在主键id上加三个行锁。
因此需要锁定索引c(5, 25)和三个行锁(id=10,id=15,id=20)。
结论:
- 插入c=6,由c(5, 25)锁定
案例10:不等号条件等价查询
开始;
select * from t where id>9 and id<12 order by id desc for update;
执行过程中,通过树搜索定位记录时,采用“等价查询”的方法。
分析:
- 第一步:根据原理1,锁定(10, 15]
- 第2步:根据优化2,退化为(10, 15)
- 第3步:向左遍历,找到id=10,锁定(5, 10],继续找到id=5,锁定(0, 5]
情况11:范围锁定
开始;
select id from t where c in(5,20,10) 以共享模式锁定;
分析:
注:锁是一一添加的。
- 第 1 步:首先 c=5,添加 (0, 5] 和 (5, 10)
- 第 2 步:然后 c=10,加上 (5, 10] 和 (10, 15)
- 第 3 步:c=20 后,添加 (15, 20] 和 (20, 25)
间隙锁不是互斥的,因为锁定范围是(0, 25),除了c=15。
僵局情况
select id from t where c in(5,20,10) order by c desc 进行更新;
存在同时执行逆序语句的情况。因为它们恰好同时执行,逐渐加锁(逆序加锁),就会出现死锁的情况。
参考文献
- 21 |为什么我只改一行语句就有这么多锁?