mysql检测到死锁后自动回滚代价较小事务并报错1213,需立即执行show engine innodb status\g定位latest detected deadlock段,比对两事务的sql、holds/waiting锁信息及lock_mode类型,结合innodb_print_all_deadlocks开启全量日志辅助分析,根因多为事务加锁顺序不一致或索引失效导致。

Deadlock found when trying to get lock 不是数据库坏了,而是 InnoDB 已经成功检测并解决了一次死锁——它回滚了其中一个事务,把错误抛给你。真正要做的,不是“救活”那个被回滚的事务,而是搞清为什么这俩事务会互相卡住,然后从代码、SQL、索引、事务边界四个层面切断循环依赖。
怎么看懂 SHOW ENGINE INNODB STATUS 里的死锁现场
执行 SHOW ENGINE INNODB STATUS\G 后,直接搜 LATEST DETECTED DEADLOCK 段落。重点盯三块:
-
TRANSACTION块里两个事务的mysql_thread_id和trx_query—— 确认是哪两条 SQL 在打架 -
*** (1) WAITING FOR THIS LOCK TO BE GRANTED和*** (2) HOLDS THE LOCK(S)—— 看清谁在等什么锁、谁持有什么锁(注意锁类型:RecordLock / Gap Lock / Next-Key Lock) - 末尾那句
WE ROLL BACK TRANSACTION (1)或类似提示 —— 明确哪个事务被选为牺牲者(通常是 undo log 小的那个)
常见陷阱:日志里显示的 SQL 往往是“最后一步”,但死锁根源可能藏在前面没提交的 UPDATE 或 SELECT FOR UPDATE 里;另外,如果 WHERE 条件没走索引,HOLDS THE LOCK(S) 可能列出几十行甚至全表主键范围,这就是间隙锁泛滥的信号。
为什么加了索引还是死锁?查查是不是间隙锁在作祟
在 REPEATABLE READ 隔离级别下,InnoDB 对范围查询(如 WHERE status = 0 AND created_at > '2026-09-01')默认加 Next-Key Lock,既锁记录又锁间隙。两个事务如果都扫描同一片间隙,就极易形成“我锁空地,你也不能插队”的死锁。
- 用
EXPLAIN确认你的查询是否真的命中索引;如果type是index或ALL,说明走了索引扫描或全表扫描,锁范围远超预期 - 检查 WHERE 条件是否包含非唯一字段 + 范围条件组合(比如
status IN (0,1) AND id > 100),这类组合非常容易触发多间隙锁定 - 临时验证:把隔离级别降为
READ COMMITTED(SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED),再压测——如果死锁消失,基本可断定是间隙锁冲突
应用层重试不能只写 try-catch,得带退避和次数控制
捕获错误码 1213 后直接重试,看似简单,但线上容易引发雪崩:多个服务同时重试,流量翻倍,锁竞争更剧烈。
- 必须用指数退避(exponential backoff):第一次重试等待 10ms,第二次 20ms,第三次 40ms……避免重试请求扎堆
- 硬性限制最大重试次数(通常 ≤ 3 次),超过就抛出业务异常,由上游决定降级或告警,而不是无限循环
- 重试前务必
ROLLBACK当前事务,否则下次BEGIN会报错 “Transaction is already active” - 注意:不要在事务内部做重试逻辑,重试必须包裹整个事务生命周期(BEGIN → 执行 → COMMIT/ROLLBACK)
最容易被忽略的点:事务边界比 SQL 本身还关键
很多死锁根本不是两条 UPDATE 冲突,而是事务拖得太长,把锁“捂热了”才引来第二个事务。
- 检查事务里有没有调外部 HTTP 接口、读文件、发 MQ、sleep、手动输入等耗时操作——这些都会让锁持有时间从毫秒级拉长到秒级
- 确认所有事务是否都在同一个数据库连接上执行;有些 ORM(如 Django 的
atomic)默认复用连接,但若中间混入了连接池切换或异步逻辑,事务可能意外跨连接,导致锁状态不可见 - 对批量操作,宁可用多条带主键的单行
UPDATE,也别用一条IN大列表——后者锁范围不可控,且容易触发锁升级
真正难的从来不是看懂死锁日志,而是把一条 SQL 放回整个业务流程里,逆推出它在什么数据分布、什么并发节奏、什么事务上下文中,才会和另一条看似无关的 SQL 碰出火花。这个还原过程,没法靠工具自动完成。











