死锁导致业务超时的典型现象是应用层报“sql执行超时”但实际为error 1213被客户端转义;关键判断点是多请求几乎同时失败且sql耗时极短(如2ms)而接口卡在30s边界,此时应立即执行show engine innodb status\g查看latest detected deadlock区块,定位被回滚事务及锁冲突详情。

死锁直接导致业务超时的典型现象
应用层报错 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction,但业务日志里只看到“SQL执行超时”,没看到明确死锁提示——这是因为客户端库(如 JDBC、PyMySQL)默认把 1213 错误转成了通用超时异常。真实原因不是慢,而是事务被 InnoDB 主动回滚了。
关键判断点:同一时间多个请求几乎同时失败,且失败堆栈里 SQL 执行耗时极短(比如 2ms),但整体接口响应却卡在 30s 边界。这时要立刻查死锁日志,而不是优化 SQL 或加索引。
快速定位最近一次死锁的完整命令
执行 SHOW ENGINE INNODB STATUS\G,重点看 LATEST DETECTED DEADLOCK 区块。这个输出是 MySQL 自动保留的最后一次死锁快照,不需要额外配置,但只保留最近一次。
- 它会显示两个(或多个)事务各自的
TRANSACTIONID、mysql_thread_id、开始时间、隔离级别 - 列出每个事务持有的锁(
HOLDS THE LOCK(S))和等待的锁(WAITING FOR THIS LOCK TO BE GRANTED) - 最关键的是末尾的
*** WE ROLL BACK TRANSACTION (1)—— 明确告诉你哪个事务被牺牲了
注意:SHOW ENGINE INNODB STATUS 输出中锁位置常带 gap before rec 或 insert intention,这说明即使在 READ COMMITTED 隔离级别下,唯一键/外键检查仍会触发间隙锁,不能盲目认为 RC 就没 gap lock。
从死锁日志反推业务代码问题
死锁日志里每条 SQL 后都附带了加锁类型(S/X)、索引名(如 idx_user_id)、具体索引值(如 0x78657274)。比对这些信息,能直接定位到代码里哪几行并发逻辑冲突:
- 如果两个事务都在等同一个二级索引上的 X 锁,但持有不同主键的 X 锁 → 很可能是批量更新 + 单行更新顺序不一致(例如 A 先改 user_id=100 再改 user_id=200,B 反过来)
- 如果出现
insert intention和gap before rec交叉 → 检查是否在高并发插入场景下,没用INSERT ... ON DUPLICATE KEY UPDATE而是先SELECT再INSERT,导致间隙锁竞争 - 如果锁落在
PRIMARY上但 SQL 是WHERE name = ?→ 说明name列没索引,InnoDB 退化为全表扫描加锁,必须补索引
别依赖 ORM 日志里的 SQL 字符串顺序——MyBatis/GORM 生成的 where 条件顺序可能每次不同,而 InnoDB 加锁顺序严格按索引物理顺序走。
避免死锁扩大影响的应急操作
发现死锁高频发生时,光改代码来不及,需立即降低影响面:
- 用
SELECT * FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(NOW()) - TIME_TO_SEC(TRX_STARTED) > 60查出运行超 1 分钟的长事务,人工KILL对应的TRX_MYSQL_THREAD_ID - 临时把关键表隔离级别调成
READ COMMITTED(SET GLOBAL tx_isolation='READ-COMMITTED'),可关闭大部分间隙锁,但要注意 binlog 格式必须是ROW,否则主从不一致 - 禁止在事务里做 HTTP 调用、文件读写、sleep 等非数据库操作——这类长事务是死锁的放大器,不是根源,但会让问题更难收敛
真正难处理的是那些锁范围看似合理、但因索引失效或隐式类型转换导致锁扩大到多行的 case。这种必须结合 EXPLAIN FORMAT=JSON 看 key_locks 和 rows_examined,不能只信死锁日志里的单行描述。











