终于来到死锁检查线程的第三步,可以解决死锁了。

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

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

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

1. 选择死锁受害事务

前面介绍了死锁线程做的准备工作,以及发现死锁的过程。现在,是时候解决死锁了。

解决死锁最重要的事情,就是决定回滚死锁环中哪个事务,也就是选择哪个事务作为死锁受害事务。

选择死锁受害事务之前,还要做一件比较重要的小事,就是按照死锁环中各事务进入锁等待状态的时间从先到后进行排序。排序之后的事务,会存放到一个数组里,我们称之为死锁数组

之所以要这么做,是为了根据其它条件无法选出哪个事务作为死锁受害事务的情况下,选择最晚进入锁等待状态的事务作为死锁受害事务。

给死锁环中各事务排序之后,就可以基于死锁数组来选择死锁受害事务了。

这个过程当然又要遍历死锁数组了,同样,每次取死锁数组中的一个事务。

第 1 轮循环有点特殊,直接把取到的事务(死锁数组中第一个事务)作为候选受害事务。

第 2 轮及以后的循环,把取到的事务和上一轮循环选出来的候选受害事务进行比较,决定两者之中谁作为本轮循环的受害事务。

选择谁作为本轮循环的受害事务,这是个艰难的决定,过程如下。

第 1 步,根据两个事务的优先级,决定谁是本轮循环的受害事务。

两个事务中,如果一个是高优先级事务(优先级大于 0),一个是低优先级事务(优先级等于 0),选择低优先级事务作为本轮循环的受害事务。

如果两个事务都是高优先级事务(优先级大于 0),选择优先级更低的事务作为本轮循环的受害事务。

如果两个事务都是低优先级事务(优先级等于 0),进入第 2 步

第 2 步,根据事务是否改变(插入、更新、删除)了不支持事务的表(例如 MyISAM 表)的数据,决定谁是本轮循环的受害事务。

两个事务中,如果只有一个事务改变了不支持事务的表的数据,选择它作为本轮循环的受害事务。

如果两个事务都没有改变,或者都改变了不支持事务的表的数据,进入第 3 步

第 3 步,根据事务的回滚成本,决定谁是本轮循环的受害事务。

事务的回滚成本,由两部分相加得到:

  • 事务进入锁等待状态之前,产生的 undo 日志数量。
  • 事务进入锁等待状态之前,加表锁和行锁总共创建了几个锁结构。

如果两个事务回滚成本不同,选择成本低的那个作为本轮循环的受害事务,否则进入第 4 步

第 4 步,选择本轮循环取到的事务作为受害事务。

来到这一步,说明前三步都无法在两个事务中选出一个作为本轮循环的死锁受害事务。

这两个事务是:本轮循环取到的事务、上一轮循环选出来的受害事务。

因为死锁数组中各事务已经按照进入锁等待状态的时间先后排了序,这一步直接把本轮循环取到的事务作为本轮循环的受害事务,其实隐含了一个逻辑,就是选择两个事务中更晚进入锁等待状态的事务,作为本轮循环的受害事务。

遍历完死锁数组中所有事务之后,最终会选出一个事务作为受害事务。

2. 计算并更新事务权重

前面介绍过,在准备工作阶段,死锁线程提升阻塞事务权重时,死锁环中锁等待事务的权重,不会累加到阻塞事务的权重上,而是要等到确定死锁受害事务之后,再为死锁环中除受害之外的其它事务进行一次提升权重的操作。

现在,是时候了。

提升权重的过程,从被死锁受害事务阻塞的那个事务开始,根据死锁环中各事务的等待关系,逐个把锁等待事务的权重累加阻塞事务的权重上。

上面只介绍了提升权重操作,其实还有一个降低权重操作,就是把死锁受害事务的权重降为 0。

以上提升权重、降低权重操作的结果,都临时存放在权重数组里。

完成以上操作之后,死锁环中所有事务的权重都会更新到对应的事务对象中。

3. 记录死锁日志

如果系统变量 innodb_print_all_deadlocks 的值为 ON,死锁检查线程还会把死锁的详细信息写入 MySQL 的错误日志文件中。

示例 SQL 写入 MySQL 错误日志文件的死锁信息如下:

