应立即执行SHOW ENGINE INNODB STATUS\G抓取LATEST DETECTED DEADLOCK快照,重点比对(1)(2)事务的HOLDS THE LOCK(S)与WAITING FOR THIS LOCK TO BE GRANTED,结合index名、heap no及lock_mode定位冲突索引与记录。

死锁不是数据库故障,而是并发逻辑缺陷的明确信号——只要加锁顺序不一致、索引失效或间隙锁失控,它就一定会复现,且频率随流量上升而指数增长。
怎么看最近一次死锁详情(SHOW ENGINE INNODB STATUS\G)
这是最直接的入口,但多数人只扫一眼 LATEST DETECTED DEADLOCK 区块就放弃。真正关键的信息藏在结构里:
- 先定位被回滚的事务(含
ROLLING BACK的那条),它通常是持有锁最少的那个,不代表它是“问题方”; - 对比两个事务的
mysql thread id和trx_id,再关联information_schema.INNODB_TRX查它们的启动时间、SQL 和状态; - 重点看每条 SQL 后面的
lock_mode X locks rec but not gap或lock_mode X locks gap before rec:前者是精准行锁,后者是间隙锁——后者才是交叉冲突高发区; - 查
lock struct(s)下的lock_trx_id和lock_rec_lock,能确认锁住的具体索引和记录值,比如PRIMARY, 123或idx_name, 'Eason'。
怎么查当前锁等待链(sys.innodb_lock_waits)
当死锁刚卡住几秒还没被检测到,或想确认是否正在形成循环等待时,这个视图比手写 JOIN 更可靠:
-
waiting_pid和blocking_pid是线程 ID,可立刻用KILL终止阻塞源(慎用,优先查清业务逻辑); -
waiting_query显示卡住的 SQL,90% 含FOR UPDATE或UPDATE ... WHERE; -
blocking_query常为 NULL —— 这很常见,元凶事务可能早已执行完上一条语句,正开着事务空等;此时要看blocking_trx_started,就知道它持锁多久了; - 返回空 ≠ 没锁,可能只是没形成显式等待链,需进一步查
performance_schema.data_locks。
怎么验证是不是索引失效导致锁扩大(EXPLAIN + data_locks)
90% 的死锁根源不在并发本身,而在一条本该走索引却全表扫描的 SQL。它会让 UPDATE 锁住成百上千行,大幅提高冲突概率:
- 对报错 SQL 执行
EXPLAIN,重点看type是否为ALL或index、key是否为NULL、rows是否远超预期; - 再查
performance_schema.data_locks,过滤LOCK_TYPE = 'RECORD'且LOCK_DATA大量重复或跨度极大(如从 1 到 5000); - 常见诱因:
WHERE条件用了函数(DATE(create_time))、隐式类型转换(phone = 13800138000)、OR中混入非索引列、联表时驱动表选错。
为什么统一加锁顺序比调优 SQL 更治本
很多团队花大量时间优化单条 SQL,却忽略一个事实:只要两个事务以不同顺序更新同一组主键,哪怕都走了索引、都只锁一行,死锁仍会稳定复现:
- 事务 A:先
UPDATE orders SET status=2 WHERE id=1001,再UPDATE users SET balance=balance-100 WHERE id=101; - 事务 B:先
UPDATE users SET balance=balance+100 WHERE id=101,再UPDATE orders SET status=3 WHERE id=1001; - 这种交错加锁天然构成循环等待,InnoDB 必然回滚其一;
- 解决方式不是“避免并发”,而是强制所有业务路径按固定顺序操作资源:比如约定「订单 → 用户 → 库存」为全局唯一顺序,ORM 层或服务编排层做校验。
最易被忽略的一点:死锁日志里出现的 SQL,往往不是你写的那条,而是 ORM 自动生成的隐式语句(比如 GORM 的 Save() 或 MyBatis 的 selectByPrimaryKey),它们可能在事务开头就悄悄加了锁。排查时务必把整个事务生命周期拉出来看,而不是只盯报错那一行。


















