MySQL死锁日志定位实战:读懂SHOW ENGINE INNODB STATUS

    发布时间:2026-09-26 04:31 更新时间:2026-09-26 04:31 阅读量:0

    线上业务偶尔会冒出一条报错:Deadlock found when trying to get lock; try restarting transaction。很多站长看到这条信息第一反应是加索引、加超时参数,但真正能解决问题的入口,是 MySQL 自己记录下来的死锁现场。这篇文章讲清楚两件事:怎么从 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段落读出两个事务的持锁顺序,以及怎么用 innodb_print_all_deadlocks 把历史死锁全部落进错误日志,方便事后比对业务代码。

    一、先打开死锁日志,让每次死锁都留下证据

    SHOW ENGINE INNODB STATUS 只保留最近一次死锁信息,一旦被下一次死锁覆盖,之前的现场就没了。如果业务是偶发死锁,建议先把 innodb_print_all_deadlocks 打开,让 InnoDB 把每一次死锁都写进 MySQL 错误日志。这个参数是动态的,不需要重启实例。

    -- 查看当前是否开启
    SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';
    
    -- 动态开启(5.7 / 8.0 均支持)
    SET GLOBAL innodb_print_all_deadlocks = ON;
    
    -- 想持久化,写进配置文件 my.cnf 的 [mysqld] 段
    -- innodb_print_all_deadlocks = ON

    打开之后,死锁信息会出现在 error log 里。日志路径可以用 SELECT @@log_error; 查,也可以用 SHOW VARIABLES LIKE 'log_error'; 看。拿到路径后用 grep 过滤关键字即可:

    # 先确认错误日志位置
    mysql -uroot -p -e "SELECT @@log_error;"
    
    

    把最近的死锁段落抓出来看

    grep -n "LATEST DETECTED DEADLOCK" /var/log/mysql/error.log

    也可以直接看最近 200 行里带死锁的部分

    tail -n 2000 /var/log/mysql/error.log | grep -A 60 "TRANSACTION"

    注意一点:开启之后每次死锁都会写日志,如果业务本身冲突非常频繁,日志量会明显增加,磁盘空间要留够。参数含义以官方文档为准,不同小版本输出格式可能略有差异。

    二、读懂死锁段落:谁持有什么锁,谁在等谁

    SHOW ENGINE INNODB STATUS 的输出里,找到 LATEST DETECTED DEADLOCK 这一段,它按时间顺序记录了两个(或多个)事务的现场。阅读顺序建议从下往上:先看被回滚的那个事务,再看另一个事务,最后回到最上面的 WAITING / HOLDING 列表。

    段落里通常包含这些关键行:TRANSACTION 开头标明事务 id 和状态;WAITING FOR ... LOCK 表示这个事务正在等锁;HOLDING ... LOCK 表示它已经持有的锁;LOCK WAIT ... RECORD LOCKS 后面会跟具体的索引名、表名和锁模式。锁模式里的 X 表示排他锁,S 表示共享锁,LOCK_MODE 里的 GAP 表示间隙锁,REC_NOT_GAP 表示记录锁,这些都直接决定两个事务为什么互相卡住。

    举一个典型形态(示意,实际字段以你的环境输出为准):事务 A 先对主键 id=10 更新,持有该记录 X 锁,随后想更新 id=20;事务 B 先更新 id=20 持有 X 锁,再想更新 id=10。两个事务的加锁顺序正好相反,形成环,InnoDB 检测到环路后回滚其中一个。日志里会看到 A 的 HOLDING 是 id=10、WAITING 是 id=20,B 的 HOLDING 是 id=20、WAITING 是 id=10。把这两组对应起来,持锁顺序就清楚了。

    还有一类更隐蔽的情况:两个事务更新的是不同行,但走的是同一张表的二级索引,或者一个走主键、一个走二级索引,最终锁到了同一批记录上,也会形成死锁。这时要重点看 LOCK WAIT 后面的 index 名称,判断业务 SQL 是否命中了同一索引。

    三、从日志回到代码:调整访问顺序与事务边界

    看懂日志只是第一步,真正减少死锁要从业务侧动手。最常见的原因是不同接口更新同一批数据时顺序不一致。比如订单扣库存和退款回补库存,一个先更库存再更订单,一个先更订单再更库存,高并发下就容易撞上。统一成固定顺序,比如都按「先订单、后库存」处理,死锁概率会明显下降。

    第二个动作是缩短事务、减少锁持有时间。把外部调用、文件读写、消息发送这类耗时操作放到事务外,事务里只保留必要的 SQL。同时确认更新语句是否走了索引:如果 UPDATE 的 WHERE 条件没有索引,InnoDB 可能扫到大量记录并加锁,锁范围被放大,冲突自然变多。可以用 EXPLAIN 检查更新条件对应的执行计划。

    -- 看更新语句是否命中索引
    EXPLAIN UPDATE orders SET status = 'paid' WHERE user_id = 1001 AND order_no = 'A0001';
    
    -- 事务尽量短:先查、再算、最后只做必要的写
    START TRANSACTION;
    UPDATE inventory SET stock = stock - 1 WHERE sku_id = 88 AND stock > 0;
    UPDATE orders SET status = 'paid' WHERE order_no = 'A0001';
    COMMIT;

    如果某些业务确实无法避免高冲突,可以考虑在应用层捕获死锁异常后重试,但重试要有次数上限和退避,不能无限循环。重试只能缓解报错,不能消除根因,访问顺序和事务边界才是关键。另外,把 innodb_lock_wait_timeout 调小可以让等锁更快失败,从而更早暴露问题,但设置过小也可能让正常业务失败,需要结合业务容忍度调整,具体以实际环境压测结果为准。

    最后给一个排查清单:先确认 innodb_print_all_deadlocks 已开启并找到错误日志;再从 LATEST DETECTED DEADLOCK 段落里对照两个事务的 HOLDING 与 WAITING,画出持锁顺序;然后回到业务代码统一更新顺序、缩短事务、确认更新条件走索引;上线后继续观察错误日志,看死锁是否真的变少。死锁本身是 InnoDB 的一种自我保护机制,不必恐慌,关键是让每一次死锁都变成一次可复盘的线索,而不是反复出现的噪声。

    继续阅读

    📑 📅
    Nginx 上游节点健康检查实战:max_fails、fail_timeout 与 backup 配置 2026-09-26
    MySQL主从复制中断后如何安全恢复 2026-09-25
    宿主机与容器时间不一致导致接口签名失败排查 2026-09-25
    Nginx与上游服务时间不同步导致签名校验失败排查 2026-09-25
    内核日志里的 OOM 线索:oom_score、cgroup 限制与判断内存不足原因 2026-09-25
    Linux服务器内存泄漏初判:RSS上涨是缓存还是泄漏 2026-09-26
    Docker 挂载 NFS 卷卡死排查:stat、df 与 nfsstat 定位失联 2026-09-26
    PostgreSQL WAL 堆积与复制槽不释放排查实战 2026-09-26
    Nginx proxy_pass 带 URI 与不带 URI:路径拼接错乱排查 2026-09-27
    Docker 容器 PID 1 与信号处理:docker stop 超时强制 kill 的成因与写法 2026-09-27