写点什么

MySQL 探秘 (七):InnoDB 行锁算法

  • 2021 年 12 月 07 日
  • 本文字数:2683 字

    阅读完需:约 9 分钟

MySQL探秘(七):InnoDB行锁算法

备注:公众号原名张狗蛋的技术之路,现已改名为程序员历小冰

在上一篇《InnoDB 一致性非锁定读》中,我们了解到 InnoDB 使用一致性非锁定读来避免在一般的查询操作(SELECT FOR UPDATE 等除外)时使用锁。然而锁这个事情是无法避免的,数据的写入,修改和删除都需要加锁。今天我们就继续学习 InnoDB 锁相关的知识。

 由于文章涉及的概念比较多,害怕大家看完后会骂人,有一种字我都认识,就不太懂的感觉,文章会给出一些实例和试验,依据具体案例来讲解这些概念。毕竟,实践才能出真知。

 InnoDB 存储引擎支持表锁和行锁。顾名思义,表锁是锁住整张表,行锁只是锁住某些行。InnoDB 通过给索引项加锁来实现行锁,如果没有索引,则通过隐藏的聚簇索引来对记录加锁。如果操作不通过索引条件检索数据,InnoDB 则对表中的所有记录加锁,实际效果就和表锁一样。InnoDB 存储引擎有 3 种行锁的算法,分别是:

  • Record Lock: 单个记录上的锁

  • Gap Lock: 间隙锁,锁定一个范围,但不包括记录本上

  • Next-Key Lock: Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身

 如下图所示,


 例如一个索引有 10,11,13,20 这四个值。InnoDB 可以根据需要使用 Record Lock 将 10,11,13,20 四个索引锁住,也可以使用 Gap Lock 将(-∞,10),(10,11),(11,13),(13,20),(20, +∞)五个范围区间锁住。Next-Key Locking 类似于上述两种锁的结合,它可以锁住的区间有为(-∞,10],(10,11],(11,13],(13,20],(20, +∞),可以看出它即锁定了一个范围,也会锁定记录本身。

 InnoDB 存储引擎的锁算法的一些规则如下所示,后续章节会给出对应的实验案例和详细讲解。

  • 在不通过索引条件查询时,InnoDB 会锁定表中的所有记录。所以,如果考虑性能,WHERE 语句中的条件查询的字段都应该加上索引。

  • InnoDB 通过索引来实现行锁,而不是通过锁住记录。因此,当操作的两条不同记录拥有相同的索引时,也会因为行锁被锁而发生等待。

  • 由于 InnoDB 的索引机制,数据库操作使用了主键索引,InnoDB 会锁住主键索引;使用非主键索引时,InnoDB 会先锁住非主键索引,再锁定主键索引。

  • 当查询的索引是唯一索引(不存在两个数据行具有完全相同的键值)时,InnoDB 存储引擎会将 Next-Key Lock 降级为 Record Lock,即只锁住索引本身,而不是范围。

  • InnoDB 对于辅助索引有特殊的处理,不仅会锁住辅助索引值所在的范围,还会将其下一键值加上 Gap LOCK。

  • InnoDB 使用 Next-Key Lock 机制来避免 Phantom Problem(幻读问题)。

真的了解本质吗?

 在不通过索引条件查询时,InnoDB 会锁定表中的所有记录。大家可以登录上自己的 MySQL 服务器,亲自试验一下。


 试验发现,会话二的查询操作真的是会发生等待。那么,这句话真的是对的吗?我们可以使用《InnoDB 锁的类型和状态查询》中查询数据锁的方法查询一下,注意必须在会话二操作还在等待时进行查询,否则查询不到


 其中 lock_trx_id 为 1851 的事务是会话二的事务,另一个是会话一的事务。我们可以看到两个锁都要对值为 1 的主键索引加锁。需要注意的是,这里是对主键进行加锁。二者之间的关系是怎么确定的呢?我们可以通过information_schema.INNODB_LOCK_WAITS中的数据确定。

 奇怪,不是说好的锁定表中的所有记录嘛?查找了很多资料,发现INNODB_LOCKS的定义如下:

