MySQL 加锁实战分析

前言

在文章 中介绍了三大类锁:全局锁、表锁、行锁

InnoDB 中支持行级锁,而 MyISAM 中只支持表级锁,锁粒度更细会提高系统整体的并发量,本篇文章主要分析在 InnoDB 中执行语句时是如何加行锁滴!!

在执行下面四类语句时都会加行级锁:

注意:上面四种类型语句的加锁只有在事务提交后才会被释放

下面先来分析一下「S 锁和 X 锁」与「Record Lock、Gap Lock、Next-Key Lock」的关系!为什么说行锁有 Record Lock、Gap Lock、Next-Key Lock,又说行锁分为 S 锁和 X 锁??

举个很简单的例子,世界上的人分为两类:男人和女人,而男人和女人中又可以细分为:老人、小孩、年轻人等。行锁的分类也类似:

3

注意:S 型 Gap Lock 和 X 型 Gap Lock 其实没有区别,都是不允许在指定间隙插入新记录,所以 Gap Lock 并没有刻意区分 S 锁和 X 锁

S 锁和 X 锁的兼容性规则如下图所示:(注意:无论是 Gap Lock 还是 Next-Key Lock 的 S 锁和 X 锁都遵循该兼容规则)

13

重点:一般情况下,对于锁定读的语句,在隔离级别为 READ UNCOMMITTED 和 READ COMMITTED 时,会为当前记录加 Record Lock;在隔离级别为 REPEATABLE READ 和 SERIALIZABLE 时,会为当前记录加 Next-Key Lock。但存在锁退化的特殊情况,后文出现时会分析原因!!

注意:虽然 REPEATABLE READ 隔离级别中并不能彻底解决幻读,但在实现时尽最大能力避免幻读的出现,所以加的是 Next-Key Lock,其中包含的间隙锁含义就是专门用于解决幻读

几个概念

在正式介绍之前再介绍几个小小的概念 (不要急~)

唯一索引:主键索引、声明为UNIQUE KEY的索引都是唯一索引,值不允许重复。需要注意UNIQUE索引中可以包含多个 NULL 值,而主键索引规定不允许为 NULL

非唯一索引:除了上面提到的两种索引外都是非唯一索引,值允许重复

等值查询:查询语句中判断条件是一个等值判断,如:where id = 1。如果是唯一索引中的等值查询,那么最多查询出一条记录;如果是非唯一索引中的等值查询,那么可能会查询出多条记录

范围查询:查询语句中判断条件是一个范围判断,如:where id > 1。无论是否为唯一索引,都可能会查询出多条记录

最后顺便在这一部分给出后文都会使用到的表结构:

表中记录如下图所示:

4

注意:后文所有实验内容都基于 MySQL 8.0.33,且在 Repeatable Read 隔离级别下!!

唯一索引等值查询

记录存在

假设事务 A 执行的语句如下:

那么事务 A 会为id = 15的记录加 X 型记录锁,如下图所示:

5

由于 X 型记录锁和其它任何锁都不兼容,如果在事务 A 没有提交期间其它事务执行需要获取id = 15记录锁的语句,都将会被阻塞,下面列出三种情况:

6

上面的分析都是口说无凭,下面直接来点证据!!在 MySQL 8 中新增表performance_schema.data_locks,它记录了正在执行事务中的锁信息,从该表中可以得知运行事务的加锁情况

从输出可以看出:

通过 LOCK_MODE 可以判断表级加锁的类型:(这是第一次出现就详细介绍了一下,下同!!)

问题:前言 部分的最后还特意强调 RR 隔离级别下会为记录加 Next-Key Lock,但在唯一索引等值查询且记录存在的情况下,给记录加的锁退化成 Record Lock,为什么??

Next-Key Lock 和 Record Lock 唯一的区别就是多了间隙锁,而间隙锁是专门为了避免出现幻读现象。在唯一索引等值查询且记录存在的条件下,那么查询结果集数量为 1

不允许其它事务对查询出来的记录删除,也就避免了再次查询后结果集为 0 的情况。因为唯一索引的约束使得不能插入 id 相同的记录,那么就算在查询记录前后插入新记录也不会使结果集数量增加

