bigywater.tech
MySQL / InnoDB · 17

InnoDB 死锁:两个事务互相等会发生什么

制造一个经典死锁:A锁行1想要行2,B锁行2想要行1,用SHOW ENGINE INNODB STATUS拿到InnoDB自己记录的死锁检测报告,看它选谁做牺牲品。

1.2毫秒
死锁形成后InnoDB检测并报错的速度
Error 1213
的Deadlock found错误码
回滚代价更小方
InnoDB选择牺牲品的依据

做的事:制造一个经典的循环等待

A先锁行1,B先锁行2,然后A想要行2、B想要行1——一个的死锁。

操作时间线A: START TRANSACTION B: START TRANSACTION A: UPDATE t4 SET v=v+1 WHERE id=1 → A锁住行1 B: UPDATE t4 SET v=v+1 WHERE id=2 → B锁住行2 A: UPDATE t4 SET v=v+1 WHERE id=2 → A想要行2(B拿着) (等待0.3秒后) B: UPDATE t4 SET v=v+1 WHERE id=1 → B想要行1(A拿着)——死锁形成

验证:InnoDB是怎么处理的

两个事务互相等对方手里的锁,InnoDB不会一直卡死,会检测出来并处理。

结果A的第二条UPDATE:成功,耗时0.3037秒(约等于B发起冲突请求前的等待时间) B的第二条UPDATE:立刻失败,耗时0.0012秒 → Error 1213: Deadlock found when trying to get lock; try restarting transaction

结果很清楚:B的请求几乎立刻(1.2毫秒)就被报错打回,而A的请求在B被回滚的瞬间就成功了。InnoDB并不会像普通锁等待那样傻等到超时(默认50秒),而是主动检测出了这个循环等待,选择其中一个事务作为"牺牲品"直接报错,把锁释放给另一个事务继续。

验证:InnoDB自己记录的死锁取证报告

SHOW ENGINE INNODB STATUS里有一段专门记录最近一次死锁的细节。

的LATEST DETECTED DEADLOCK片段*** (1) TRANSACTION: TRANSACTION 2307 ... UPDATE t4 SET v=v+1 WHERE id=2 *** (1) HOLDS THE LOCK(S): ... index PRIMARY of table demo.t4 ... (对应id=1这一行) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: ... (对应id=2这一行) *** (2) TRANSACTION: TRANSACTION 2308 ... UPDATE t4 SET v=v+1 WHERE id=1 *** (2) HOLDS THE LOCK(S): ... (对应id=2这一行) *** (2) WAITING FOR THIS LOCK TO BE GRANTED: ... (对应id=1这一行) *** WE ROLL BACK TRANSACTION (2)

这份报告把整个循环等待原原本本记录了下来:事务(1)拿着id=1的锁,等id=2;事务(2)拿着id=2的锁,等id=1——两个事务互相牵制,谁都走不下去。最后一行 WE ROLL BACK TRANSACTION (2) 就是InnoDB做出的裁决:回滚持有较少undo日志(代价更小)的那个事务,让另一个能继续执行。

这也是为什么写涉及多行更新的业务代码时,通常建议"总是按同一个顺序去更新多行数据"——如果A和B总是先锁行1再锁行2,就永远不会出现这种互相等待的循环,死锁天然就不会发生。InnoDB的死锁检测是兜底机制,不是让你可以随意加锁顺序的免死金牌。

分步演示:死锁从形成到被检测处理的过程

每一步都是本文上面用两个MySQL连接跑出来的原始结果。

循环等待形成的瞬间,InnoDB立刻检测并回滚一方
这一步新验证的状态

    本文用到的工具

    • 本机 MySQL 26.7.0(InnoDB)
    • pymysql + threading:构造A、B两个循环等待的事务
    • SHOW ENGINE INNODB STATUS:的LATEST DETECTED DEADLOCK取证报告
    京ICP备2026067502号-1京公网安备11010502063487号