必须逐行读LATEST DETECTED DEADLOCK模块,它是死锁现场快照而非日志;重点比对两个事务块中HOLDS THE LOCK(S)与WAITING FOR THIS LOCK TO BE GRANTED的交叉关系,结合末尾SQL、索引名及lock_mode还原加锁顺序。

怎么看懂 SHOW ENGINE INNODB STATUS\G 里的事务块
死锁日志不是日志,是现场快照;LATEST DETECTED DEADLOCK 模块必须逐行读,不能跳。重点不是“谁被回滚”,而是两个事务之间“谁先锁了什么、又在等什么”。每个事务块以 TRANSACTION xxx 开头,后面紧跟着三类关键行:HOLDS THE LOCK(S)、WAITING FOR THIS LOCK、末尾的 SQL 原文。
常见误读:把 WAITING FOR 当成孤立等待,其实它一定对应另一个事务的 HOLDS THE LOCK(S)。比如事务 A 等 index idx_user_id,那事务 B 的 HOLDS 行里大概率就含这个索引——要手动比对,不能靠猜。
实操建议:
- 用文本编辑器把两段事务块分开,左右对照着看,标出锁的
space id和page no,相同值 = 同一数据页,基本可确认冲突点 - 忽略
heap no和物理记录细节,除非你真在查页级损坏;关注index名和lock_mode X/S类型即可 - SQL 原文必须还原到业务上下文:比如
UPDATE order SET status='paid' WHERE id=1001是支付成功,而UPDATE account SET balance=balance-100 WHERE user_id=2002是扣款,两者顺序反了就是典型交叉更新
WAITING FOR 显示 GEN_CLUST_INDEX 怎么办
这是索引失效的强信号。GEN_CLUST_INDEX 不是真实索引名,是 InnoDB 在找不到有效二级索引时,退化为聚簇索引扫描(即主键)的标记。意味着你的 WHERE 条件没走索引,InnoDB 被迫锁住扫描路径上的所有行,锁范围远超预期。
典型场景包括:
-
WHERE字段没建索引,或建了但类型不匹配(如user_id是VARCHAR,查询却传整数) - 使用了函数或表达式:
WHERE DATE(create_time) = '2026-08-12',导致索引失效 - 隐式类型转换:
WHERE mobile = 13800138000(mobile 是字符串类型) - 范围查询过大:
WHERE amount BETWEEN 1 AND 1000000,触发大量间隙锁
验证方式:对报错 SQL 执行 EXPLAIN,看 key 列是否为 NULL 或 possible_keys 为空。
为什么两个事务都锁了同一张表的同一索引还死锁
因为锁的不是“同一个值”,而是“不同行 + 不同锁模式 + 间隙重叠”。尤其在 REPEATABLE READ 隔离级别下,WHERE 带范围条件(如 WHERE status='unpaid' AND amount > 50)会触发 Next-Key Lock,即记录锁 + 间隙锁组合。事务 A 锁了 (50, 100],事务 B 锁了 (80, 150],两者间隙重叠,插入新记录时就可能互相阻塞。
更隐蔽的是“唯一索引的插入意向锁”冲突:两个事务同时 INSERT 同一个唯一键(如重复 order_no),都会先加插入意向锁,再尝试加唯一约束检查锁,此时若检查失败回滚不及时,就可能卡在锁等待链里。
关键判断点:
- 看
lock_mode是否含gap或next-key—— 出现在范围查询、SELECT ... FOR UPDATE或唯一键冲突场景 - 查
information_schema.INNODB_TRX中的trx_isolation_level,确认是不是 RR 级别放大了锁粒度 - 避免在 RR 下对非精确条件加锁;改用
READ COMMITTED可禁用间隙锁(但需评估幻读风险)
如何从日志反推代码层加锁顺序
日志本身不记录调用栈,但 SQL 原文 + 表名 + 索引名 + 业务语义能倒推出逻辑路径。比如事务 A 的 SQL 是 UPDATE stock SET qty = qty - 1 WHERE sku='SKU001',事务 B 是 UPDATE order SET status='shipped' WHERE order_id='ORD001',而两者又都涉及 stock_log 表的 INSERT,说明业务流程是“扣库存 → 更新订单 → 记日志”。如果另一处日志显示相反顺序(先记日志再扣库存),就是代码层加锁顺序不一致。
真正容易被忽略的点:
- ORM 自动生成 SQL 的 where 条件顺序可能随机(如 MyBatis 的
<where>标签拼接),导致同一业务在不同请求中加锁顺序不一致 - 异步任务与同步接口共用同一套 DAO,但事务边界不同(如 @Transactional 注解漏加或传播行为设为 SUPPORTS)
- 缓存穿透场景下,未命中缓存后直接查 DB 加锁,而缓存命中的请求跳过锁,造成“有锁/无锁”混跑,表面看不是死锁,实则埋下锁竞争隐患
复杂点永远在“多层抽象叠加”:SQL 日志只暴露最后一公里,但锁顺序问题往往藏在 service 层的调用链、分布式事务的本地事务嵌套、甚至跨库操作的补偿逻辑里。盯着日志读是对的,但不能只读日志。


