综上所述:在一个事务中前后查询的结果集数量即不会增加也不会减少,避免了幻读的出现,所以只加记录锁就够了,不需要加间隙锁!!

记录不存在

假设事务 A 执行的语句如下:

那么事务 A 会在id > 1且差值最小的记录出加间隙锁,如下图所示:

7

由于间隙锁不允许插入新记录,如果在事务 A 没有提交期间其它事务执行插入id < 5记录的操作,都将会被阻塞,具体见下图:

8

上面的分析都是口说无凭,下面直接来点证据!!为了节约篇幅,只摘出了关键信息

问题:在唯一索引等值查询且记录不存在的情况下,给记录加的锁从 Next-Key Lock 退化成 Gap Lock,为什么??

在唯一索引等值查询且记录不存在的条件下,那么查询结果集数量为 0,结果集不可能变为负数,所以完全不用担心其它事务删除操作。加间隙锁可以避免插入id < 5的记录,那么查询结果集不可能增加

综上所述:在一个事务中前后查询的结果集数量即不会增加也不会减少,避免了幻读的出现,所以只加间隙锁就够了,不需要加记录锁!!

总结

对于记录存在的情况,可以找到符合条件的记录,按理来说应该为符合条件的记录加 Next-Key Lock,但由于唯一索引等值查询的约束,直接可以退化成记录锁

对于记录不存在的情况,可以找到大于要求条件的第一条记录,按理来说应该为这条记录加 Next-Key Lock,但由于唯一索引等值查询的约束,直接可以退化成间隙锁

唯一索引范围查询

对于 > 的范围查询

假设事务 A 执行的语句如下:

那么事务 A 会加三个 X 型 Next-Key Lock,如下图所示:

9

在事务 A 没有提交期间,其它事务不允许对id = 20, 25的记录更新、删除、锁定读,也不允许插入15 < id < 20 || 20 < id < 25 || 25 < id的记录

🤔️ 疑惑:给 Supremum 记录加 Next-Key Lock,相当于加了记录锁和间隙锁,可是 Supremum 记录本身就无法被访问,那为什么不直接退化成间隙锁呢???!!!

锁退化的前提是:退化前的锁和退化后的锁可以实现同样的效果。对于普通记录而言,锁退化可以使锁住的范围更小,从而提高系统整体的并发度;对于 Supremum 记录而言,Next-Key Lock 只会锁间隙,因为 Supremum 记录也无法访问,所以加 Next-Key Lock 或者只加间隙锁效果都一样,锁住的范围也一样,但为了追求简单,加锁时不用判断,所以并没有锁退化

上面的分析都是口说无凭,下面直接来点证据!!为了节约篇幅,只摘出了关键信息

对于 >= 的范围查询 (临界值记录存在)

假设事务 A 执行的语句如下:

那么事务 A 会加一个 X 型记录锁 和三个 X 型 Next-Key Lock,如下图所示:

10

在事务 A 没有提交期间,其它事务不允许对id = 15, 20, 25的记录更新、删除、锁定读,也不允许插入15 < id < 20 || 20 < id < 25 || 25 < id的记录

上面的分析都是口说无凭,下面直接来点证据!!为了节约篇幅,只摘出了关键信息

问题:按理来说应该会加四个 X 型 Next-Key Lock,但其中有一个锁退化成 X 型记录锁,为什么??

只要将事务 A 执行的查询语句拆分成下面两部分就可以一眼看出原因:

由于id = 15的记录存在,那么第一条语句就和 唯一索引等值查询 - 记录存在 的情况一样了,锁退化原因也一样!!

对于 >= 的范围查询 (临界值记录不存在)

假设事务 A 执行的语句如下:

该情况和 对于 > 的范围查询 相同,加锁情况也相同,这里就不展开介绍

对于 < 的范围查询

假设事务 A 执行的语句如下:

那么事务 A 会加一个 X 型间隙锁 和两个 X 型 Next-Key Lock,如下图所示:

11

在事务 A 没有提交期间,其它事务不允许对id = 5, 10的记录更新、删除、锁定读,也不允许插入id < 5 || 5 < id < 10 || 10 < id < 15的记录

