作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。
爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。
本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。
正文
1. 加过锁了吗?
快速加锁逻辑主打简单、快速,它只能处理简单的情况,即通过简单的判断就能确定本次加锁操作不会被阻塞。
对于复杂一点的情况,就需要慢速加锁逻辑来处理了。
关于什么是复杂的情况,可以看前面介绍的慢速加锁条件,命中任何一个慢速加锁条件的,就是复杂的情况。
为了方便介绍和理解,本文中,我们把本次加锁的事务称为 T1,本次加锁的记录称为 R1。
慢速加锁逻辑主打全方位无死角,可以处理更复杂的情况。它会判断事务 T1 是否对记录 R1 加过相同或者更高级别的行锁。如果是,本次就不需要重复加锁了。
既然主打全方位无死角,那就不能漏掉任何一个行锁结构。慢速加锁逻辑会遍历记录 R1 所属数据页对应的所有行锁结构。
遍历过程需要先找到记录 R1 所属数据页在 rec_hash
的数组中对应的行锁结构链表,步骤如下:
- 根据记录 R1 所属数据页的页号、表空间 ID 计算得到哈希值。
- 上一步的哈希值,对 rec_hash 的数组单元数量取模(哈希值 % 数组单元数量),得到 rec_hash 的数组下标。
- 根据上一步的数组下标,得到对应的行锁结构链表。
如果事务 T1 没有对记录 R1 加过锁,找到的行锁结构链表有可能为空,也有可能不为空。
因为其它事务对记录 R1 加锁,锁结构会加入这个行锁结构链表。包含 T1 在内的任何事务对其它记录加锁,锁结构也可能放入这个行锁结构链表。
如果事务 T1 对记录 R1 加过锁,找到的行锁结构链表一定不为空。
如果找到的行锁结构链表不为空,就可以开始遍历了。遍历时,从链表头部开始,每次取出一个行锁结构。
因为行锁结构链表是多个行锁结构通过各自的 hash
属性串连成的链表,我们把遍历过程中每次取出的一个行锁结构称为 hash 行锁结构。
对于每个 hash 行锁结构,都会判断两个条件:
- hash 行锁结构的
page_id
属性中保存的数据页的页号、表空间 ID,和记录 R1 所属数据页的页号、表空间 ID 相同。 - hash 行锁结构的 bitmap 中,对应记录 R1 的位的值为 1。
如果以上两个条件有一个不成立,说明这个 hash 行锁结构和记录 R1 没有关系,直接忽略这个行锁结构。
如果以上两个条件都成立,说明这个 hash 行锁结构有可能和记录 R1 有关系,需要进一步判断。
首先,看看这个 hash 行锁结构是否处于锁等待状态,如果是,说明这个行锁结构对应的行锁不可能满足本次加锁要求,直接忽略。
然后,看看这个 hash 行锁结构是否是事物 T1 创建的,如果不是,说明这个行锁结构对应的行锁也不可能满足本次加锁要求,直接忽略。
经过前面的一系列判断,如果这个 hash 行锁结构还没有被忽略,那么,它对应的行锁就有可能满足本次加锁要求。
接下来,需要判断它的锁模式、精确模式和本次加锁的锁模式、精确模式的强弱关系,判断过程分为两部分。
第一部分,判断 hash 行锁结构的锁模式,和本次加锁的锁模式的强弱关系。
和表锁一样,判断行锁的锁模式的强弱关系,也需要借助一个强弱关系图。
具体的判断逻辑如下:
- 根据 hash 行锁结构的锁模式,找到上图中对应的行。
- 根据本次加锁的锁模式,找到上一步的行中对应的列。
- 确定了行和列之后,就有了表示锁模式强弱关系的结果。
如果结果为减号(-
),表示 hash 行锁结构对应的行锁,比本次要加的行锁级别低,说明这个行锁结构不满足本次加锁要求,忽略这个行锁结构。
如果结果为加号(
),表示 hash 行锁结构对应的行锁,和本次要加的行锁相比,级别相同或者更高,说明这个行锁结构有可能满足本次加锁要求,进入第二部分
,再进一步判断。
第二部分,判断 hash 行锁结构的精确模式,和本次加锁的精确模式的强弱关系。
判断行锁的精确模式的强弱关系,没有强弱关系图可用,好在这个判断过程也不太复杂。
行锁的精确模式有四种:
- LOCK_REC_NOT_GAP(普通记录锁)。
- LOCK_GAP(间隙锁)。
- LOCK_ORDINARY(Next-Key 锁)。
- LOCK_INSERT_INTENTION(插入记录锁)。
插入意向锁,其实是一种特殊的间隙锁,它的精确模式包含 LOCK_GAP
和 LOCK_INSERT_INTENTION
。但是,它的加锁场景,都是某个事务想要插入记录到某个间隙,而这个间隙被其它事务加了间隙锁或者 Next-Key 锁。
其它三种精确模式单独使用时,都用于查询、更新、删除记录,和插入意向锁的场景不一样。
所以,判断行锁的精确模式的强弱关系时,会排除插入意向锁。
也就是说,如果 hash 行锁结构的精确锁模式包含 LOCK_INSERT_INTENTION
,遍历行锁结构的过程中,这个行锁结构会被忽略,不会进入判断锁模式、精确模式的强弱关系的逻辑。
如果 hash 行锁结构的精确锁模式是其它三种之一,只要这个精确模式和本次加锁的精确模式符合以下五种情况的一种:
- hash 行锁结构和本次加锁的精确模式都为
LOCK_REC_NOT_GAP
(普通记录锁)。 - hash 行锁结构和本次加锁的精确模式都为
LOCK_GAP
(间隙锁)。 - hash 行锁结构和本次加锁的精确模式都为
LOCK_ORDINARY
(Next-Key 锁)。 - hash 行锁结构的精确模式为
LOCK_ORDINARY
(Next-Key 锁),本次加锁的精确模式为LOCK_REC_NOT_GAP
(普通记录锁)。 - hash 行锁结构的精确模式为
LOCK_ORDINARY
(Next-Key 锁),本次加锁的精确模式为LOCK_GAP
(间隙锁)。
说明 hash 行锁的精确模式,和本次加锁的精确模式相比,级别相同(前三种情况)或者更高(后两种情况)。
这意味着事务 T1 对记录 R1 加过的锁,满足本次加锁的要求,不需要再重复加锁,本次加锁流程就此结束。
如果 hash 行锁结构的精确模式,和本次加锁的精确模式,不符合以上五种情况,说明事务 T1 对记录 R1 加过的锁,不满足本次加锁的要求。
如果遍历完整个行锁结构链表,都没有找到符合本次加锁要求的行锁结构,本次加锁流程需要继续进行,以完成加锁操作。
2. 需要等待吗?
如果事务 T1 没有对记录 R1 加过锁,或者之前加的锁比本次要加的锁级别低,就需要继续进行后面的加锁步骤了。
接下来,就要看看其它事务持有的行锁会不会阻塞事务 T1 对记录 R1 加锁,也就是判断本次加锁操作是否要进入锁等待状态。
这需要先找到记录 R1 所属数据页在 rec_hash
的数组中对应的行锁结构链表,步骤如下:
- 根据记录 R1 所属数据页的页号、表空间 ID 计算得到哈希值。
- 上一步的哈希值,对 rec_hash 的数组单元数量取模(哈希值 % 数组单元数量),得到 rec_hash 的数组下标。
- 根据上一步的数组下标,得到对应的行锁结构链表。
找到行锁结构链表之后,就可以遍历链表了。遍历时,每次取出一个行锁结构,我们同样称之为 hash 行锁结构。
对于每个 hash 行锁结构,都会判断两个条件:
- hash 行锁结构的
page_id
属性中保存的数据页的页号、表空间 ID,和记录 R1 所属数据页的页号、表空间 ID 相同。 - hash 行锁结构的 bitmap 中,对应记录 R1 的位的值为 1。
如果以上两个条件有一个不成立,说明这个 hash 行锁结构和记录 R1 没有关系,不会阻塞本次加锁操作,可以忽略这个行锁结构,继续进行下一轮遍历。
如果以上两个条件都成立,说明这个 hash 行锁结构和记录 R1 有关系,有可能会阻塞本次加锁,需要进一步判断,过程如下。
第 1 步,判断 hash 行锁结构是不是事务 T1 创建的。如果是,肯定不会阻塞它自己本次对记录 R1 的加锁操作,判断过程结束。
第 2 步,判断 hash 行锁结构的锁模式,和本次加锁的锁模式的兼容性。
和表锁一样,判断行锁的锁模式的兼容性,也需要借助锁模式的兼容关系图。
具体的判断逻辑如下:
- 根据本次加锁的锁模式,找到上图中对应的行。
- 根据 hash 行锁结构的锁模式,在上一步的行中找到对应的列。
- 确定行和列之后,就有了表示两种锁模式的兼容关系的结果。
如果结果为加号(
),说明 hash 行锁结构的锁模式和本次加锁的锁模式兼容,判断过程结束。
如果结果为减号(-
),说明两者的锁模式不兼容,进入第 3 步
,看看是不是符合其它条件,让这个 hash 行锁结构不会阻塞本次加锁操作。
第 3 步,如果事务 T1 是高优先级事务,创建 hash 行锁结构的事务不是高优先级事务,并且 hash 行锁结构处于等待状态,它就不会阻塞事务 T1 对记录 R1 的本次加锁操作,判断过程结束。
这意味着事务 T1 插队了。
否则,进入第 4 步
。
第 4 步,如果本次加的不是插入意向锁,只要满足以下两个条件之一:
- R1 是 supremum 记录(数据页中最后一条记录,也就是伪记录)。
- 本次加的是间隙锁(LOCK_GAP)。
hash 行锁结构就不会阻塞本次加锁操作,判断过程结束。否则进入第 5 步
。
第 5 步,如果本次加的是普通记录锁(LOCK_REC_NOT_GAP
)或者 Next-Key 锁(LOCK_ORDINARY
),hash 行锁结构的对应的是间隙锁(LOCK_GAP
),它就不会阻塞本次加锁操作,判断过程结束。否则,进入第 6 步
。
第 6 步,如果本次加的是插入意向锁,hash 行锁结构对应的是普通记录锁(LOCK_REC_NOT_GAP
),它就不会阻塞本次加锁操作,判断过程结束。否则,进入第 7 步
。
第 7 步,如果 hash 行锁结构对应的是插入意向锁,不会阻塞本次加锁操作,判断过程结束。否则,进入第 8 步
。
第 8 步,如果本次加的不是插入意向锁,加锁模式是排他锁,hash 行锁结构的锁模式也是排他锁,并且事务 T1 阻塞了 hash 行锁结构获得记录 R1 的锁,hash 行锁结构也不会阻塞本次加锁操作,判断过程结束。
如果前面所有步骤都没有结束判断过程,意味着 hash 行锁结构会阻塞本次加锁操作,事务 T1 对记录 R1 的加锁操作会进入锁等待状态。
如果前面任何一个步骤结束了判断过程,说明 hash 行锁结构不会阻塞本次加锁操作,可以继续进行加锁操作的后续步骤。
3. 先找个复用的行锁结构
经过前面的一系列步骤,如果本次加锁操作不会被阻塞,那就意味着可以立即获得锁。
接下来,先尝试找一个可以复用的行锁结构,分为两步进行。
首先,找到记录 R1 所属数据页对应的第一个行锁结构,过程如下:
- 根据记录 R1 所属数据页的页号、表空间 ID 计算得到哈希值。
- 上一步的哈希值,对 rec_hash 的数组单元数量取模(哈希值 % 数组单元数量),得到 rec_hash 的数组下标。
- 根据上一步的数组下标,得到对应的行锁结构链表。
- 遍历行锁结构链表,每次取一个行锁结构,碰到
page_id
属性中保存的数据页的页号、表空间 ID,和记录 R1 所属数据页的页号、表空间 ID 相同的第一个行锁结构时,结束遍历,返回这个行锁结构。
然后,判断上一步找到的行锁结构,是否满足以下条件:
- 创建这个行锁结构的事务就是 T1,也就是本次要加锁的事务。
- 这个行锁结构的锁模式、精确模式和本次加锁的锁模式、精确模式相同,并且不处于等待状态。
- 这个行锁结构的 bitmap 区域有记录 R1 对应的位,可以用于标识记录 R1 的加锁状态。
如果满足以上三个条件,说明上一步找到的行锁结构可以复用,否则,需要继续寻找行锁结构链表中,记录 R1 所属数据页对应的下一个行锁结构。
对于每个找到的行锁结构,都判断是否满足以上三个条件,只要碰到一个满足的,就复用这个行锁结构。
如果找到了复用的行锁结构,直接把这个行锁结构的 bitmap 区域中,对应记录 R1 的位设置为 1,就表示事务 T1 对记录 R1 加了锁。
如果遍历完整个行锁结构链表,都没有找到可以复用的行锁结构,那就需要申请一个新的行锁结构。
4. 没找到就申请一个新的
经过前面的步骤,如果没有找到可以复用的行锁结构,就申请一个新的行锁结构。
拿到新的行锁结构之后,初始化行锁结构的各属性,并把行锁结构加入 rec_hash 中记录 R1 所属数据页对应的行锁结构链表的头部,以及事务对象的 trx_locks 链表的尾部。
这个步骤的过程和前面介绍第一种快速加锁逻辑(快速加锁之一)申请新的行锁结构的一样,这里就不再重复了。
5. 总结
慢速加锁逻辑的主要流程:
- 判断事务 T1 对记录 R1 是否加过锁。 如果加过锁,并且满足本次加锁的要求,本次不需要重复加锁,加锁流程就此结束。
- 判断其它事务是否会阻塞事务 T1 对记录 R1 的本次加锁操作。 如果是,本次加锁操作进入锁等待状态,否则,进入下一步,继续后面的加锁步骤。
- 先尝试找一个可以复用的行锁结构。 如果找到了,把行锁结构的 bitmap 区域中对应记录 R1 的位设置为 1,加锁流程就此结束,否则,进入下一步。
- 申请新的行锁结构。 拿到新的行锁结构之后,初始化行锁结构的各属性,把行锁结构的 bitmap 区域中对应记录 R1 的位设置为 1,然后把行锁结构加入两个行锁结构链表。