说明
mysql的innodb引擎中有七种锁:
- S锁(Share Lock)共享锁,所有类似于
select * from t where id = 1 lock in share mode
或者update t set c = c+1 where id = 1
的形式,行级锁; - X锁(Exclusive Lock)排他锁,所有
select * from t where id = 1 for update
的形式。行级锁; - IS锁,意向共享锁,在获取S锁之前加的锁,表级锁;
- IX锁,意向排他锁,在获取X锁之前加的锁,表级锁;
- Record Lock记录锁,行级锁的真正实现;
- Gap Lock间隙锁,锁住一个范围;
- Next-key Lock临键锁,Gap Lock+Record Lock的组合;
笔记记载的内容是在版本5.7.24和8.0.13验证通过,以后其他版本的MySql的加锁策略可能会变化。“临键锁”称呼来源于“架构之路”公众号。
加锁策略
加锁锁住的是什么?
索引
注意:MySql只有在RR的隔离级别下才有gap lock和next-key lock。
- 原则1:加锁的基本单位是Next-key Lock(左开右闭区间);
- 原则2:查找到的对象才会加锁;
- 优化1:索引上的等值查询,给唯一索引加锁的时候,会退化为行锁;
- 优化2:索引上的等值查询,向右遍历最后一个值不满足等值条件时,next-key lock会退化为间隙锁。
- 一个不合理的地方:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
案例分析
建表语句:
1 | CREATE TABLE `t` ( |
id | c | d |
---|---|---|
0 | 0 | 0 |
5 | 5 | 5 |
10 | 10 | 10 |
15 | 15 | 15 |
20 | 20 | 20 |
25 | 25 | 25 |
案例一:唯一索引等值查询
1 | --session A |
上面开启了三个会话分别执行了三个动作。
分析:
- 原则1:加锁区间为:(5,10];
- 原则2:id=7的不存在,所以这一行没有锁;
- 优化1:id=7的行没有锁,不满足优化1;
- 优化2:id=7,与(5,10]的最后一个条件不相等,退化为gap lock。
综上:所以session A加了gap lock(5,10),session B会阻塞,C会成功。
案例二:非唯一索引等值查询
1 | --session A |
分析:
- 原则1:索引C加了next-key lock(0,5];
- 原则2:c=5存在,所以索引c=5加锁;
- 优化1:由于索引覆盖(不访问id索引就可以拿到id的值),虽然C=5索引加锁,但是id=5的索引实际并没有加锁,不满足优化1;
- 优化2:非唯一索引需要向右继续查找,直到10,所以加上next-key lock(5,10],由于10不满足c=5的条件,所以next-key lock退化为gap lock(5,10)。
所以session B的操作会成功,C会失败。
如果想避开索引优化,可以使用select id from t where c = 5 for update
,在加锁时会直接锁住主键索引,或者查询一些其他字段。
案例三:唯一索引范围查询
1 | --session A |
分析:
- 原则1:next-key lock(5,10]和(10,15];
- 原则2:查找过程中找到了id为10这一行,加了行锁;
- 优化1:由于给唯一索引加锁,所以next-key lock(5,10]退化为id=10的行锁;
- 优化2:不满足等值查询的条件;
所以上面操作的结果是session B的第一个插入操作会成功,第二个插入操作会阻塞;
session C的操作会阻塞。
案例四:非唯一索引范围查询
1 | --session a |
分析:
- 原则1:next-key lock(5,10]和(10,15];
- 原则2:由于查找到10,所以对10加锁;
- 优化1:由于不是唯一索引,因此(5,10]不会退化;
- 优化2:由于不是等值查询,所以(10,15]不会退化;
所以上面的操作是session b阻塞,session c也阻塞。
案例五:唯一索引范围锁bug
对应上面加锁策略中的不合理之处。
1 | --session a |
分析:
- 原则1:范围查询next-key lock(10,15];
- 原则2:搜索到id=15这一行,所以这一行数据要加锁;
- 优化1:id=15满足等主键索引值查询,所以next-key lock(10,15]退化为行锁;
- 优化2:满足等值查询,所以不会退化为gap lock;
- 由于id是唯一索引,所以理论来说没必要再往后搜索,但是实际上还有一个next-key lock(15,20];
所以session b会锁住,session c也会锁住。
案例六:非唯一索引存在”相等值“
首先插入一条数据:
1 | mysql> insert into t values(30,10,30); |
数据库测试有两条c=10的数据了。即:
id | c | d |
---|---|---|
0 | 0 | 0 |
5 | 5 | 5 |
10 | 10 | 10 |
15 | 15 | 15 |
20 | 20 | 20 |
25 | 25 | 25 |
30 | 10 | 30 |
此时索引C的状态是:
{5-5}-{10-10}-{10-30}-{15,15}-……
执行SQL如下:
1 | --session a |
分析:
- next-key lock(5,10];
- c=10这一行加锁;
- 非唯一索引,不满足退化条件;
- 满足条件,向右查询第一个不等的值即c=15,加上next-key lock(10,15],由于15!=10,所以退化为gap lock(10,15);
所以上述操作是session b阻塞,c成功。
案例七:limit加锁
1 | --session a |
分析:
同案例六,但是在优化2的时候,由于limit 2的存在,所以在遍历到(30,10,30)这一行的时候就停止遍历了,所以session b的结果是成功。
案例八:死锁
1 | --session a |
分析:
session a:
- next-key lock(5,10];
- c = 10加锁;
- 不满足优化1;
- 满足优化2,加上next-key lock(10,15]并退化为gap lock(10,15)。
session b:
- 获取gap lock(5,10)成功,再获取c = 10的record lock时进入等待;
session a:
插入c = 8的行被死锁,a b进入互相等待。
这个案例说明:next-key lock是由gap lock和record lock组成的,而且是分两部分获取的。
案例九:排序对锁的影响
1 | --session a |
分析:
由于是order by c desc,所以遍历时第一个是c=20以及最右边的行:
- next-key(15,20](20,25],其中(20,25]会退化为(20,25)gap lock;
- c=15,c=20,c=10加锁,同时id=10,15,20也会枷锁;
- 由于不是唯一索引,所以不满足优化1;
- 在索引c上向左遍历,直到c=10才会停止(因为15的左边是10,为了保证10的安全还会对(5,10]加锁),所以next-key lock还包括(5,10]。
案例十:多索引等值查询
首先创建表:
1 | CREATE TABLE z ( |
1 | ----session a |
分析:
- next-key lock(4,6];
- b=6这一行加锁;
- 不满足;
- 满足,加gap lock(6,8);
- 需要牢记的是:这个锁是加在b索引上的,而索引都是有序的,所以我们很轻易得到下图:
所以session b的结果是:
1 | INSERT INTO z VALUES (2, 4);--success |
参考
架构师之路:
https://mp.weixin.qq.com/s/y_f2qrZvZe_F4_HPnwVjOw
案例十详解参见其他博客: