MySQL 核心模块揭秘 | 28 期 | 什么时候释放锁?

2024-09-14 18:49:19 浏览数 (1)

作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。

爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。


本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。

正文

1. 概述

InnoDB 事务执行过程中,加表锁或者行锁之后,释放锁最常见的时机是事务提交或者回滚即将完成时。

因为事务的生命周期结束,它加的锁的生命周期也随之结束。

有一种情况,加锁只是权宜之计,临时为之。如果这种锁也要等到事务提交或者回滚即将完成时才释放,阻塞其它事务的时间也可能更长,这就有点不合理了。所以,这种锁会在事务运行过程中及时释放。

还有一种情况,虽然是在事务提交过程中释放锁,但是并不会等到提交即将完成时才释放,而是在二阶段提交的 prepare 阶段就提前释放。

最后,有点特殊的就是 AUTO-INC 锁了。

2. 不匹配 where 条件

我们先来看看只是权宜之计的加锁场景。

select、update、delete 语句执行过程中,不管 where 条件是否命中索引,也不管是等值查询还是范围查询,只要扫描过的记录,都会加行锁。

和 update、delete 不一样,select 只在需要加锁时,才会按照上面的逻辑加锁。

可重复读(REPEATABLE-READ)、可串行化(SERIALIZABLE)两种隔离级别,只要加了锁,不管是表锁还是行锁,都要等到事务提交或者回滚即将完成时才释放(手动加的表锁除外)。这就是我们前面说的释放锁最常见的时机了。

读未提交(READ-UNCOMMITTED)、读已提交(READ-COMMITTED)两种隔离级别,如果发现记录不匹配 where 条件,会及时释放行锁。这又分为两种情况。

情况 1,如果部分或者全部 where 条件下推到了存储引擎,InnoDB 每读取一条记录,都会判断记录是否匹配下推的 where 条件。

情况 2,server 层每次收到 InnoDB 返回的一条记录,也会判断记录是否匹配 server 层的 where 条件。

以上两种情况,只要记录不匹配 where 条件,就会马上释放当前 SQL 语句对记录加的行锁(其实有个例外情况,稍后介绍)。

这里释放行锁,只会释放不匹配 where 条件的这一条记录上的行锁,过程也比较简单,就是把行锁结构的 bitmap 内存区域中,这条记录对应的位设置为 0。

如果有其它事务正在等待获得这条记录的行锁,还会根据行锁的授予规则,给其它事务授予锁。

前面提到有一个例外情况,现在,该它出场了。

如果事务对某条记录加行锁,没有立即获得锁,而是进入了锁等待状态,等其它事务释放锁之后才获得锁。InnoDB 或者 server 层发现这条记录不匹配 where 条件,并不会释放它的行锁。

这是为什么呢?

因这经过锁等待状态之后才获得的行锁,事务就不知道是哪条 SQL 语句执行时给加的行锁了,所以,即使发现记录不匹配 where 条件,也不会释放它的行锁。

3. prepare 阶段

读未提交(READ-UNCOMMITTED)、读已提交(READ-COMMITTED)两种隔离级别下:

  • select、update、delete 语句全表扫描、索引范围扫描过程中,只会对索引记录加普通记录锁,不会加间隙锁和 Next-Key 锁。
  • 外键约束检查、重复值检查这两个场景下,还是会对索引记录加间隙锁或者 Next-Key 锁的。

这两种隔离级对索引记录前面间隙的锁定,不需要等到事务提交或者回滚即将完成时才释放,在事务二阶段提交的 prepare 阶段就可以提前释放。

Next-Key 锁既锁定索引记录本身,又锁定索引记录前面的间隙。

如果释放索引记录的 Next-Key 锁,就意味着释放了索引记录本身和前面间隙的锁定,这显然是不行的,因为索引记录本身的锁定,要等到事务提交或者回滚即将完成时,才能释放。

那么,Next-Key 锁要怎么释放?

稍安勿躁,我们一起来看。

释放索引记录前面间隙的锁定,需要遍历事务对象的 trx_locks 链表,遍历过程中,每次取一个锁结构(可能是表锁结构或者行锁结构)。

如果锁结构对应的是间隙锁,直接释放索引记录上的间隙锁,主要流程如下:

  • 从事务对象的 trx_locks 链表中删除行锁结构。
  • 从 rec_hash 的数组中找到锁结构所在的行锁结构链表,然后从链表中删除锁结构。
  • 锁结构的 bitmap 内存区域中,可能有一个或者多个位的值为 1,这些位对应的记录都被加了间隙锁。如果有其它事务正在等待获得这些记录上的行锁,根据行锁的授予规则,给这些事务授予锁。

