必须禁用ON UPDATE CASCADE并改用显式两步操作,因其导致锁顺序失控、隐式死锁及全表扫描阻塞;须同步建立外键列独立索引、应用层实现先子后父的有序批量更新、彻底删除外键约束。

为什么ON UPDATE CASCADE会触发隐式死锁
它不报语法错,但会让锁顺序完全失控。执行UPDATE parent SET id = ?时,InnoDB会自动扫描子表所有匹配行并加X锁——这个过程你没写SQL,也不受事务内语句顺序控制。
典型死锁链:事务A先UPDATE parent→隐式锁住子表部分行→再显式UPDATE child;事务B先UPDATE child→持有子表行锁→再UPDATE parent→触发级联→尝试重锁同一行→Deadlock found when trying to get lock。
更危险的是:子表外键列(如parent_id)若无索引,InnoDB直接全表扫描+表级意向锁,多个事务一碰就卡死,不是死锁而是阻塞。
必须立即做的三件事:索引、拆分、禁用
这三步缺一不可,只做其中一两个仍大概率复现死锁:
- 为每个外键列单独建索引:
ALTER TABLE child_table ADD INDEX idx_parent_id (parent_id);。不能依赖联合索引前缀,InnoDB只认独立单列索引或以该列为最左前缀的复合索引 - 把级联更新逻辑从数据库层移到应用层:先
SELECT id FROM child_table WHERE parent_id = ?查出待更新ID列表,再UPDATE child_table SET parent_id = ? WHERE id IN ()批量更新 - 删除外键约束:
ALTER TABLE child_table DROP FOREIGN KEY fk_name;。残留的外键仍会触发完整性校验S锁,别留着“以防万一”
应用层两步更新的关键细节
显式两步看似简单,漏掉这些点照样死锁:
-
SELECT语句必须加FOR UPDATE,否则可能读到旧快照,导致子表更新遗漏;但不能用SELECT ... JOIN parent_table,会额外锁父表 - 整个操作必须包裹在同一个事务中:
BEGIN→SELECT ... FOR UPDATE→UPDATE child_table→UPDATE parent→COMMIT,且所有业务路径严格按「先子后父」顺序执行 - ID列表要
ORDER BY id ASC升序排列,每批不超过500个,避免IN过长或优化器放弃索引
隔离级别和ORM配置的隐藏陷阱
MySQL默认的REPEATABLE READ会让问题更隐蔽:
即使已禁用级联、建好索引,在RR下SELECT ... FOR UPDATE仍可能加Next-Key Lock(记录锁+间隙锁),导致并发插入时意外锁住空隙。若业务允许,可临时将该事务设为READ COMMITTED。
检查ORM配置:Django的on_update=models.CASCADE、Rails的dependent: :update这类配置会自动生成隐式级联SQL,必须清理。SHOW ENGINE INNODB STATUS里看不到外键动作,它藏在事务前一条父表语句之后——这才是最难排查的地方。


















