MySQL 死锁排查实战:从 SHOW ENGINE INNODB STATUS 到代码修复

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-15 06:41 ·12 浏览 ·0 回复

死锁排查的正确顺序是:先用 `SHOW ENGINE INNODB STATUS`(或打开 `innodb_print_all_deadlocks` 留全量日志)抓到现场,再读日志里两个事务的「持有锁 / 等待锁」确认是 AB-BA 的加锁顺序冲突,最后回到代码层做三件事——统一加锁顺序、把慢操作移出事务、对 1213 错误做退避重试。只改数据库参数不改代码,死锁基本会反复出现。

第一步:先抓现场,别急着重启

结论:`SHOW ENGINE INNODB STATUS` 只保留最近一次死锁,要做常态化排查必须打开全量记录。

在 MySQL 客户端执行:

SHOW ENGINE INNODB STATUS\G

结果里找 `LATEST DETECTED DEADLOCK` 段落。它只存最后一次,一旦被新死锁覆盖就没了。所以生产库建议开启:

SET GLOBAL innodb_print_all_deadlocks = ON;  -- MySQL 5.6.2+,重启后需写入配置文件

打开后每次死锁都会写进 error log,配合日志切割可以回溯「哪张表、哪个时段、哪个接口」最容易出问题。

如果死锁还在持续发生、想实时看谁在等谁,MySQL 8.0 查 `performance_schema.data_locks`(当前持有的锁)和 `data_lock_waits`(等待关系);5.7 及以下版本用 `information_schema.innodb_locks` 和 `innodb_lock_waits`。`information_schema.innodb_trx` 则用来揪出跑了很久的长事务。

第二步:读懂死锁日志的三个关键点

结论:死锁日志的核心信息只有两组——每个事务「HOLDS THE LOCK(S)」(已持有)和「WAITING FOR THIS LOCK TO BE GRANTED」(正在等),把两组对照就能还原出循环等待。

一条典型的日志(已简化):

*** (1) TRANSACTION:
UPDATE accounts SET balance = balance - 10 WHERE id = 2
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS ... index PRIMARY ... lock_mode X locks rec but not gap
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS ... index PRIMARY ... lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
UPDATE accounts SET balance = balance + 10 WHERE id = 1
...
*** WE ROLL BACK TRANSACTION (1)

读法:事务 1 持有 id=1 的行锁、在等 id=2;事务 2 持有 id=2、在等 id=1。典型的加锁顺序相反。注意最后一行 `WE ROLL BACK TRANSACTION (1)`——InnoDB 默认开启 `innodb_deadlock_detect`,会自动挑「代价较小」的那个事务回滚,所以业务代码必须处理这个异常,不能假设事务一定成功。

日志里还要看锁模式:`lock_mode X` 是排他行锁,带 `gap` 字样说明涉及间隙锁(在可重复读 RR 隔离级别下范围条件会锁间隙),不带 gap 的就是纯记录锁。间隙锁引发的死锁靠调 SQL 顺序往往解决不了。

第三步:四种高频死锁模式对照

结论:90% 的业务死锁都能归到下面四类,先对号入座再动手改。

  1. 批量操作顺序不一致:`UPDATE t SET x=1 WHERE id IN (1,2,3)` 和 `WHERE id IN (3,2,1)` 并发,加锁顺序不同就会互等。修复:应用层对 ID 排序后再逐条执行。
  2. 走不同索引导致锁定集合不同:同一行数据,一个事务走主键、另一个走二级索引需要回表,实际锁定范围不一致,容易与间隙锁叠加成死锁。
  3. 并发插入相同唯一键:`INSERT ... ON DUPLICATE KEY UPDATE` 或唯一索引冲突,会用加锁的方式做去重判断,高并发下互相等待。
  4. 先查后改(乐观写法):`SELECT` 判断「不存在才插入」,没有实际加锁语义,并发时两个请求都以为能插。

第四步:代码层修复方案

结论:修复优先级是「统一加锁顺序 > 缩短事务 > 降隔离级别 > 加重试兜底」,只有最后一项是权宜之计。

统一加锁顺序是最有效的一招。凡是涉及多行多表的事务,一律按固定顺序访问,比如按主键 ID 升序、按表名字典序。转账类业务把人按 ID 排序后再依次扣加,死锁直接消失。

缩短事务:把 HTTP 请求、消息推送、文件读写、日志落库全部挪到 `commit()` 之后。事务越长,持锁时间越长,冲突概率是线性上升的。

降隔离级别:`SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED` 可以大幅减少间隙锁,但会让同一事务内两次读结果可能不同。是否需要,取决于业务能不能接受不可重复读。

退避重试是最后一道闸门。死锁无法 100% 消除,捕获 1213 错误码重试是工程标配:

$retry = 3;
while (true) {
    try {
        $pdo->beginTransaction();
        // ... 业务 SQL
        $pdo->commit();
        break;
    } catch (PDOException $e) {
        $pdo->rollBack();
        // 1213 = 死锁;1205 是锁等待超时,不要混着重试
        if (($e->errorInfo[1] ?? 0) === 1213 && $retry-- > 0) {
            usleep(random_int(50000, 200000));
            continue;
        }
        throw $e;
    }
}

重试次数建议 3 次、间隔加随机抖动,避免多个请求同步重试造成新一轮冲突。

第五步:验证与常态化监控

结论:改完必须复现验证,并把死锁计数纳入告警,否则等于没改。

开两个会话手动模拟是最快的验证方式:会话 A 先 `BEGIN` 更新 id=1,会话 B `BEGIN` 更新 id=2,再让两边交叉更新,看是否仍报 1213。改完后同样的操作应该变成「阻塞等待」而非死锁。

监控方面,对 error log 里的 `Deadlock found when trying to get lock` 做关键字计数,设个阈值告警。本站在环境要求上只要求 MySQL 5.7+,所以 `innodb_print_all_deadlocks` 和 `performance_schema` 相关表都能正常使用,不需要额外升级。

一句话收束:死锁排查是「日志定位 → 模式归类 → 代码改顺序和事务边界 → 重试兜底 → 监控回归」的闭环,抓现场用 `SHOW ENGINE INNODB STATUS`,防复发靠代码,不靠调参。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-360.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~