间隙锁规则:

两个“原则”、两个“优化”和一个“bug”:
1.加锁的基本单位是next-key lock,next-key lock是前开后闭区间。
2.查找过程中访问到的对象才会加锁。
3.索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
4.索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
5.唯一索引上的范围查询会访问到不满足条件的第一个值为止。

规则前提说明:
MySQL后面的版本可能会改变加锁策略,所以这个规则只限于截止到现在的最新版本,即5.x系列<=5.7.24,8.0系列 <=8.0.13。

表/数据初始化:

表t的建表语句和初始化语句
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);

案例一:等值查询间隙锁

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_mysql 间隙锁隔离级别


分析:

1.根据原则1,加锁单位是next-key lock,session A加锁范围就是(5,10];

2.同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁的范围是(5,10)。

session B要往这个间隙里面插入id=8的记录会被锁住,但是session C修改id=10这行是可以的。

案例二:非唯一索引等值锁(关于覆盖索引)

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_主键_02


分析:

1.session A要给索引c上c=5的这一行加上读锁。

2.根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock。

注意c是普通索引,因此仅访问c=5这一条记录是不能马上停下来的,需要向右遍历,查到c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock。

3.符合优化2:等值判断,向右遍历,最后一个值不满足c=5这个等值条件,因此退化成间隙锁(5,10)。

4.根据原则2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,

执行 for update时,系统会认为你接下来要更新数据,会给主键索引上满足条件的行加上行锁。

这个例子说明,锁是加在索引上的;同时,如果你要用lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。

案例三:主键索引范围锁

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_记录锁_03


分析:

1.要找到第一个id=10的行,因此本该是next-key lock(5,10]。

2. 根据优化1, 主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁。

范围查找就往后继续找,找到id=15这一行停下来,因此需要加next-key lock(10,15]。

3.session A这时候锁的范围就是主键索引上,行锁id=10和next-key lock(10,15]。

4.session A定位查找id=10的行的时候,是当做等值查询来判断的,而向右扫描到id=15的时候,用的是范围查询判断。

案例四:非唯一索引范围锁

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_加锁_04


分析:

加锁规则跟案例三唯一的不同是:在第一次用c=10定位记录的时候,索引c上加了(5,10]这个next-key lock后,由于索引c是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终sesion A加的锁是,索引c上的(5,10] 和(10,15] 这两个next-key lock。

案例五:唯一索引范围锁bug

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_加锁_05


分析:

session A是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个next-key lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了。

但是实现上,InnoDB会往前扫描到第一个不满足条件的行为止,也就是id=20。而且由于这是个范围扫描,因此索引id上的(15,20]这个next-key lock也会被锁上。

session B要更新id=20这一行,是会被锁住的。session C要插入id=16的一行,也会被锁住。

案例六:非唯一索引上存在"等值"的例子

mysql> insert into t values(30,10,30);

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_加锁_06


虽然有两个c=10,但是它们的主键值id是不同的(分别是10和30),因此这两个c=10的记录之间,也是有间隙的。

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_主键_07


分析:

1.session A在遍历的时候,先访问第一个c=10的记录。

2.根据原则1,这里加的是(c=5,id=5)到(c=10,id=10)这个next-key lock。

3.session A向右查找,直到碰到(c=15,id=15)这一行,循环才结束。

4.根据优化2,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成(c=10,id=10) 到 (c=15,id=15)的间隙锁。

5.这个delete语句在索引c上的加锁范围,就是下图中蓝色区域覆盖的部分。

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_记录锁_08


这个蓝色区域左右两边都是虚线,表示开区间,即(c=5,id=5)和(c=15,id=15)这两行上都没有锁。

案例七:limit 语句加锁

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_mysql 间隙锁隔离级别_09


分析:

delete语句明确加了limit 2的限制,因此在遍历到(c=10, id=30)这一行之后,满足条件的语句已经有两条,循环就结束了。

索引c上的加锁范围就变成了从(c=5,id=5)到(c=10,id=30)这个前开后闭区间:

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_加锁_10


在删除数据的时候尽量加limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。

案例八:一个死锁的例子

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_mysql 间隙锁隔离级别_11


分析:

