MySQL实战45讲——MySQL加锁的规则

文章摘抄自林晓斌老师《MySQL实战45讲》。首先说明一下, 这些加锁规则我没在别的地方看到过有类似的总结, 以前我自己判断的时候都是想着代码里面的实现来脑补的。 这次为了总结成不看代码的同学也能理解的规则, 是我又重新刷了代码临时总结出来的。 所以, 这个规则有以下两条前提说明

  1. MySQL后面的版本可能会改变加锁策略, 所以这个规则只限于截止到现在的最新版本, 即5.x系列<=5.7.24, 8.0系列 <=8.0.13。
  2. 如果大家在验证中有发现bad case的话, 请提出来, 我会再补充进这篇文章, 使得一起学习本专栏的所有同学都能受益。

因为间隙锁在可重复读隔离级别下才有效, 所以本篇文章接下来的描述, 若没有特殊说明, 默认是可重复读隔离级别。我总结的加锁规则里面, 包含了两个“原则”、 两个“优化”和一个“bug”

  1. 原则1: 加锁的基本单位是next-key lock。 希望你还记得, next-key lock是前开后闭区间。
  2. 原则2: 查找过程中访问到的对象才会加锁。
  3. 优化1: 索引上的等值查询, 给唯一索引加锁的时候, next-key lock退化为行锁。
  4. 优化2: 索引上的等值查询, 向右遍历时且最后一个值不满足等值条件的时候, next-key lock退化为间隙锁(“最后一个值”看半天也没明白,字面理解就是正无穷了,还是评论里面的解释比较明确:从第一个满足等值条件的索引记录开始向右遍历到第一个不满足等值条件记录, 并将第一个不满足等值条件记录上的next-key lock 退化为间隙锁)。
  5. 一个bug: 唯一索引上的范围查询会访问到不满足条件的第一个值为止。

我还是以上篇文章的表t为例, 和你解释一下这些规则。 表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实战45讲——MySQL加锁的规则_第1张图片

  • 由于等值查询id=7的记录不存在, 此时会把包含id=7的这个间隙加上Next-key Lock(范围锁) , 就是(5,10]。
  • 由于id=10这个记录明显不符合id=7的条件, 所以范围锁退化成间隙锁, 只锁住(5,10)

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

