Scott's world.

MySQL-加锁规则相关

Word count: 1.8kReading time: 6 min
2020/02/16 Share

MySQL-加锁规则相关

今天我们会谈到一些加锁规则,因为间隙锁在RR级别下才有效,所以这篇文章下面的描述若没有特殊说明,默认是RR级别。

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

总结的加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”

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

本篇文章的示例表构建语句如下

1
2
3
4
5
6
7
8
9
10
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);

案例一:等值查询间隙锁

第一个例子是关于等值条件操作间隙:

由于表t中没有id=7的记录,根据上面提到的加锁规则判断一下的话:

  • 根据原则1,session A加锁范围为(5,10]
  • 根据优化2,这是一个等值查询而id=10不满足查询条件,next-key lock退化成间隙锁,即最终加锁范围为(5,10)

案例二:非唯一索引等值锁

第二个例子是关于覆盖索引上的锁:

我们可以看出session A要给索引c=5的这一行加上读锁。

  1. 根据原则1,加锁范围为(0,5]
  2. c为普通索引,访问c=5这一条记录需要向右遍历,直到c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock
  3. 同时符合优化2中的等值判断,向右遍历,最后一个值不满足c=5等值条件,因此退化成间隙锁(5,10)
  4. 根据原则2,只有访问到的对象才会加锁,这个查询为覆盖索引并不需要访问主键索引,所以主键索引上没有加锁。

需要注意,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update就不一样了。 执行 for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。

这个例子说明,锁是加在索引上的。同时若要用lock in share mode加行锁的话必须绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。

案例三:主键索引范围锁

1
2
mysql> select * from t where id=10 for update;
mysql> select * from t where id>=10 and id<11 for update;

上面的语句其实并不完全等价。虽然在逻辑上是一样的,但加锁规则不同。

根据前面提到的规则,分析如下:

  1. 开始执行的时候要找到第一个id=10的行,加next-key lock(5,10]。但根据优化1,主键id上的等值条件即退化成行锁即id=10这一行。
  2. 范围查找往后继续找,找到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。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。

案例八:一个死锁的例子

我们先来看下面这个例子:

现在,我们按时间顺序来分析一下上面的结果:

  1. session A启动事务后在索引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回滚。

其实是这样的,session B的“加next-key lock(5,10] ”操作,实际上分成了两步:

  • 加(5,10)的间隙锁,加锁成功
  • 加c=10的行锁,这时候才被锁住的

即具体执行的时候,分析next-key lock是要分成间隙锁和行锁两段来执行的

CATALOG
  1. 1. MySQL-加锁规则相关
    1. 1.1. 案例一:等值查询间隙锁
    2. 1.2. 案例二:非唯一索引等值锁
    3. 1.3. 案例三:主键索引范围锁
    4. 1.4. 案例四:非唯一索引范围锁
    5. 1.5. 案例五:唯一索引范围锁bug
    6. 1.6. 案例六:非唯一索引上存在“等值”的例子
    7. 1.7. 案例七:limit语句加锁
    8. 1.8. 案例八:一个死锁的例子