必须禁用ON DELETE/UPDATE CASCADE并为外键列单独建索引,因其隐式加锁不可控:无索引时全表扫描并逐行加X锁,导致锁范围失控与高概率阻塞。

外键约束本身不显式加锁,但每次 INSERT、UPDATE 或 DELETE 涉及外键列时,InnoDB 会隐式加锁——先对父表加 LOCK_S(共享锁),再对子表当前行加 LOCK_X(排他锁)。这个两阶段非原子加锁过程,就是锁等待问题的根源。
子表外键列没索引 → 全表扫描 + 行锁升级
这是最常见也最容易被忽略的诱因。InnoDB 在验证外键引用时,必须快速定位子表中所有 parent_id = ? 的行。若 parent_id 列没有索引,它只能全表扫描,并对每行逐个加 LOCK_X——哪怕你只更新子表一条记录,也可能锁住上万行。
- 现象:执行
UPDATE child SET status = 'done' WHERE id = 123卡住,SHOW ENGINE INNODB STATUS显示大量waiting for lock - 后果:锁范围远超预期,与其他事务冲突概率飙升
- 验证方式:
SELECT * FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_NAME = 'child' AND COLUMN_NAME = 'parent_id' AND SEQ_IN_INDEX = 1,查不到结果即为缺失 - 修复语句:
ALTER TABLE child ADD INDEX idx_parent_id (parent_id)—— 必须是parent_id为最左前缀
ON DELETE CASCADE / ON UPDATE CASCADE 是锁放大器
级联操作看似省事,实则把一次逻辑操作拆成多个隐式 DML,每个都重复走一遍外键检查流程,且无法批量优化。
-
DELETE FROM parent WHERE id = 100触发级联后,等价于:对父表加X锁 → 扫描子表所有parent_id = 100行 → 对每一行单独加X锁 → 再删 → 循环 - 风险点:子表数据量大时,父表记录锁会长时间持有,阻塞其他事务访问该记录
- 更隐蔽的问题:
ON UPDATE CASCADE让锁顺序失控——事务 A 先UPDATE parent隐式锁子表某些行,再显式UPDATE child,可能和事务 B 的锁形成 ABBA 环 - 建议:直接禁用,改用应用层分步操作:
SELECT id FROM child WHERE parent_id = ? FOR UPDATE→ 批量UPDATE child→ 最后UPDATE parent
外键检查触发的 S 锁会被误判为“不危险”,实际极易冲突
很多人觉得 LOCK_S 是共享锁,不会阻塞读,就放松警惕。但当多个事务交叉操作父子表时,S 锁会成为死锁链中关键一环。
- 典型死锁场景:
事务 A:UPDATE child SET parent_id = 200 WHERE id = 123→ 先锁父表id = 200(S锁),再锁子表该行(X锁)
事务 B:DELETE FROM parent WHERE id = 200→ 先锁父表id = 200(X锁),再锁子表所有匹配行(X锁)
→ A 持 S 等 X,B 持 X 等 S,循环等待成立 - 排查线索:看
SHOW ENGINE INNODB STATUS\G中的LATEST FOREIGN KEY ERROR段,有记录说明外键检查已触发;再结合INNODB_TRX和INNODB_LOCK_WAITS定位谁在等谁 - 注意:即使父表主键有索引,子表仍需自己建索引——InnoDB 不复用父表索引
真正麻烦的不是锁本身,而是这些锁完全隐式发生、不体现在 SQL 里,也很难在业务代码中感知。一旦子表外键列缺索引,或用了级联,问题就会在高并发下突然爆发,而且往往只在特定数据分布或事务组合下才复现。


