MySQL实战45讲——MySQL加锁的规则_第2张图片这里session A要给索引c上c=5的这一行加上读锁。

  1.  根据原则1, 加锁单位是next-key lock, 因此会给(0,5]加上next-key lock。
  2. 要注意c是普通索引, 因此仅访问c=5这一条记录是不能马上停下来的, 需要向右遍历, 查到c=10才放弃。 根据原则2, 访问到的都要加锁, 因此要给(5,10]加next-keylock。
  3. 但是同时这个符合优化2: 等值判断, 向右遍历, 最后一个值不满足c=5这个等值条件, 因此退化成间隙锁(5,10)。
  4.  根据原则2 , 只有访问到的对象才会加锁, 这个查询使用覆盖索引, 并不需要访问主键索引, 所以主键索引上没有加任何锁, 这就是为什么session B的update语句可以执行完成。(如果session A改成 begin;select d from t where c=5 lock in share mode 或者 begin;select * from t where c=5 lock in share mode;此时session B就会阻塞,因为这里回表使用到主键索引,主键索引会被锁

      但session C要插入一个(7,7,7)的记录, 就会被session A的间隙锁(5,10)锁住。
     需要注意, 在这个例子中, lock in share mode只锁覆盖索引, 但是如果是for update就不一样了。 执行 for update时, 系统会认为你接下来要更新数据, 因此会顺便给主键索引上满足条件的行加上行锁。
     这个例子说明, 锁是加在索引上的; 同时, 它给我们的指导是, 如果你要用lock in share mode来给行加读锁避免数据被更新的话, 就必须得绕过覆盖索引的优化, 在查询字段中加入索引中不存在的字段。 比如, 将session A的查询语句改成select d from t where c=5 lock in share mode。你可以自己验证一下效果。
 

案例三:主键索引范围锁

举例之前, 你可以先思考一下这个问题: 对于我们这个表t, 下面这两条查询语句, 加锁范围相同吗?

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

 你可能会想, id定义为int类型, 这两个语句就是等价的吧? 其实, 它们并不完全等价。在逻辑上, 这两条查语句肯定是等价的, 但是它们的加锁规则不太一样。 现在, 我们就让session A执行第二个查询语句, 来看看加锁效果。


MySQL实战45讲——MySQL加锁的规则_第3张图片

现在我们就用前面提到的加锁规则,来分析一下 session A 会加什么锁呢?

  1. 开始执行的时候,要找到第一个 id=10 的行,因此本该是 next-key lock(5,10]。 根据优化 >1, 主键 id 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁。
  2. 范围查找就往后继续找,找到 id=15 这一行停下来,因此需要加 next-key lock(10,15]。所以,session A 这时候锁的范围就是主键索引上,行锁 id=10 和 next-key lock(10,15]。这样,session B 和 session C 的结果你就能理解了。这里你需要注意一点,首次 session A 定位查找 id=10 的行的时候,是当做等值查询来判断的,而向右扫描到 id=15 的时候,用的是范围查询判断。

案例四:非唯一索引范围锁MySQL实战45讲——MySQL加锁的规则_第4张图片

这次session A用字段c来判断, 加锁规则跟案例三唯一的不同是: 在第一次用c=10定位记录的时候, 索引c上加了(5,10]这个next-key lock后, 由于索引c是非唯一索引, 没有优化规则, 也就是说不会蜕变为行锁, 因此最终sesion A加的锁是, 索引c上的(5,10] 和(10,15] 这两个next-key lock。
      所以从结果上来看, sesson B要插入(8,8,8)的这个insert语句时就被堵住了。这里需要扫描到c=15才停止扫描, 是合理的, 因为InnoDB要扫到c=15, 才知道不需要继续往后找了。

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

MySQL实战45讲——MySQL加锁的规则_第5张图片

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

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

所以你看到了, session B要更新id=20这一行, 是会被锁住的。 同样地, session C要插入id=16
的一行, 也会被锁住。

照理说, 这里锁住id=20这一行的行为, 其实是没有必要的。 因为扫描到id=15, 就可以确定不用
往后再找了。 但实现上还是这么做了, 因此我认为这是个bug。

我也曾找社区的专家讨论过, 官方bug系统上也有提到, 但是并未被verified。 所以, 认为这是
bug这个事儿, 也只能算我的一家之言, 如果你有其他见解的话, 也欢迎你提出来。
 

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

接下来的例子, 是为了更好地说明“间隙”这个概念。 这里, 我给表t插入一条新记录:

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

新插入的这一行c=10, 也就是说现在表里有两个c=10的行。 那么, 这时候索引c上的间隙是什么状态了呢? 你要知道, 由于非唯一索引上包含主键的值, 所以是不可能存在“相同”的两行的。

MySQL实战45讲——MySQL加锁的规则_第6张图片

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

图中我画出了索引c上的主键id。 为了跟间隙锁的开区间形式进行区别, 我用(c=10,id=30)这样的形式, 来表示索引上的一行。

现在, 我们来看一下案例六。这次我们用delete语句来验证。 注意, delete语句加锁的逻辑, 其实跟select ... for update 是类似的, 也就是我在文章开始总结的两个“原则”、 两个“优化”和一个“bug”。

MySQL实战45讲——MySQL加锁的规则_第7张图片

 这时, session A在遍历的时候, 先访问第一个c=10的记录。 同样地, 根据原则1, 这里加的是(c=5,id=5)到(c=10,id=10)这个next-keylock。

然后, session A向右查找, 直到碰到(c=15,id=15)这一行, 循环才结束。 根据优化2, 这是一个等值查询, 向右查找到了不满足条件的行, 所以会退化成(c=10,id=10) 到 (c=15,id=15)的间隙锁。也就是说, 这个delete语句在索引c上的加锁范围, 就是下图中蓝色区域覆盖的部分。

MySQL实战45讲——MySQL加锁的规则_第8张图片

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

案例七:limit 语句加锁

例子6也有一个对照案例, 场景如下所示:

MySQL实战45讲——MySQL加锁的规则_第9张图片
这个例子里, session A的delete语句加了 limit 2。 你知道表t里c=10的记录其实只有两条, 因此加不加limit 2, 删除的效果都是一样的, 但是加锁的效果却不同。 可以看到, session B的insert语句执行通过了, 跟案例六的结果不同。这是因为, 案例七里的delete语句明确加了limit 2的限制, 因此在遍历到(c=10, id=30)这一行之后, 满足条件的语句已经有两条, 循环就结束了。
因此, 索引c上的加锁范围就变成了从(c=5,id=5)到(c=10,id=30)这个前开后闭区间, 如下图所示:
 

MySQL实战45讲——MySQL加锁的规则_第10张图片

可以看到, (c=10,id=30) 之后的这个间隙并没有在加锁范围里, 因此insert语句插入c=12是可以执行成功的。

这个例子对我们实践的指导意义就是, 在删除数据的时候尽量加limit。 这样不仅可以控制删除数据的条数, 让操作更安全, 还可以减小加锁的范围。
 

案例八:一个死锁的例子

前面的例子中, 我们在分析的时候, 是按照next-keylock的逻辑来分析的, 因为这样分析比较方便。 最后我们再看一个案例, 目的是说明: next-keylock实际上是间隙锁和行锁加起来的结果。

你一定会疑惑, 这个概念不是一开始就说了吗? 不要着急, 我们先来看下面这个例子:
MySQL实战45讲——MySQL加锁的规则_第11张图片

现在, 我们按时间顺序来分析一下为什么是这样的结果。

  1.  session A 启动事务后执行查询语句加lock in share mode, 在索引c上加了next-key-lock(5,10] 和间隙锁(10,15);
  2. session B 的update语句也要在索引c上加next-keylock(5,10] , 进入锁等待;
  3.  然后session A要再插入(8,8,8)这一行, 被session B的间隙锁锁住。 由于出现了死锁, InnoDB让session B回滚。

你可能会问, session B的next-keylock不是还没申请成功吗?

其实是这样的, session B的“加next-keylock(5,10] ”操作, 实际上分成了两步, 先是加(5,10)的间隙锁, 加锁成功; 然后加c=10的行锁, 这时候才被锁住的。也就是说, 我们在分析加锁规则的时候可以用next-keylock来分析。 但是要知道, 具体执行的时候, 是要分成间隙锁和行锁两段来执行的。

小结
这里我再次说明一下, 我们上面的所有案例都是在可重复读隔离级别(repeatable-read)下验证的。 同时, 可重复读隔离级别遵守两阶段锁协议, 所有加锁的资源, 都是在事务提交或者回滚的时候才释放的。

在最后的案例中, 你可以清楚地知道next-keylock实际上是由间隙锁加行锁实现的。 如果切换到读提交隔离级别(read-committed)的话, 就好理解了, 过程中去掉间隙锁的部分, 也就是只剩下行锁的部分。

其实读提交隔离级别在外键场景下还是有间隙锁, 相对比较复杂, 我们今天先不展开。另外, 在读提交隔离级别下还有一个优化, 即: 语句执行过程中加上的行锁, 在语句执行完成后, 就要把“不满足条件的行”上的行锁直接释放了, 不需要等到事务提交。也就是说, 读提交隔离级别下, 锁的范围更小, 锁的时间更短, 这也是不少业务都默认使用读提交隔离级别的原因。

不过, 我希望你学过今天的课程以后, 可以对next-keylock的概念有更清晰的认识, 并且会用加锁规则去判断语句的加锁范围。
在业务需要使用可重复读隔离级别的时候, 能够更细致地设计操作数据库的语句, 解决幻读问题的同时, 最大限度地提升系统并行处理事务的能力。
 

你可能感兴趣的:(Mysql,mysql,数据库,database)