事务获得锁之后,哪些情况下会释放锁?本期我们聊聊这个主题。

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

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

1. 主动和被动

事务申请加表锁或者行锁,有可能立即获得锁,也有可能被其它事务持有的锁阻塞,需要先进入锁等待状态,等其它事务释放表锁或者行锁之后,申请加锁的事务才能获得锁。

立即获得锁,我们可以认为是主动获得锁;进入锁等待状态之后才获得锁,我们可以认为是被动获得锁,或者是授予锁。

我们解决工作日午餐的流行方式,莫过于订餐了。订餐之后,我们有两种方式可以拿到餐,一种是到店自取,另一种是外卖。

如果把事务加锁比作订餐,申请加锁就是下单,立即获得锁就是到店取餐,授予锁就是外卖。

商家备好餐之后,如果我们自己到店取餐,可以立即拿到餐,不需要等待。

如果是外卖送餐,外卖小哥一次并不只给一个人送餐,通常是给多个人送餐。

给多个人送餐,就涉及到先后的问题,他可能先送顺路的餐(对应高优先级事务),也可能先送快要超时的餐(锁等待时间太长,提升了权重),然后才给我们送餐,我们只能坐等送达。

关于立即获得锁,前面加表锁、行锁的快速加锁和慢速加锁,已经介绍过。

现在,我们来看看怎么授予锁。

2. 授予表锁

事务释放某个表的表锁,从事务对象的 trx_locks 链表、表对象的 locks 链表中删除表锁结构之后,就要给等待获得这个表的表锁的事务授予锁了。

授予表锁需要遍历表对象的 locks 链表,遍历过程中,每次取一个表锁结构。

不过,这里并不会从头开始遍历表对象的 locks 链表,而是从当前释放的表锁对应表锁结构的下一个表锁结构开始。

对于每个表锁结构,都会判断以下两个条件:

  • 表锁结构处于等待状态(type_mode 属性的第 9 位为 1)。
  • 表锁结构对应的表锁,被当前释放的表锁阻塞。

为了方便介绍和理解,我们把满足以上两个条件的表锁结构称为 waiting 表锁结构

只有 waiting 表锁结构,才会进行接下来授予表锁的操作,决定是否给它所属的事务授予表锁。

接下来的操作,就是判断 waiting 表锁结构是否会被表对象的 locks 链表中,排在它前面的表锁结构对应的表锁阻塞。

如果不会被阻塞,就给 waiting 表锁结构所属的事务授予表锁,主要流程如下:

  • 取消 waiting 表锁结构的等待状态(type_mode 属性的第 9 位修改为 0)。
  • 唤醒 waiting 表锁结构所属的事务(触发事务占用的 slot 中 srv_slot_t 对象的 event 事件)。

如果会被阻塞,就要更新 waiting 表锁结构所属事务的锁等待关系了。

这个事务,之前是等待当前正在释放的表锁所属的事务,现在变成等待表对象的 locks 链表中,排在 waiting 表锁结构前面的某个表锁结构所属的事务了。

更新锁等待关系之后,会给死锁检查线程(也是超时检查线程)发送通知,告诉死锁检查线程该开工了。

通过以上授予锁的流程,我们可以看到,授予表锁的逻辑比较简单,就是按照进入锁等待状态的先后顺序来授予锁。

3. 授予行锁

事务释放行锁,分为两种情况:

  • 释放一条记录的行锁。
  • 释放一个或多个行锁结构中所有记录的行锁。

对于第二种情况,释放过程中,会遍历每个行锁结构的 bitmap 内存区域。如果某个位为 1,释放这条记录的行锁,也就变成第一种情况了。

归根结底,InnoDB 释放行锁的操作,每次只释放一条记录的行锁。如果要释放多条记录的行锁,重复进行释放一条记录的行锁的过程就可以了。

既然释放行锁是按单条记录进行的,授予行锁自然也是一次只处理一条记录了。

接下来,我们就来看看 InnoDB 怎么授予一条记录的行锁。

3.1 准备两个数组

