外键检查时锁父表被操作记录的lock_s共享锁和子表匹配行的lock_x排他锁;子表外键列无有效索引会导致全表扫描并等效表锁,级联操作进一步放大锁范围与死锁风险。

外键检查时到底在锁什么
外键检查本身不直接“卡住”语句,但它强制 InnoDB 对两张表做隐式加锁:执行 UPDATE parent 或 DELETE FROM parent 时,InnoDB 必须确认子表里没有正在引用该记录的行——这个校验过程会触发两层锁:对父表被操作记录加 LOCK_S(共享锁),对子表所有匹配 parent_id 的行加 LOCK_X(排他锁)。这不是业务代码写的锁,是引擎自动加的,且无法跳过。
子表外键列没索引 = 实际表锁
最常见也最致命的问题:子表的外键列没建索引,或索引结构无效。InnoDB 查子表依赖行时,必须快速定位;一旦 parent_id 不在索引最左位置(比如只在组合索引中间),引擎就放弃走索引,退化为全表扫描。每扫一行就加一个行锁,最终效果等同于“锁全表”。此时任何其他事务想读/写该子表,基本都会卡在 Lock wait timeout exceeded。
-
ALTER TABLE child ADD INDEX idx_fk_parent_id (parent_id)✅ 有效 -
ALTER TABLE child ADD INDEX idx_wrong (status, parent_id)❌ 无效 ——parent_id不是最左前缀 - 即使父表
id是主键,子表仍需自己建索引;InnoDB 不复用父表索引
ON DELETE CASCADE 让锁范围翻倍
级联删除看起来省事,实则把锁压力放大数倍。一条 DELETE FROM parent WHERE id = 1 触发级联后,InnoDB 会逐行处理子表匹配记录:对每条子行做外键校验 → 加锁 → 删除 → 维护二级索引。整个过程串行执行,锁持有时间远长于显式分步操作。子表若上万行,父表那条记录的 LOCK_S 可能被持有一分钟以上,阻塞所有其他访问该父记录的事务。
- 级联不是批量删,是单行循环,无优化空间
- 锁等待链变长,死锁概率陡增(尤其并发高时)
- 替代做法:先
SELECT id FROM child WHERE parent_id = ?,再DELETE FROM child WHERE id IN (...),最后删父表
为什么 SHOW PROCESSLIST 看不到真正堵点
你看到某个 DELETE 在 “Sending data” 或 “Updating” 状态卡住,不代表它自身慢——它很可能正等着另一个事务释放子表某行的 LOCK_X。而那个持锁事务可能早已执行完 SQL,却忘了 COMMIT,或者正在做 HTTP 调用、文件读写等外部耗时操作,导致锁一直挂着。这时 SHOW ENGINE INNODB STATUS\G 里的 LATEST DETECTED DEADLOCK 或 INNODB_TRX 中长时间运行的 trx_state = 'RUNNING' 才是关键线索。
真正容易被忽略的是:外键锁是“隐身”的。它不出现在你的 SQL 里,但会实实在在挡住后续所有涉及父表或子表的操作——尤其是当子表数据量大、索引又没建对的时候,问题会从“偶发超时”迅速恶化成“整库响应迟滞”。