1.session A 启动事务后执行查询语句加lock in share mode,在索引c上加了next-key lock(5,10] 和间隙锁(10,15);

2.session B 的update语句也要在索引c上加next-key lock(5,10] ,进入锁等待;

3.session A要再插入(8,8,8)这一行,被session B的间隙锁锁住。由于出现了死锁,InnoDB让session B回滚。

4.session B的“加next-key lock(5,10] ”操作,实际上分成了两步,先是加(5,10)的间隙锁,加锁成功;然后加c=10的行锁,这时候才被锁住的。

我们在分析加锁规则的时候可以用next-key lock来分析。具体执行的时候,是要分成间隙锁和行锁两段来执行的。

案例九:倒序加锁

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_加锁_12


分析:

1.由于是order by c desc,第一个要定位的是索引c上“最右边的”c=20的行,所以会加上间隙锁(20,25)和next-key lock (15,20]。

2.在索引c上向左遍历,要扫描到c=10才停下来,所以next-key lock会加到(5,10],这正是阻塞session B的insert语句的原因。

3.在扫描过程中,c=20、c=15、c=10这三行都存在值,由于是select *,所以会在主键id上加三个行锁。

因此,session A 的select语句锁的范围就是:

1.索引c上 (5, 25);

2.主键索引上id=15、20两个行锁。

案例十:不等号条件里的等值查询

select * from t where id>9 and id<12 order by id desc for update;

这个语句的加锁范围是主键索引上的 (0,5]、(5,10]和(10, 15)。也就是说,id=15这一行,并没有被加上行锁。

我们的查询语句中where条件是大于号和小于号,这里的“等值查询”又是从哪里来的呢?

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_记录锁_13


分析:

1.这个过程是通过索引树的搜索过程得到的,在引擎内部,其实是要找到id=12的这个值,只是最终没找到,但找到了(10,15)这个间隙。

2.然后向左遍历,在遍历过程中,就不是等值查询了,会扫描到id=5这一行,所以会加一个next-key lock (0,5]。

也就是说,在执行过程中,通过树搜索的方式定位记录的时候,用的是“等值查询”的方法。

案例十一:等值查询的过程

select id from t where c in(5,20,10) lock in share mode;

这条语句的explain结果:

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_主键_14


这条in语句使用了索引c并且rows=3,说明这三个值都是通过B+树搜索定位的。

分析:

1.在查找c=5的时候,先锁住了(0,5]。但是因为c不是唯一索引,为了确认还有没有别的记录c=5,就要向右遍历,找到c=10才确认没有了,这个过程满足优化2,所以加了间隙锁(5,10)。

2.执行c=10这个逻辑的时候,加锁的范围是(5,10] 和 (10,15);

4.执行c=20这个逻辑的时候,加锁的范围是(15,20] 和 (20,25)。

所以,这条语句在索引c上加的三个记录锁的顺序是:先加c=5的记录锁,再加c=10的记录锁,最后加c=20的记录锁。

这些锁是“在执行过程中一个一个加的”,而不是一次性加上去的。

案例十二:间隙锁范围扩大导致锁等待

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_加锁_15


分析:

由于delete操作把id=10这一行删掉了,原来的两个间隙(5,10)、(10,15)变成了一个(5,15)。

所谓“间隙”,其实就是由“这个间隙右边的那个记录”定义的。

案例十三:更新数据导致间隙锁范围变更

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_主键_16

分析:

session A的加锁范围是索引c上的 (5,10]、(10,15]、(15,20]、(20,25]和(25,supremum]。

根据c>5查到的第一个记录是c=10,因此不会加(0,5]这个next-key lock。

之后session B的第一个update语句,要把c=5改成c=1,你可以理解为两步:

1.插入(c=1, id=5)这个记录;

2.删除(c=5, id=5)这个记录。

索引c上(5,10)间隙是由这个间隙右边的记录,也就是c=10定义的。

session A的加锁范围:

mysql 间隙锁隔离级别 mysql 间隙锁 唯一索引_主键_17


session B要执行 update t set c = 5 where c = 1这个语句了,拆成两步:

插入(c=5, id=5)这个记录;

删除(c=1, id=5)这个记录。

第一步试图在已经加了间隙锁的(1,10)中插入数据,所以就被堵住了。