准备

MySQL 内核版本: 8.0.17

在MySQL中,当两个或两个以上的事务相互持有或者请求锁,并形成一个循环的依赖关系,就会产生死锁. 多个事务同时锁定同一个资源时,也会产生死锁. 在一个事务系统中,死锁是确切存在并且是不能完全避免的. InnoDB 会在每一个事务申请锁时触发死锁检测,并选择一个事务回滚.

在 MySQL 中,事务在申请 record lock 后假如无法立即获取锁会进行死锁检测. 在事务的回滚中,会释放该事务持有的所有 lock.

用户可以配置--innodb-deadlock-detect[={OFF|ON}]选择是否打开死锁检测.

死锁检测

我们从源码层面分析 MySQL 的死锁检测机制,直接通过源码分析可以更直观的介绍死锁检测机制. MySQL 的死锁检测算法是深度优先搜索,如果在搜索过程中发现了环,就说明发生了死锁. 为了避免死锁检测开销过大,如果搜索深度超过了 200(LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK)也同样认为发生了死锁。

基本的代码流程如下, add_to_waitq()是申请 Record Lock 的入口函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

/* storage/innobase/lock/lock0lock.cc */

-------------------------
| RecLock::add_to_waitq() |
-------------------------
|
| /* 创建 lock. */
| -------------------
--> | RecLock::create() |
| -------------------
| |
| | /* 分配 lock, 初始化 lock_t. */
| | -----------------------
| --> | RecLock::lock_alloc() |
| | -----------------------
| |
| | /* 插入 lock_sys->rec_hash. */
| | ---------------------
| --> | RecLock::lock_add() |
| ---------------------
|
| /* 假如事务的优先级较高,尝试跳过低优先级的事务直接获取 lock. */
| -----------------------
--> | RecLock::jump_queue() |
| -----------------------
|
| /* 否则需要进行死锁检测. */
| ---------------------------
--> | RecLock::deadlock_check() |
---------------------------
|
| /* 死锁检测,假如存在死锁返回一个需要被回滚的事务. */
| --------------------------------------
--> | DeadlockChecker::check_and_resolve() |
| --------------------------------------
|
| /* 检查死锁检测的结果. */
| ----------------------------------
--> | RecLock::check_deadlock_result() |
----------------------------------

死锁检测的主流程代码在DeadlockChecker::check_and_resolve():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* storage/innobase/lock/lock0lock.cc */

/* lock: 当前事务申请的 lock
* trx: 当前事务
*/
const trx_t *DeadlockChecker::check_and_resolve(const lock_t *lock,
trx_t *trx) {
/* 确保同时持有 lock_sys->mutex 和 trx->mutex. */
ut_ad(lock_mutex_own());
ut_ad(trx_mutex_own(trx));
check_trx_state(trx);
ut_ad(!srv_read_only_mode);

if (trx->in_innodb & TRX_FORCE_ROLLBACK_ASYNC) {
/* 假如 trx 设置了 TRX_FORCE_ROLLBACK_ASYNC, 即不允许该事务等待锁从而
* 造成可能的死锁,我们应该选择该事务进行回滚操作. */
return (trx);
} else if (!innobase_deadlock_detect) {
/* 假如用户关闭了死锁检测,直接返回 NULL. */
return (NULL);
}

const bool was_trx_mutex_ownership_tracked = trx->owns_mutex;
trx->owns_mutex = false;
/* 释放 trx->mutex: trx 的事务状态只能被当前 thread 修改, 所以是安全的. */
trx_mutex_exit(trx);

const trx_t *victim_trx;

do {
/* 构建死锁检测 DeadlockChecker. */
DeadlockChecker checker(trx, lock, s_lock_mark_counter);

/* 进行死锁检测,并返回选中要回滚的事务. */
victim_trx = checker.search();

if (checker.is_too_deep()) {
/* 假如死锁检测过深, 打印死锁信息. */
ut_ad(trx == checker.m_start);
ut_ad(trx == victim_trx);

rollback_print(victim_trx, lock);

MONITOR_INC(MONITOR_DEADLOCK);

break;

} else if (victim_trx != NULL && victim_trx != trx) {
ut_ad(victim_trx == checker.m_wait_lock->trx);

/* 进行回滚. 释放持有的锁并唤醒 thread. */
checker.trx_rollback();

lock_deadlock_found = true;

MONITOR_INC(MONITOR_DEADLOCK);
}

} while (victim_trx != NULL && victim_trx != trx);

/* ... */

/* 重新持有 trx->mutex 锁. */
trx_mutex_enter(trx);
trx->owns_mutex = was_trx_mutex_ownership_tracked;

return (victim_trx);
}

关于MySQL死锁检测如何判断是否存在死锁核心代码在函数DeadlockChecker::search():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/* storage/innobase/lock/lock0lock.cc */