授予表锁,只考虑先来后到。授予行锁,逻辑复杂一点,除了先来后到,还要考虑事务的优先级和权重。

为了把这个复杂的逻辑简单化,InnoDB 会按照事务的优先级、权重、先来后到三个维度,把处于等待状态的所有事务都归置到一起,放到 waiting 数组里。

然后从前到后遍历 waiting 数组,进行授予行锁的操作就可以了,具体逻辑我们稍后会介绍。

除了 waiting 数组,授予行锁还需要一个 granted 数组。正式开始执行授予行锁的操作之前,需要先构造好这两个数组。

granted 数组,存放已经获得锁(包括立即获得锁和授予锁)的行锁结构。

这个数组里的行锁结构,按照获得锁的时间倒序存放。最早获得锁的行锁结构,放在数组末尾;最晚获得锁的行锁结构,放在数组开头。

waiting 数组,存放处于锁等待状态的行锁结构。

首先,所有高优先级事务(优先级大于 0)的行锁结构,不管优先级的值大小,都按照进入锁等待状态的先后顺序,放到这个数组里。

然后,高权重事务(权重大于 1)的行锁结构,按照权重从大到小、权重相同则按照进入锁等待状态的先后顺序,放到这个数组里。

最后,低权重事务(权重小于等于 1)的行锁结构,不管权重值的大小,都按照进入锁等待状态的先后顺序,放到这个数组里。

3.2 正式授予锁

为了更好理解授予行锁的逻辑,我们有必要提前介绍 granted 数组在授予行锁过程中的变化。

授予行锁过程中,如果某个事务被授予了锁,它的处于锁等待状态的行锁结构会被追加到 granted 数组里(放到最后)。

也就是说,granted 数组里,既包括之前就已经获得锁的行锁结构,也包括本次授予行锁过程中新获得锁的行锁结构。它在授予行锁过程中是动态变化的。

介绍完 granted 数组,接下来就是正式为一条记录授予行锁的操作了。

这个过程会遍历 waiting 数组,每次取一个行锁结构,我们称之为 waiting 行锁结构

对于每个 waiting 行锁结构,判断它对应的行锁,是否会被 granted 数组中某个行锁结构对应的行锁阻塞。

如果不会被阻塞,就给 waiting 行锁结构所属的事务授予行锁,主要流程如下:

  • 取消行锁结构的等待状态(type_mode 属性的第 9 位修改为 0)。
  • 唤醒行锁结构所属的事务(触发事务占用的 slot 中 srv_slot_t 对象的 event 事件)。
  • 把行锁结构移动到 rec_hash 的数组中对应行锁结构链表的最前面(先删除,再插入到最前面)。
  • 把行锁结构加入 granted 数组。

以上流程中,所有行锁结构都是指的 waiting 行锁结构。

如果会被阻塞,就要更新 waiting 行锁结构所属事务的锁等待关系了。

这个事务,之前是等待当前正在释放的行锁所属的事务,现在变成等待 granted 数组中某个行锁结构所属的事务了。

更新锁等待关系之后,会给死锁检查线程(也是超时检查线程)发送通知,告诉死锁检查线程该开工了。

4. 总结

给某个表授予表锁的逻辑比较简单,按照先来后到的顺序,把表锁授予最先进入锁等待状态的事务。

如果有其它事务申请的表锁和已经授予的表锁兼容,也会一同授予表锁。

给某条记录授予行锁的逻辑有点复杂,授予顺序如下:

  • 先授予高优先级事务,按照进入锁等待状态的先后顺序授予。
  • 没有高优先级事务,再授予高权重事务。
    权重不同,先授予权重大的;权重相同,则按照进入锁等待状态的先后顺序授予。
  • 没有高权重事务,再授予低权重事务,按照进入锁等待状态的先后顺序授予。

当然了,以上授予顺序只针对多个事务申请的行锁互斥的情况。

如果给某个事务授予了行锁,其它事务申请的行锁和已经被授予的行锁兼容,也都会被授予行锁,不受上面授予顺序的影响。


操盛春

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