2024-07-07T13:00:15.602373Z 0 [Note] [MY-012468] [InnoDB] Transactions deadlock detected, dumping detailed information.
2024-07-07T13:00:15.602446Z 0 [Note] [MY-012469] [InnoDB]  *** (1) TRANSACTION:
TRANSACTION 227599, ACTIVE 21 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1192, 2 row lock(s)
MySQL thread id 8, OS thread handle 123145400471552, query id 96 localhost 127.0.0.1 root statistics
SELECT i1 FROM t1 WHERE id = 20 FOR UPDATE
2024-07-07T13:00:15.602597Z 0 [Note] [MY-012469] [InnoDB]  *** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 46 n bits 80 index PRIMARY of table `test`.`t1` trx id 227599 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 0000000a; asc     ;;
 1: len 6; hex 000000035958; asc     YX;;
 2: len 7; hex 82000000a50110; asc        ;;
 3: len 4; hex 80000065; asc    e;;

2024-07-07T13:00:15.603277Z 0 [Note] [MY-012469] [InnoDB]  *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 46 n bits 80 index PRIMARY of table `test`.`t1` trx id 227599 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000014; asc     ;;
 1: len 6; hex 000000035958; asc     YX;;
 2: len 7; hex 82000000a5011d; asc        ;;
 3: len 4; hex 800000c9; asc     ;;

2024-07-07T13:00:15.603950Z 0 [Note] [MY-012469] [InnoDB]  *** (2) TRANSACTION:
TRANSACTION 227600, ACTIVE 17 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1192, 2 row lock(s)
MySQL thread id 11, OS thread handle 123145401536512, query id 97 localhost 127.0.0.1 root statistics
SELECT * FROM t1 WHERE id = 10 FOR UPDATE
2024-07-07T13:00:15.604083Z 0 [Note] [MY-012469] [InnoDB]  *** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 46 n bits 80 index PRIMARY of table `test`.`t1` trx id 227600 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000014; asc     ;;
 1: len 6; hex 000000035958; asc     YX;;
 2: len 7; hex 82000000a5011d; asc        ;;
 3: len 4; hex 800000c9; asc     ;;

2024-07-07T13:00:15.604741Z 0 [Note] [MY-012469] [InnoDB]  *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 46 n bits 80 index PRIMARY of table `test`.`t1` trx id 227600 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 0000000a; asc     ;;
 1: len 6; hex 000000035958; asc     YX;;
 2: len 7; hex 82000000a50110; asc        ;;
 3: len 4; hex 80000065; asc    e;;

2024-07-07T13:00:15.605401Z 0 [Note] [MY-012469] [InnoDB] *** WE ROLL BACK TRANSACTION (2)

前面带日期和时间的日志,只有系统变量 log_error_verbosity 的值为 3,才会记录到 MySQL 错误日志文件中。

4. 唤醒死锁受害事务

死锁环中,选择出来的受害事务,会回滚。回滚操作并不是由死锁检查线程完成,而是由事务自己完成。

要想让受害事务自己回滚,它得知道自己被选择成为死锁受害事务了,这个操作由死锁检查线程完成。

死锁检查线程会给死锁受害事务打个标志,让它在被唤醒之后,知道自己被选择成为死锁受害事务了。

死锁受害事务进入锁等待状态之前,创建了一个锁结构,这个锁结构的 type_mode 属性的第 9 位被设置为 1 了,表示这个锁结构处于锁等待状态。

现在,这个锁结构需要从事务对象的 trx_locks 链表中删除。

如果这个锁结构对应的是行锁,还需要从 rec_hash 的数组中对应的行锁结构链表中删除。

如果这个锁结构对应的是表锁,还需要从表对象的 locks 链表中删除。

然后,死锁检查线程会触发死锁受害事务的等待事件,唤醒死锁受害事务。这个等待事件,保存在死锁受害事务占用的那个 slot 对应的 srv_slot_t 对象的 event 属性中。

到这里,死锁检查线程检查并解决死锁的过程就结束了。

剩下工作,就由死锁受害事务自己完成了。

死锁受害事务要完成什么工作?

当然是回滚了。

5. 总结

死锁检查线程解决死锁的过程如下:

  • 把死锁环中各事务按照进入锁等待状态的先后顺序排好序,放到死锁数组中。
  • 遍历死锁数组,每轮循环取一个事务。
  • 第 1 轮循环取死锁数组中第 1 个事务作为候选死锁受害事务。
  • 第 2 轮及以后的循环,根据事务的优先级、是否改变了不支持事务的表的数据、事务的回滚成本,从本轮循环取到的事务,和上一轮循环选出来的死锁受害事务两者中选择一个,作为本轮循环的受害事务。
  • 最后一轮循环选出来的受害事务,就是最终的死锁受害事务,这个事务会回滚。

选出死锁受害事务之后,死锁检查线程还会根据系统变量 innodb_print_all_deadlocks 的值,决定是否记录死锁日志。

然后,会给死锁受害事务打个标记,再唤醒死锁受害事务。


操盛春

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