上面的分析都是口说无凭,下面直接来点证据!!为了节约篇幅,只摘出了关键信息

对于 <= 的范围查询 (临界值记录存在)

假设事务 A 执行的语句如下:

那么事务 A 会加两个 X 型 Next-Key Lock,如下图所示:

12

在事务 A 没有提交期间,其它事务不允许对id = 5, 10的记录更新、删除、锁定读,也不允许插入id < 5 || 5 < id < 10的记录

上面的分析都是口说无凭,下面直接来点证据!!为了节约篇幅,只摘出了关键信息

对于 <= 的范围查询 (临界值记录不存在)

假设事务 A 执行的语句如下:

该情况和 对于 < 的范围查询 相同,加锁情况也相同,这里就不展开介绍

总结

对于 < 的范围查询,必须遍历到第一条不符合条件的记录才停止,按理来说应该为这条记录加 Next-Key Lock,但由于唯一索引的约束,直接可以退化成间隙锁,为符合条件的记录加 Next-key Lock 即可

对于 <= 的范围查询 (临界值记录存在),由于唯一索引的约束,遍历到临界值记录就可以停止,此时遍历的记录均为符合要求的记录,为这些记录加 Next-key Lock 即可

对于 <= 的范围查询 (临界值记录不存在),直接等价于对于 < 的范围查询

非唯一索引等值查询

非唯一索引属于二级索引,如果通过执行计划得知语句使用非唯一索引查询,那么就是在对应的二级索引树中,二级索引树中的记录是按照二级索引大小排序,如下图所示:

13

记录存在

假设事务 A 执行的语句如下:

那么事务 A 会为age = 18的记录对应的主键索引加一个 X 型记录锁,以及对应的二级索引加一个 X 型 Next-Key Lock 和一个 X 型间隙锁,如下图所示:

14

在事务 A 没有提交期间,其它事务不允许对age = 18的记录对应的二级索引更新、删除、锁定读,不允许对id = 20的记录对应的主键索引更新、删除、锁定读

关于其它事务是否可以插入新记录的情况,略微的麻烦,慢慢分析~。插入一条记录会在所有的索引树中也添加该记录,目前只有二级索引中有两个间隙锁,会影响记录的插入:

重点:判断能否插入的标准就是新记录是否会插入到间隙锁定的区间内,如果是,就不能插入;否则就可以插入

15

上面的分析都是口说无凭,下面直接来点证据!!为了节约篇幅,只摘出了关键信息

记录不存在

假设事务 A 执行的语句如下:

那么事务 A 为二级索引加一个 X 型间隙锁,如下图所示:

16

在事务 A 没有提交期间,其它事务不允许插入18 < age < 20的记录,对于插入age = 18, 20的记录还需要根据id的值来判断,判断标准同 非唯一索引等值查询 - 记录存在

上面的分析都是口说无凭,下面直接来点证据!!为了节约篇幅,只摘出了关键信息

非唯一索引范围查询

注意:在非唯一索引范围查询时,二级索引上查询的记录全都加 Next-key Lock,不会出现锁退化的情况

假设事务 A 执行的语句如下:

那么事务 A 会为记录对应的主键索引加两个 X 型记录锁,以及对应的二级索引加三个 X 型 Next-Key Lock,如下图所示:

17

在事务 A 没有提交期间,其它事务不允许对age = 20 30的记录对应的二级索引更新、删除、锁定读,不允许对id = 15 25的记录对应的主键索引更新、删除、锁定读

其它事务不允许插入18 < age的记录,对于插入age = 18的记录,也必须满足id < 20,否则会插入到第一个间隙中,导致插入失败

上面的分析都是口说无凭,下面直接来点证据!!为了节约篇幅,只摘出了关键信息

查询不使用索引

假设事务 A 执行的语句如下:

通过执行计划可知该语句没有使用任何索引:

那么事务 A 会加六个 X 型记录锁,相当于为每个记录都加了一个 X 型记录锁,即:锁全表

上面的分析都是口说无凭,下面直接来点证据!!为了节约篇幅,只摘出了关键信息

总结

唯一索引加锁流程:

18

非唯一索引加锁流程:

19