mysql死锁无法完全避免,关键在优化事务设计:缩短锁持有时间、统一加锁顺序、确保sql走索引、拆分批量操作、按主键升序查询、禁用动态sql,并合理配置超时与重试机制。

MySQL死锁不是配置调大就能消失的问题,InnoDB检测到循环等待后必然回滚一个事务,Deadlock found when trying to get lock 错误本身是系统在“自救”,关键是要让被回滚的不是你业务里那个该成功的事务。
怎么看懂 SHOW ENGINE INNODB STATUS 的死锁快照
执行 SHOW ENGINE INNODB STATUS\G 后,直接跳到 LATEST DETECTED DEADLOCK 区域——这里不是日志归档,而是最近一次死锁的“现场录像”。重点盯三块:
-
TRANSACTIONID 和对应线程 ID(mysql thread id),用来关联应用日志里的请求 trace -
HOLDS THE LOCK(S):这个事务当前锁住了哪些索引、哪几行(注意lock_mode X locks rec but not gap表示纯行锁,gap出现说明有范围查询) -
WAITING FOR THIS LOCK TO BE GRANTED:它卡在哪条 SQL、哪个索引上,和另一个事务的HOLDS是否形成交叉
别只看 SQL 文本,要结合表结构和索引定义判断:比如 WHERE status = 'pending' AND created_at > '2026-06-01' 如果 status 列没索引,InnoDB 就得扫全表加锁,死锁概率陡增。
为什么统一更新顺序能解决 70% 的死锁
两个事务分别执行 UPDATE order SET ... WHERE id=1 和 UPDATE stock SET ... WHERE product_id=100,只要顺序不一致(A 先 order 后 stock,B 先 stock 后 order),就可能卡住。这不是运气问题,是 InnoDB 加锁顺序严格按 SQL 执行顺序来。
- 所有涉及订单+库存的事务,必须固定为:先
UPDATE order,再UPDATE stock(或反过来,但必须全局一致) - 如果业务逻辑天然无法控制(比如异步消息触发不同入口),就在代码层用
SELECT ... FOR UPDATE显式按主键升序锁定关联记录,例如:SELECT id FROM order WHERE id IN (1,2,3) ORDER BY id FOR UPDATE - ORM 自动生成 SQL 时容易乱序(如 MyBatis 动态 SQL 中
if分支导致 WHERE 条件排列不同),建议对关键事务禁用动态拼接,改用预编译语句
间隙锁(Gap Lock)是怎么悄悄引发死锁的
在默认的 REPEATABLE READ 隔离级别下,WHERE price BETWEEN 100 AND 500 这类范围条件不仅锁住匹配的行,还会锁住 100–500 之间的“空隙”。两个事务同时执行类似操作,极易因间隙重叠而互相等待。
- 确认是否真需要 RR 级别:如果业务能容忍幻读,把隔离级别降到
READ COMMITTED,InnoDB 会自动禁用间隙锁 - 避免在高并发扣减场景用范围条件更新,比如库存扣减不要写
WHERE quantity >= 1,而应先SELECT quantity再用主键精确更新:UPDATE stock SET quantity = quantity - 1 WHERE id = ? AND quantity >= 1 - 检查索引覆盖性:间隙锁只作用于索引扫描路径,如果
price列没有索引,InnoDB 会升级为全表间隙锁,后果更严重
应用层重试不能代替根本修复
捕获 Deadlock found 异常后自动重试,是快速兜底手段,但掩盖了真正瓶颈。高频重试会导致:
- 相同事务反复失败,用户感知为“下单总是失败”
- 数据库 CPU 持续飙升(死锁检测本身有开销)
- 掩盖了索引缺失、长事务等深层问题
重试只应在明确知道冲突短暂且可预期时使用(例如秒杀场景前 100ms),并限制次数(最多 2 次)和退避时间(指数级延迟)。真正的稳定,来自锁范围最小化、事务边界清晰、以及每个 UPDATE 都落在有效索引上——这些细节,比任何重试逻辑都重要。











