MySQL-加锁规则相关
今天我们会谈到一些加锁规则,因为间隙锁在RR级别下才有效,所以这篇文章下面的描述若没有特殊说明,默认是RR级别。
这个规则是自己总结所以有前提说明:MySQL后面的版本可能会改变加锁策略,所以这个规则只限于截止到现在的最新版本,即5.x系列<=5.7.24,8.0系列 <=8.0.13。
总结的加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”:
- 原则1:加锁的基本单位是next-key lock,next-key lock是前开后闭区间。
- 原则2:查找过程中访问到的对象才会加锁。
- 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
- 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
- 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
本篇文章的示例表构建语句如下
1 | CREATE TABLE `t` ( |
案例一:等值查询间隙锁
第一个例子是关于等值条件操作间隙:
由于表t中没有id=7的记录,根据上面提到的加锁规则判断一下的话:
- 根据原则1,session A加锁范围为(5,10]
- 根据优化2,这是一个等值查询而id=10不满足查询条件,next-key lock退化成间隙锁,即最终加锁范围为(5,10)
案例二:非唯一索引等值锁
第二个例子是关于覆盖索引上的锁:
我们可以看出session A要给索引c=5的这一行加上读锁。
- 根据原则1,加锁范围为(0,5]
- c为普通索引,访问c=5这一条记录需要向右遍历,直到c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock
- 同时符合优化2中的等值判断,向右遍历,最后一个值不满足c=5等值条件,因此退化成间隙锁(5,10)
- 根据原则2,只有访问到的对象才会加锁,这个查询为覆盖索引并不需要访问主键索引,所以主键索引上没有加锁。
需要注意,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update就不一样了。 执行 for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。
这个例子说明,锁是加在索引上的。同时若要用lock in share mode
加行锁的话必须绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。
案例三:主键索引范围锁
1 | mysql> select * from t where id=10 for update; |
上面的语句其实并不完全等价。虽然在逻辑上是一样的,但加锁规则不同。
根据前面提到的规则,分析如下:
- 开始执行的时候要找到第一个id=10的行,加next-key lock(5,10]。但根据优化1,主键id上的等值条件即退化成行锁即id=10这一行。
- 范围查找往后继续找,找到id=15这一行停下来,因此需要加(10,15]。
故可以看出session A锁的范围就是主键索引上即行锁id=10和next-key lock(10,15]。
这里你需要注意一点,首次session A定位查找id=10的行的时候,是当做等值查询来判断的,而向右扫描到id=15的时候,用的是范围查询判断。
案例四:非唯一索引范围锁
与案例三不同的是,案例四中查询语句的where部分用的是字段c。
与案例三不同的当第一次c=10定位记录时索引c上加了(5,10]锁后并不会退化为行锁,因为c是非唯一索引。所以最后索引c上加的锁是(5,10]和(10,15]这两个next-key lock。
案例五:唯一索引范围锁bug
前面的四个案例,我们已经用到了加锁规则中的两个原则和两个优化,接下来再看一个关于加锁规则中bug的案例。
session A是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个next-key lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了。但是实现上,InnoDB会往前扫描到第一个不满足条件的行为止,也就是id=20。而且由于这是个范围扫描,因此索引id上的(15,20]这个next-key lock也会被锁上。
照理说,这里锁住id=20这一行的行为,其实是没有必要的。因为扫描到id=15,就可以确定不用往后再找了。但实现上还是这么做了,因此我认为这是个bug。
案例六:非唯一索引上存在“等值”的例子
接下来的例子,是为了更好地说明“间隙”这个概念。这里,我给表t插入一条新记录。
1 | mysql> insert into t values(30,10,30); |
新插入的这一行c=10,也就是说现在表里有两个c=10的行。
这次我们用delete语句来验证。
delete语句加锁的逻辑,其实跟select … for update 是类似的
根据上面的规则很容易得到其加锁范围
案例七:limit语句加锁
例子6也有一个对照案例,场景如下所示:
delete语句明确加了limit 2的限制,因此在遍历到(c=10, id=30)这一行之后,满足条件的语句已经有两条,循环就结束了。
这个例子对我们实践的指导意义就是,在删除数据的时候尽量加limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。
案例八:一个死锁的例子
我们先来看下面这个例子:
现在,我们按时间顺序来分析一下上面的结果:
- session A启动事务后在索引c加了next-key lock(5,10]和间隙锁(10,15)
- session B 的update语句也要在索引c上加next-key lock(5,10] ,进入锁等待;
- 然后session A要再插入(8,8,8)这一行,被session B的间隙锁锁住。由于出现了死锁,InnoDB让session B回滚。
其实是这样的,session B的“加next-key lock(5,10] ”操作,实际上分成了两步:
- 加(5,10)的间隙锁,加锁成功
- 加c=10的行锁,这时候才被锁住的
即具体执行的时候,分析next-key lock是要分成间隙锁和行锁两段来执行的。