症状
MySQL 替你完成了大部分分析——读它就行:SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段打印两个事务各自持有的锁与等待的锁。事务 1 持有 id=3 等 id=7,事务 2 持有 id=7 等 id=3——这就是完整的环。把每条语句映射回代码路径,就得到了必须统一的两套加锁顺序。ERROR 1213 总是发生在少量事务上、并在批量任务或热点行更新重叠时飙升,受害者被完全回滚,这也是重试安全性的来源。
诊断思路
先读死锁报告拿两个等待边,映射到语句与代码路径;再对比两条路径的行键获取顺序——重叠键集上的相反顺序就是死锁;最后 EXPLAIN 涉事语句,type=index 或全扫描意味着锁远超预期范围,与所有并发事务的碰撞窗口都被拉宽。二级索引按不同顺序扫描加间隙锁、check-then-update 的复合操作在交错下颠倒顺序,是两个高频变体。
根因
InnoDB 检测到环:事务 A 持有 B 等待的锁,B 又持有 A 等待的锁,于是回滚一个受害者并报 ERROR 1213。环几乎总能追溯到各代码路径间不一致的获取顺序——不同语句序列以不同顺序锁定同一批行。
修复与验证
全局统一加锁顺序:所有路径先小 id 后大 id(应用侧 sorted(ids),SQL 侧 LEAST/GREATEST);补齐缺失索引让语句只锁目标行;缩短事务;并按错误信息本身的建议加上有界退避的自动重试——受害者已被完全回滚,重试因此安全,但必须在事务边界、以相同输入幂等地进行。验证时在触发死锁的并发负载下断言新增 1213 为零、业务结果不变、此前失败的场景全部完成。
代码示例
mysql> SHOW ENGINE INNODB STATUS\G
--------
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
UPDATE accounts SET balance = balance - 10 WHERE id = 7;
HOLDS THE LOCK(S): record lock on id = 3
WAITING FOR: record lock on id = 7
*** (2) TRANSACTION:
UPDATE accounts SET balance = balance + 10 WHERE id = 3;
HOLDS THE LOCK(S): record lock on id = 7
WAITING FOR: record lock on id = 3
-- every path updates low id first, high id second:
UPDATE accounts ... WHERE id = LEAST(a, b);
UPDATE accounts ... WHERE id = GREATEST(a, b);
ids = sorted([from_id, to_id]) # application-side same rule
for i in ids:
transfer_step(i, delta)
for attempt in range(3):
try:
conn.begin()
transfer(conn, from_id, to_id, amount)
conn.commit()
break
except DeadlockError as e: # 1213: victim rolled back, safe to retry
conn.rollback()
time.sleep(0.05 * 2 ** attempt)