MySQL死锁报错1213是InnoDB检测到循环等待后主动回滚代价小的事务,应用层须捕获该错误、在新事务中按指数退避重试,并通过固定锁顺序、索引优化和缩短事务来降低被选为牺牲者的概率。
死锁报错(错误码 1213)不是数据库卡住,而是 mysql 主动终止了其中一个事务——它已经识别出循环等待,必须砍断才能继续。关键不是“避免被砍”,而是让自己的事务更不容易成为那个被砍的。
为什么 UPDATE 会触发死锁检测?
死锁本质是两个及以上事务互相持有对方需要的锁,又在等对方释放。UPDATE 尤其容易中招,因为它默认加行级写锁(X lock),且常伴随隐式事务。常见组合有:
- 事务 A 先更新
id = 1,再试图更新id = 2;事务 B 反过来先更新id = 2,再更新id = 1 - UPDATE 语句没走索引,导致扫描全表或大范围间隙,锁住大量行(甚至升级为间隙锁或 next-key 锁)
- 事务中混用 SELECT ... FOR UPDATE 和 UPDATE,顺序不一致时极易形成等待链
- 应用层手动开启事务但未及时提交,让锁长时间悬着
如何快速定位死锁根源?
别只看 phpMyAdmin 报的 “Deadlock found when trying to get lock”,要立刻查 InnoDB 的实时快照:
在 phpMyAdmin 的 SQL 窗口执行:SHOW ENGINE INNODB STATUS;
滚动到输出末尾的 LATEST DETECTED DEADLOCK 区块。这里会明确写出:
立即学习“PHP免费学习笔记(深入)”;
- 哪个事务(
trx id)被回滚了 - 两个事务各自持有的锁(
HOLDS THE LOCK(S)) - 各自在等什么锁(
WAITING FOR THIS LOCK TO BE GRANTED) - 它们正在执行的具体 SQL(
query id和UPDATE ...语句)
重点比对两者的 WHERE 条件和访问顺序——这直接暴露了资源争用路径。
怎么写 UPDATE 才不容易被选为牺牲品?
MySQL 回滚的是“undo log 最小、代价最低”的那个事务。所以你的 UPDATE 要尽量轻量、确定、快:
- 确保 WHERE 条件命中**唯一索引或主键**,避免全表扫描和间隙锁膨胀
- UPDATE 前先用
EXPLAIN检查执行计划,确认type是const或eq_ref,而不是ALL或range - 批量 UPDATE 分拆成小批次(比如每次 100 行),用 LIMIT 控制,避免单条语句锁太久
- 如果业务允许,把隔离级别从默认的
REPEATABLE READ临时降为READ COMMITTED(需确认无幻读风险) - 绝对不要在事务里穿插无关查询,尤其是未加锁的 SELECT —— 它可能干扰锁的获取顺序
应用层捕获并重试的硬性要求
如果 UPDATE 是由后端代码触发的(比如 PHP 调用 PDO),不能靠 phpMyAdmin 点重试。必须在代码里显式处理错误码 1213:
必须满足三个条件,重试才安全:
- PDO 必须开启异常模式:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - 每次重试前,必须重新调用
$pdo->beginTransaction(),不能复用已中断的事务上下文 - 重试间隔建议用指数退避(如第一次
usleep(50000),第二次usleep(100000)),而非固定 sleep(1)
漏掉任何一条,都可能导致数据不一致或无限重试失败。
死锁本身不可完全杜绝,但高频发生一定意味着访问模式或索引设计存在系统性问题。与其反复重试,不如花十分钟看一眼 SHOW ENGINE INNODB STATUS 里的死锁现场——那里面写的,比任何文档都准。



















