上篇文章中,介绍了间隙锁和临键锁,但并未说明加锁规则。本文首先介绍加锁规则,由于间隙锁在可重复读隔离级别下才有效,因此接下来的内容默认在可重复读隔离级别下。
加锁规则(限5.x系列<=5.7.24, 8.0系列<=8.0.13):
-
原则1:加锁的基本单位是临键锁,是一个前开后闭区间;
-
原则2:查找过程中访问到的对象才会加锁;
-
优化1:索引上的等值查询,给唯一索引加锁的时候,临键锁退化为行锁;
-
优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,临键锁退化为间隙锁;
-
一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
后续例子用到的表:
/* by yours.tools - online tools website : yours.tools/zh/xpath.html */
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
案例一:等值查询间隙锁

由于表中没有id=7
的记录,用加锁规则判断:
-
根据原则1,加锁单位是临键锁,session A加锁范围是(5,10];
-
根据优化2,这是一个等值查询
id=7
,而id=10
不满足查询条件,临键锁退化成间隙锁,因此最终加锁范围是(5,10)。
所以session B要插入id=8
的记录会被锁住,但是session C修改id=10
这行是可以的。
案例二:非唯一索引等值锁

这里session A要给索引c上c=5
这一行加读锁:
-
根据原则1,加锁单位是临键锁,因此会给(0,5]加临键锁;
-
由于c是普通索引,因此仅访问
c=5
这一条记录不能马上停下来,需要向右遍历查到c=10
才放弃。根据原则2,访问到的都要加锁,因此给(5,10]加临键锁; -
这个遍历符合优化2,由于最后一个值不满足
c=5
这个等值条件,临键锁退化成间隙锁; -
根据原则2,只有访问到的对象才会加锁,这个查询使用覆盖索引,不需要访问主键索引,所以主键索引上不加任何锁,因此session B的update语句可以成功。
而session C的插入操作,会被session A的间隙锁(5,10)锁住。
在该案例中,lock in share mode
只锁覆盖索引,但如果是for update
就不同了,因为系统会认为接下来更新数据,会顺便给主键索引上满足条件的行加上行锁。
该案例说明,锁是加在索引上的,同时如果要用lock in share mode
来给行加读锁避免数据被更新,就必须绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段,比如将session A的查询语句改成select d from t where c=5 lock in share mode
。
案例三:主键索引范围锁
考虑下面这两条查询语句,加锁范围是否相同:
/* by yours.tools - online tools website : yours.tools/zh/xpath.html */
select * from t where id=10 for update;
select * from t where id>=10 and id<11 for update;
在逻辑上,这两条查询语句等价,但加锁规则不太一样。看看第二个语句的加锁效果:

分析session A的加锁情况:
-
先找到第一个
id=10
的行,本该加临键锁(5,10],根据优化1,主键是唯一索引,因此该临键锁退化成行锁,只加了id=10
这一行的行锁; -
范围查找会继续往后找,找到
id=15
这一行停下来,因此会加临键锁(10,15]。
这里需要注意的是,session A定位查找id=10
的行的时候,是当做等值查询来判断的,而向右扫描到id=15
的时候,用的是范围查询判断。
案例四:非唯一索引范围锁

由于索引c是非唯一索引,与案例三相比,没有优化规则,因此最终session A加的锁是:索引c上的(5,10]和(10,15]这两个临键锁。
案例五:唯一索引范围锁bug

session A是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个临键锁,且由于id唯一,所以循环判断到id=15
这一行就应该停止。
但是实现上,InnoDB会往前扫描到第一个不满足条件的行为止,即id=20
,由于这是范围扫描,因此索引id上的(15,20]这个临键锁也会被锁上。
所以session B和session C的操作都会被锁住。
案例六:非唯一索引上存在等值的例子
接下来的例子,是为了更好说明间隙的概念。这里插入一条新纪录:
insert into t values(30,10,30);
新插入一行后,表里有两个c=10
的行。由于非唯一索引上包含主键的值,所以不存在完全相同的两行,此时索引c:

索引c中两个c=10
的记录之间,也是有间隙的。
接下来看例子:

session A在遍历时,先访问第一个c=10
的记录,根据原则1,会加(c=5,id=5)到(c=10,id=10)的临键锁。之后继续向右查找,直到碰到(c=15,id=15)这一行,根据优化2,这是一个等值查询,向右查找到了不满足条件的行,会退化成(c=10,id=10)到(c=15,id=15)的间隙锁。
因此delete语句的加锁范围实际上如下:

虚线表示这是个开区间。
案例七:limit语句加锁
案例六的对照案例:

表t里c=10
的记录只有两条,因此limit 2不影响删除效果,但会影响加锁效果。可以看到session B的插入语句通过,跟案例六结果不同。
这是因为加了limit 2后,遍历到(c=10,id=30)这一行后,满足条件的语句已经有两条,循环结束。
因此在该案例中,加锁范围如下:

该案例的指导意义就是,在删除数据的时候尽量加上limit。
案例八:一个死锁的例子
该案例目的是说明:临键锁实际上是间隙锁和行锁加起来的结果。

按顺序分析:
-
session A启动事务后,在索引c上加了(5,10]和(10,15)的锁;
-
session B的update语句要在索引c上加(5,10],进入锁等待;
-
session A要插入时被session B的间隙锁锁住。由于出现死锁,InnoDB会让session B回滚。
可能会有疑惑,session B的临键锁还没申请成功,为什么也会死锁?
因为session B的临键锁实际分为两步,先加(5,10)的间隙锁,加锁成功,然后加c=10
的行锁才进入等待。