准备

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 的入口函数:


/* 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():

/* 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():

/* 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 这两个事务中选一个优先级较低的事务回滚.