The INNODB_LOCKS table contains information about each lock that an InnoDB transaction has requested but not yet acquired, and each lock that a transaction holds that is blocking another transaction.

 也就是说,这张表并不会显示所有锁的信息,而是只显示要申请却没有申请到,和已经持有锁并且阻塞其他线程的锁信息。怪不得必须在会话二进行等待时进行查询才能查得到数据。

 因为两个会话的操作都要锁住所有的行,所以发现每次在第一行记录上就发生了锁等待。那我们使用插入语句试试。表 e1 的主键 a 的值为 1-4,我们分别插入主键为 1-4(当然会有主键重复问题,但是由于有锁,一直等待)的新记录,分别查询锁信息,就能看到会话一的事务对所有的主键都加了锁,也就是对所有的记录都加了锁。

是索引,而不是记录

 InnoDB 存储引擎的行锁是通过锁住索引实现的,而不是记录。这是理解很多数据库锁问题的关键。

 由于 InnoDB 特殊的索引机制,数据库操作使用主键索引时,InnoDB 会锁住主键索引;使用非主键索引时,InnoDB 会先锁住非主键索引,再锁定主键索引。

 如下图所示,当 InnoDB 锁定非主键索引 b 时,它也会锁住其对应的主键索引,所以锁住 b 值为 2 和 3 的非主键索引,那么与其相关的 a 值为 6,5 的主键索引也需要被锁住。


 比如说,一种常见的死锁情况一般出现在如下图所示的操作场景中。


 会话一的语句使用了 b 上的索引,因为它是非主键索引,所以会先在 b 索引上添加锁,再去 a 索引上加锁。而会话二的语句恰恰相反,会先在索引 a 上加锁,再去索引 b 加锁。这种情况下,就可能出现死锁。

Next-Key Lock 锁到底有什么用?

 默认隔离级别 REPEATABLE-READ 下,InnoDB 中行锁默认使用算法 Next-Key Lock,只有当查询的索引是唯一索引或主键时,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。当查询的索引为辅助索引时,InnoDB 则会使用 Next-Key Lock 进行加锁。InnoDB 对于辅助索引有特殊的处理,不仅会锁住辅助索引值所在的范围,还会将其下一键值加上 Gap LOCK。

 废话不多说,我们来看一下相关的实验,先做一下准备。

CREATE TABLE e4 (a INT, b INT, PRIMARY KEY(a), KEY(b));INSERT INTO e4 SELECT 1,1;INSERT INTO e4 SELECT 3,1;INSERT INTO e4 SELECT 5,3;INSERT INTO e4 SELECT 7,6;INSERT INTO e4 SELECT 10,8;
复制代码

 然后开启一个会话执行下面的语句。

SELECT * FROM e4 WHERE b=3 FOR UPDATE; 
复制代码

 因为通过索引 b 来进行查询,所以 InnoDB 会使用 Next-Key Lock 进行加锁,并且索引 b 是非主键索引,所以还会对主键索引 a 进行加锁。对于主键索引 a,仅仅对值为 5 的索引加上 Record Lock(因为之前的规则)。而对于索引 b,需要加上 Next-Key Lock 索引,锁定的范围是(1,3]。除此之外,还会对其下一个键值加上 Gap Lock,即还有一个范围为(3,6)的锁。 大家可以再新开一个会话,执行下面的 SQL 语句,会发现都会被阻塞。

SELECT * FROM e4 WHERE a = 5 FOR UPDATE;  # 主键a被锁INSERT INTO e4 SELECT 4,2;   # 插入行b的值为2,在锁定的(1,3]范围内INSERT INTO e4 SELECT 6,5; # 插入行b的值为5,在锁定的(3,6)范围内
复制代码

 InnoDB 引擎采用 Next-Key Lock 来解决幻读问题。因为 Next-Key Lock 是锁住一个范围,所以就不会产生幻读问题。但是需要注意的是,InnoDB 只在 Repeatable Read 隔离级别下使用该机制。

后记

 我们后续还会探讨 InnoDB 的事务的知识,请大家持续关注。

发布于: 2 小时前阅读数: 6
用户头像

程序员历小冰 2018.04.28 加入

历小冰的技术博客,专注于探讨后端生态的点点滴滴,内容包括微服务、分布式、数据库、性能调优和各类源码分析。

评论

发布
暂无评论
MySQL探秘(七):InnoDB行锁算法