MySQL死锁如何定位和解决

作者:袖梨 2026-08-14

SHOW ENGINE INNODB STATUS可实时获取最近一次死锁快照,重点查看LATEST DETECTED DEADLOCK段中的两个事务SQL、WAITING FOR THIS LOCK和HOLDS THE LOCK(S)信息;需配合innodb_print_all_deadlocks=ON持久化全量死锁日志,并用INFORMATION_SCHEMA.INNODB_LOCK_WAITS查实时锁等待链,应用层须捕获错误码1213并幂等重试。

MySQL死锁无法避免,但能快速定位、精准归因、稳定恢复——关键不是“怎么不发生”,而是“怎么秒级抓到现场并让应用不崩”。

SHOW ENGINE INNODB STATUS 能看到什么?

这是最直接、最可靠的实时死锁快照,不需要日志轮转或配置重启,执行即得最近一次死锁详情。

重点看输出中 LATEST DETECTED DEADLOCK 区块,它包含:

  1. 两个事务各自的 TRANSACTION ID 和完整 SQL(比如 UPDATE orders SET status='shipped' WHERE order_id=1001
  2. 谁在等哪条记录锁:WAITING FOR THIS LOCK TO BE GRANTED 行明确指出等待的索引、页号、主键值
  3. 谁持有了对方要的锁:HOLDS THE LOCK(S) 对应的 RECORD LOCKS 描述了锁定范围
  4. 最终被回滚的是哪个事务(WE ROLL BACK TRANSACTION (1)

注意:该命令只返回最近一次死锁。如果频繁发生,仅靠它会漏掉中间发生的多次死锁。

如何让每次死锁都留下痕迹?

innodb_print_all_deadlocks = ON 把所有死锁写进错误日志,这是生产环境必须开的开关。

操作分两步:

  1. 临时开启(立即生效,重启失效):SET GLOBAL innodb_print_all_deadlocks = 1;
  2. 永久生效:在 my.cnf[mysqld] 段落添加:innodb_print_all_deadlocks = 1,然后重启 MySQL

开启后,每次死锁都会在 log_error 指定路径(如 /var/log/mysql/error.log)追加一段完整上下文,包括时间戳、线程ID、SQL、锁结构——比 SHOW ENGINE INNODB STATUS 更全、更可追溯。

风险提示:高并发下死锁频发时,日志量可能陡增,需配合日志轮转策略,但绝不应因此关闭该选项。

查当前锁等待关系用哪张表?

当死锁正在发生(还没被检测到或刚发生),或者你想确认是否存在隐性阻塞链时,用 information_schema.INNODB_LOCK_WAITS 关联查询最有效。

标准查询模板:

SELECT r.trx_id AS waiting_trx_id, r.trx_mysql_thread_id AS waiting_thread, r.trx_query AS waiting_query, b.trx_id AS blocking_trx_id, b.trx_mysql_thread_id AS blocking_thread, b.trx_query AS blocking_queryFROM information_schema.INNODB_LOCK_WAITS wJOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_idJOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;

结果直接告诉你:哪个线程(waiting_thread)卡在哪条 SQL(waiting_query),被哪个线程(blocking_thread)的哪条 SQL(blocking_query)挡住。

这个查询不依赖死锁是否已触发,只要存在锁等待就可查出,是排查“疑似死锁卡顿”的第一手证据。

应用层该怎么应对死锁错误?

MySQL 返回的错误码是 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction,应用必须捕获并重试,不能当作普通异常抛出。

重试逻辑要点:

  1. 只重试幂等写操作(如 UPDATEINSERT ... ON DUPLICATE KEY UPDATE),非幂等操作(如无条件 INSERT)重试可能引发重复数据
  2. 指数退避:第一次重试延迟 100ms,第二次 200ms,第三次 400ms,避免重试风暴
  3. 设上限:最多重试 3 次,超限则抛出业务异常,交由人工介入

Python 示例中捕获 e.errno == 1213 就是标准做法;Java 用 SQLException.getSQLState().equals("40001") 判断。

真正容易被忽略的是:重试必须在同一个数据库连接内完成,否则新连接可能拿到旧事务未提交的脏状态,导致逻辑错乱。

相关文章

精彩推荐