一、准备工作
死锁检测一般采用Wait-For-Graph算法,本例所述实现为递归式(深度优先搜索),后续改为非递归实现(栈)。
参考资料:http://www.gpfeng.com/?p=426
准备两个session如下:
start transaction start transaction;
update nkeys set c5=0 where c1=50000;
update nkeys set c5=1 where c1=50001;
update nkeys set c5=11 where c1=50001;
update nkeys set c5=10 where c1=50000;
可以看出,session2在执行第二个更新操作时,会发现死锁。实际上,死锁检测发生在加锁请求无法立即满足需要进入所等待时,lock_deadlock_occurs(它进一步调用lock_deadlock_recursive)被两个函数锁调用:lock_rec_enqueue_waiting和lock_table_enqueue_waiting。所以,session1的第二条语句也会触发死锁检测,因为它需要等待,但是此时还没有死锁存在。
二、lock_deadlock_recursive源码分析
InnoDB在实现Wait-For-Graph时基于性能方面的考虑,定义了两个变量:
LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK(默认200):深度优先遍历的层数超过此值,即认为发生死锁,回滚当前事务。LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK(默认1000000):lock_deadlock_recursive递归调用的次数超过此值,即认为发生死锁,回滚当前事务。
几个关键点:
1、该函数中的start表示顶层调用该函数的事务,在递归过程中,如果出现lock_trx == start,则说明发生死锁。(lock_trx为某递归深度时的事务)
2、该函数中的wait_lock是指该递归深度下在等待的锁,lock是在该递归深度下与wait_lock冲突的锁。
3、注意:在加锁时,会把锁放在哈希表链表中的最后位置,这样在遍历这条链表时,会先发现有冲突(锁了同一行)的锁,而不是自己(如果只发现自己,则说明现在还没有死锁。)
// trx-sys中所有事务trx->deadlock_mark在lock_deadlock_occurs中被置为0
if (trx->deadlock_mark == 1) {
/* We have already exhaustively searched the subtree starting from this trx */
return(0);
}
// cost 增加1
*cost = *cost + 1;
// 等待的锁为行锁,找到lock_sys中相同行上的第一个锁
if (lock_get_type_low(wait_lock) == LOCK_REC) {
space = wait_lock->un_member.rec_lock.space;
page_no = wait_lock->un_member.rec_lock.page_no;
lock = lock_rec_get_first_on_page_addr(space, page_no);
/* Position the iterator on the first matching record lock. */
while (lock != NULL
&& lock != wait_lock
&& !lock_rec_get_nth_bit(lock, heap_no)) {
lock = lock_rec_get_next_on_page(lock);
}
// 找到自身,不需要继续找lock_sys中相同行上的下一个锁
if (lock == wait_lock) {
lock = NULL;
}
} else { // 等待的锁为表锁
lock = wait_lock;
}
/* Look at the locks ahead of wait_lock in the lock queue */
for (;;) {
// lock为表锁,定位到table对应表锁链表中lock前一个表锁
/* Get previous table lock. */
if (heap_no == ULINT_UNDEFINED) {
lock = UT_LIST_GET_PREV(un_member.tab_lock.locks, lock);
}
// 没有找到前一个表锁,即lock为最后一个表锁请求,不会造成死锁,返回FALSE
// 或者,lock_sys中找到相同行上第一个锁lock == wait_lock,不会造成死锁,返回FALSE
// 死锁检测时,不需要关心是否与之后加的锁有冲突,Wait-For-Graph节点之间是单向关系
if (lock == NULL) {
/* We can mark this subtree as searched */
trx->deadlock_mark = 1;
return(FALSE);
}
// Wait-For-Graph中找到了一条边,需要检查是否死锁
if (lock_has_to_wait(wait_lock, lock)) {
// 判断是否调用层次太深,或者调用次数过多
ibool too_far
= depth > LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK
|| *cost > LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK;
lock_trx = lock->trx;
// Wait-For-Graph中找到了回路,出现死锁!
if (lock_trx == start) {
// 判断事务权重,若start权重较小,返回LOCK_VICTIM_IS_START,start将被回滚
if (trx_weight_ge(wait_lock->trx, start)) {
return(LOCK_VICTIM_IS_START);
}
// wait_lock->trx权重较小,需要回滚,返回LOCK_VICTIM_IS_OTHER
lock_deadlock_found = TRUE;
wait_lock->trx->was_chosen_as_deadlock_victim = TRUE;
lock_cancel_waiting_and_release(wait_lock);
return(LOCK_VICTIM_IS_OTHER);
}
// 调用层次太深,或者调用次数过多,返回LOCK_EXCEED_MAX_DEPTH
if (too_far) {
return(LOCK_EXCEED_MAX_DEPTH);
}
// 目前还没有出现死锁,继续在Wait-For-Graph寻找可能的边,继续深度搜索
if (lock_trx->que_state == TRX_QUE_LOCK_WAIT) {
ret = lock_deadlock_recursive(start, lock_trx, lock_trx->wait_lock, cost, depth + 1);
// 只有当检测出死锁或者发现调用层次(次数)过深(过多)才返回,否则为Wait-For-Graph选择下一个节点
if (ret != 0) {
return(ret);
}
}
}
// wait_lock与lock没有冲突,继续找lock_sys中相同行上的下一个锁
/* Get the next record lock to check. */
if (heap_no != ULINT_UNDEFINED) {//只对于行锁,而不需要考虑表锁
do {
lock = lock_rec_get_next_on_page(lock);
} while (lock != NULL && lock != wait_lock && !lock_rec_get_nth_bit(lock, heap_no));
// 找到自身,不需要继续找lock_sys中相同行上的下一个锁
if (lock == wait_lock) {
lock = NULL;
}
}
}/* end of the 'for (;;)'-loop */
lock_deadlock_recursive返回值的处理:
1. LOCK_VICTIM_IS_OTHER,发生死锁,需要回滚其它事务,但可能多个事务与当前事务发生死锁,因此需要再次调用lock_deadlock_recursive
2. LOCK_EXCEED_MAX_DEPTH,调用层次(次数)过深(过多),回滚当前事务
3. LOCK_VICTIM_IS_START,发生死锁,需要回滚当前事务