必须立即执行show engine innodb status\g查看latest detected deadlock段落,它是唯一能还原死锁冲突链的原始快照;重点对照(1)(2)事务的holds the lock(s)与waiting for this lock to be granted,结合sql、索引名、lock_mode(如gap before rec)定位加锁顺序冲突与索引失效问题。

Deadlock found when trying to get lock 错误一出现,说明死锁已经发生,但真正要解决的不是“怎么回滚”,而是“为什么两个事务会卡在彼此等待上”。InnoDB 自动选一个事务回滚是默认行为,靠它撑不住高并发场景。
怎么看懂 SHOW ENGINE INNODB STATUS\G 里的死锁现场
别跳过 LATEST DETECTED DEADLOCK 段落——它不是日志摘要,是唯一能还原冲突链的原始证据。
-
*** (1) TRANSACTION和*** (2) TRANSACTION分别对应两个卡住的事务,注意它们的trx_id和MySQL thread id - 每个事务下面的
HOLDS THE LOCK(S)是它已经抢到的锁,WAITING FOR THIS LOCK TO BE GRANTED是它正堵在哪——这两行必须对照看 - 重点比对两段 SQL 的 WHERE 条件和涉及的索引名(比如
index `idx_product_id`),确认是不是同一索引、同一范围、但加锁顺序相反 - 如果看到
lock_mode X locks gap before rec,说明是间隙锁冲突,基本锁定在WHERE用了范围条件(如id > 100)或没走索引
为什么统一更新顺序还踩坑:ORM 自动生成 SQL 的陷阱
你代码里写了先 update A 再 update B,但 MyBatis 或 GORM 可能因参数顺序、动态 SQL 分支、甚至字段别名,生成出相反执行顺序的语句。这不是 bug,是隐式行为。
MySQL 9.6.0是面向Linux平台的2026年创新版本,核心架构迎来重大革新。其将外键约束与级联操作从InnoDB引擎层上移至SQL层,确保所有数据变更均被完整记录至Binlog,彻底解决了CDC(变更数据捕获)与主从复制中的数据不一致难题。此外,该版本引入container_aware启动选项以原生适配容器环境,并对审计日志进行了组件化重构,为追求极致数据一致性与云原生体验的开发者提供了全新选择。
- 用
SELECT * FROM information_schema.INNODB_TRX查活跃事务时,直接看trx_query字段内容,别信代码逻辑 - 开启
general_log或用代理(如 ProxySQL)抓真实发出的 SQL,确认 WHERE 条件字段顺序、IN 列表顺序、ORDER BY 是否一致 - 避免
UPDATE ... WHERE status IN (1,2,3)这类写法——InnoDB 对 IN 列表内部的加锁顺序不保证,不同事务可能按不同路径扫描索引 - 强制加锁顺序:对主键 ID 显式排序,例如
UPDATE t SET x=1 WHERE id IN (3,1,2) ORDER BY id,确保所有事务都升序加锁
索引失效才是死锁真正的放大器
没索引不等于慢,它等于“锁不准”——本该只锁 1 行,结果锁了 5000 行,其他事务一来全排队,死锁概率指数级上升。
- 执行出问题的 SQL 前,先跑
EXPLAIN FORMAT=JSON,盯紧key和type字段:type: ALL或type: index就是红灯 - 常见隐形失效:字段类型不匹配(
WHERE user_id = '123'但 user_id 是 INT)、函数包裹(WHERE DATE(create_time) = '2026-07-01')、联合索引没用最左前缀 - 库存扣减类场景,
UPDATE stock SET qty = qty - 1 WHERE product_id = ? AND qty > 0必须确保product_id有单独索引或作为联合索引最左列;否则qty > 0会让优化器放弃使用索引 - 如果业务允许,把隔离级别从
REPEATABLE READ降到READ COMMITTED,能直接禁掉间隙锁,但得同步检查幻读是否可接受
应用层重试不是补丁,是必须落地的兜底动作
即使你把顺序、索引、事务长度全调优了,网络抖动、瞬时热点、用户重复提交仍会导致死锁。这时候不重试,用户就看到“下单失败”。
- 重试间隔不能固定,用指数退避:
Thread.sleep(50 * (2 ^ retryCount)),避免雪崩式重试冲击数据库 - 只重试错误码为
1213(ER_LOCK_DEADLOCK)的 SQLException,别把主键冲突、超时等也混进来 - 重试次数建议 ≤ 3 次,再失败就抛异常并记录完整上下文(含 traceId、SQL、参数),否则掩盖真实问题
- Spring Boot 用户可以直接用
@Retryable(value = SQLException.class, include = {SQLStateSQLExceptionTranslator.class}, maxAttempts = 3),但务必自定义BackOff
真正难的不是定位哪条 SQL 报错,而是发现两段看似无关的业务代码,因为共享一张表、一个索引、甚至同一个 MySQL 实例的锁管理器,悄悄形成了循环依赖。锁是资源,也是契约——所有事务都得按同一份契约行事,少一行 ORDER BY,少一个索引,少一次重试,都可能让高并发从流畅变成卡顿。










