MySQL的next-key lock
InnoDB引擎为了解决幻读带来的问题,引入了间隙锁。而间隙锁和行锁组合起来叫做next-key lock,他是一个左开右闭的区间,代表锁住对应数据行以及数据行之间的间隙。
比如现在有两行数据,(5,5,5)和(10,10,10)。在可重复读得隔离级别下执行如下sql ,,
1 | select * from table where id = 7 |
就会锁住(5, 10]之间的间隙,以及第(10,10,10)这一行。因为锁是加在索引上面,而索引是有序的,所以需要保证这两条数据间没有新的数据插入。
在《MySQL45讲》中给出了加锁的一些规则:
- 原则1:加锁的基本单位是next-key lock。希望你还记得,next-key lock是前开后闭区间。
- 原则2:查找过程中访问到的对象才会加锁。
- 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
- 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候(比如查询id = 5,而最后一个值是id = 10,那么就会变为间隙锁),next-key lock退化为间隙锁。
- 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
假如现在有如下表:
1 | CREATE TABLE `t` ( |
下面我们看一些具体的例子。
等值查询间隙锁
课程中讲的是:
1、根据原则1,加锁单位是next-key lock,session A加锁范围就是(5,10];
2、同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁的范围是(5,10)。
所以插入(8,8,8)失败,而update10成功。
这里有一个疑问:为什么没有根据优化1,将(5,10]的next-key lock转变为行锁呢?
个人理解:虽然是唯一索引的等值操作,但是查询条件是id = 7,而表中的值是id = 5和 id = 10这两条记录,并不相等,所以没有退化成行锁。
非唯一索引等值锁
这个例子中,由于数据是(0,0,0)(5,5,5)(10,10,10),而查询条件是c = 5,所以会先在(0,5]之间加上next-key lock。
但是c是非唯一索引,所以需要遍历到不满足条件的为止,也就是说会访问到c = 10这一行。所以还会给(5,10]加上锁,但是该锁最后一行c = 10并不满足c = 5的查询条件,所以会退化为间隙锁,也就是(5,10),并不锁c = 10这一行。
而这个查询语走索引c,却只查询了id,恰好是覆盖索引,并不会回表,也就是说只锁了索引c,而没有锁索引id,那么意味着sessionB要操作的数据并没有加锁,所以B成功了,而C失败了。
主键索引范围锁

sessionA会先找到id = 5这一行,然后是id = 10这一行,所以会加(5,10]的next-key lock。但是id = 10满足了查询条件,所以这个锁会退化为行锁,只锁id = 10这一行。
但是查询还有第二个条件id < 11,则会继续往后走,查找到id = 15这一行。所以会加(10,15]的next-key lock。由于查询条件id < 11在id = 15这一行不满足,所以无法退化成间隙锁,也即是说15这一行也会被锁住。就会导致如图的情况。
非唯一索引范围锁

这个例子中,由于查询所有的值,所以走索引c需要回表。意味着不仅要锁索引c,也要锁id。
在第一次用c=10定位记录的时候,索引c上加了(5,10]这个next-key lock后,由于索引c是非唯一索引,没有优化规则,也就是说不会变为行锁,因此最终sesion A加的锁是,索引c上的(5,10] 和(10,15] 这两个next-key lock。
所以插入8 和修改c = 15也都会被阻塞。
非唯一索引上存在”等值”的例子
假如新插入了一行记录(30,10,30)那么索引c就会变为如下:

那么如下操作:
这里会加5,10以及10,15的锁,然后不包括端点。