如果锁结构对应的是 Next-Key 锁,只释放索引记录前面间隙的锁定,保留索引记录本身的锁定,主要流程如下:

  • 给锁结构的 type_mode 属性加上 LOCK_REC_NOT_GAP 标志,也就是直接把 Next-Key 锁变成了普通记录锁。
  • 锁结构的 bitmap 内存区域中,可能有一个或者多个位的值为 1,这些位对应的记录都被加了 Next-Key 锁,现在都变成了普通记录锁。如果有其它事务想往这些记录前面的间隙插入记录被阻塞了,现在就可以根据行锁的授予规则,给这些事务授予插入意向锁了。

4. 事务提交或回滚

事务加行锁的共享锁、排他锁之前,会分别加表级别的意向共享锁、意向排他锁,这两种表锁都要到事务提交或者回滚即将完成时才释放。

事物加的所有行锁,除了读未提交(READ-UNCOMMITTED)、读已提交(READ-COMMITTED)两种隔离级别已经释放的不匹配 where 条件的记录上的行锁、索引记录前面间隙的锁定之外,剩下的行锁,都要等到事务提交或者回滚即将完成时才释放。

事务提交或者回滚事务即将完成时,释放表锁和行锁需要遍历 trx_locks 链表,遍历过程中,每次取一个锁结构。

对于行锁结构,释放锁的主要流程如下:

  • 从事务对象的 trx_locks 链表中删除行锁结构。
  • 从 rec_hash 的数组中找到锁结构所在的行锁结构链表,然后从链表中删除行锁结构。
  • 锁结构的 bitmap 内存区域中,可能有一个或者多个位的值为 1,这些位对应的记录都被加了某种行锁。如果有其它事务正在等待获得这些记录上的行锁,根据行锁的授予规则,给这些事务授予锁。

对于表锁结构,释放锁的主要流程如下:

  • 从事务对象的 trx_locks 链表中删除表锁结构。
  • 从表对象的 locks 链表删除表锁结构。
  • 如果有其它事务正在等待获得这个表的表锁,根据表锁的授予规则,给这些事务授予锁。

5. AUTO-INC 锁

把 AUTO-INC 锁单独拿出来说,是因为它有点特殊。

前面介绍 InnoDB 表锁时,我们介绍过,AUTO-INC 锁有两种类型,一种是轻量锁,这其实只是个互斥量。

另一种才是真正的表级别的 AUTO-INC 锁,它会创建表锁结构,和表级别的意向共享锁、意向排他锁一样,都属于表锁。

AUTO-INC 锁的这两种类型,释放时机不同。

轻量锁,用完就释放,也就是 insert 或者 update 语句获取到自增列的自增值之后,就可以释放了。

因为这种类型,只是个互斥量,释放也很简单,调用释放互斥量的方法就可以了。

此时,可能有其它事务正在等待获得这个表的轻量 AUTO-INC 锁,也就是等待获得这个互斥量,由操作系统决定哪个事务能获得这个互斥量。

真正的表级别的 AUTO-INC 锁,要等到加锁的 SQL 语句执行完成才释放,主要流程如下:

  • 从事务对象的 autoinc_locks 数组中删除表锁结构。
  • 从事务对象的 trx_locks 链表中删除表锁结构。
  • 从表对象的 locks 链表中删除表锁结构。
  • 如果有其它事务正在等待获得这个表的 AUTO-INC 锁,根据表锁的授予规则,给这些事务授予锁。

6. 总结

事务执行过程中加的表锁,都要等到提交或者回滚即将完成时才释放(手动加的表锁除外)。

事务执行过程中加的行锁,根据事务隔离级别的不同,释放时机不同。

可重复读(REPEATABLE-READ)、可串行化(SERIALIZABLE)两种隔离级别,事务加的所有行锁,都要等到提交或者回滚即将完成时才释放。

读未提交(READ-UNCOMMITTED)、读已提交(READ-COMMITTED)两种隔离级别,事务加的行锁,释放时机不同。

  • 对于不匹配 where 条件的记录,发现不匹配之后,server 层或者 InnoDB 就会释放这些记录的行锁。
  • 对于间隙锁或者 Next-Key 锁,在二阶段提交的 prepare 阶段,会释放记录前面间隙的锁定,保留记录本身的锁定。
  • 剩余未释放的行锁,都要等到事务提交或者回滚即将完成时才释放。

AUTO-INC 锁有两种类型,对应两种释放时机:

  • 轻量锁,用完就释放。
  • 真正的表级别的 AUTO-INC 锁,加锁的 SQL 语句执行完成时释放。

0 人点赞