const trx_t *DeadlockChecker::search() {
/* 确保持有 lock_sys->mutex. */
ut_ad(lock_mutex_own());
/* 确保没有持有 trx->mutex. */
ut_ad(!trx_mutex_own(m_start));

/* m_start: 发起死锁检测的事务, 死锁检测全程不会改变, 以该 trx 为基准判断是否存在环.
* m_wait_lock: 发起死锁检测的事务等待的 lock, m_wait_lock 会随着 DFS 深度搜索过程改变.
*/
ut_ad(m_start != NULL);
ut_ad(m_wait_lock != NULL);
check_trx_state(m_wait_lock->trx);
ut_ad(m_mark_start <= s_lock_mark_counter);

ulint heap_no;
/* 获取 m_wait_lock 指向的 heap_no 上的第一个 lock. */
const lock_t *lock = get_first_lock(&heap_no);

for (;;) {
/* We should never visit the same sub-tree more than once. */
ut_ad(lock == NULL || !is_visited(lock));

while (m_n_elems > 1 && lock == NULL) {
/* 假如栈的元素数量大于1且 lock 为 NULL, 则代表某一条路径已经被搜索至尽头, 则进行
* 回溯从而重新搜索未被访问的节点, 即存在一行数据上有多个锁. */

pop(lock, heap_no);

/* 获取同一行数据上的下一个锁. */
lock = get_next_lock(lock, heap_no);
}

if (lock == NULL) {
/* 假如 lock 为 NULL, DFS 搜索结束, 结束循环. */
break;
} else if (lock == m_wait_lock) {
/* 假如 lock == m_wait_lock, 需要标记该子树已经被访问过. */

/* 这种情况只存在 DFS 回溯的阶段:
* lock_t 维护的 hash table 插入的顺序排列, 在 DFS 回溯阶段,
* 假如存在一个 record 上有多个 lock_t 在等待,
* 死锁检测算法会调用 get_next_lock(), 假如拿到了 lock == m_wait_lock,
* 即代表后面的 lock 应该都是 wait 的顺序的, 所以没有必要再去看那些等待的 trx.
ut_ad(lock->trx->lock.deadlock_mark <= m_mark_start);

/* 设置已经被访问的标记. */
lock->trx->lock.deadlock_mark = ++s_lock_mark_counter;

ut_ad(s_lock_mark_counter > 0);

/* 设置 lock 为 NULL. */
lock = NULL;

} else if (!lock_has_to_wait(m_wait_lock, lock)) {
/* 假如 m_wait_lock 和 lock 之间不存在等待关系,则需要
* 获取 heap_no 对应链表上的下一个lock. */
/* No conflict, next lock */
lock = get_next_lock(lock, heap_no);

} else if (lock->trx == m_start) {
/* 假如 lock 所指向的事务是当前发起死锁检测的事务, 即存在环. */

/* 打印关于死锁信息的Log. */
notify(lock);

/* ... */

return (select_victim());

} else if (is_too_deep()) {
/* 假如 DFS 搜索的栈元素超过了200或者访问的节点数目超过了 1000000,
* 则返回 m_start 作为回滚的事务. */
m_too_deep = true;
return (m_start);

} else if (lock->trx_que_state() == TRX_QUE_LOCK_WAIT) {
/* Another trx ahead has requested a lock in an
incompatible mode, and is itself waiting for a lock. */

/* 假如 lock 所属的 trx 处于 TRX_QUE_LOCK_WAIT,即处于锁等待的状态.
* 需要将<lock, heap_no> 入栈,DeadlockChecker 利用数组实现栈. */
++m_cost;

if (!push(lock, heap_no)) {
/* 假如入栈失败,即栈的元素数量超过了4096, 标记 m_too_deep, 并返回
* m_start 事务回滚. */
m_too_deep = true;
return (m_start);
}

/* 使用 lock 替换 m_wait_lock, 用作下一次搜索. */
m_wait_lock = lock->trx->lock.wait_lock;

/* 获取当前 m_wait_lock 所属的 heap_no 的第一个 lock. */
lock = get_first_lock(&heap_no);

/* 假如该 lock 已经被访问过,则获取下一个 lock. */
if (is_visited(lock)) {
lock = get_next_lock(lock, heap_no);
}

} else {
/* 否则获取下一个 lock. */
lock = get_next_lock(lock, heap_no);
}
}

ut_a(lock == NULL && m_n_elems == 0);

/* 没有发现死锁. */
return (0);
}

select_victim()返回一个选中需要被回滚的事务,MySQL 并不会迭代所有的 trx 来选择一个代价较小的事务,仅仅在m_startm_wait_lock->trx这两个事务中选一个优先级较低的事务回滚.