为什么不推荐使用外键?

#中间件#面试八股

为什么不推荐使用外键?

结论:外键把「引用完整性」的校验放进了数据库引擎,代价是写入路径变长、锁范围扩大、DDL 与迁移被卡住,而且一分库就彻底失效。互联网业务普遍改为「应用层保证 + 逻辑外键 + 离线校对」,把一致性责任上移到可以水平扩展的一侧。

一、外键在写入时做了什么

INSERT INTO order_item(order_id, sku_id) VALUES (1001, 88);

  ① 检查父表 order 是否存在 id=1001
       └─▶ 对父表对应记录加共享锁(S 锁),防止并发删除

  ② 检查通过后写入子表
  ③ 父表执行 DELETE/UPDATE 主键时
       └─▶ 反向检查子表并加锁,按 ON DELETE 规则级联或拒绝

一次子表写入变成「两次索引查找 + 两把锁」,高并发下这把父表的共享锁会成为热点。

二、不推荐的具体理由

维度外键带来的问题实际影响
写入性能每次写子表都要查父表并加锁QPS 高时父表行锁竞争、死锁概率上升
锁范围父表上的 S 锁与业务 UPDATE 的 X 锁互斥秒杀、下单等热点表被拖慢
DDL加外键要校验全表数据大表加约束可能锁表数分钟
分库分表跨库无法建外键拆分后约束直接失效,还得先删掉
数据迁移导入顺序必须严格父子有序批量导入、回档、影子表迁移全部变复杂
级联删除ON DELETE CASCADE 静默删数据误删一条主记录带走几千行子记录
排查报错发生在引擎层,堆栈不指向业务代码定位成本高,错误信息对业务不友好

三、代码:外键写法与替代方案

外键写法(不推荐用于高并发业务表):

CREATE TABLE `order` (
  `id`     BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `status` TINYINT NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

CREATE TABLE `order_item` (
  `id`       BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `order_id` BIGINT UNSIGNED NOT NULL,
  `sku_id`   BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_order` (`order_id`),
  CONSTRAINT `fk_item_order` FOREIGN KEY (`order_id`)
    REFERENCES `order` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;

应用层替代:父记录与子记录在同一个本地事务里写入,删除走「先删子、再删父」,并发靠行锁或版本号控制。

START TRANSACTION;
INSERT INTO `order` (id, status) VALUES (1001, 1);
INSERT INTO `order_item` (order_id, sku_id) VALUES (1001, 88);
COMMIT;

离线校对孤儿数据(把实时约束换成定时体检):

-- 子表存在、父表已消失的脏数据
SELECT i.id, i.order_id
FROM order_item i
LEFT JOIN `order` o ON o.id = i.order_id
WHERE o.id IS NULL
LIMIT 1000;

批导数据时关掉校验再打开,能省掉排序成本:

SET FOREIGN_KEY_CHECKS = 0;
-- 批量导入 ...
SET FOREIGN_KEY_CHECKS = 1;

四、什么情况下仍然值得用

  • 后台管理、报表类低并发系统,数据正确性优先于吞吐。
  • 单库单表、数据量小、以人工录入为主的系统,外键是免费的文档。
  • 需要数据库层面兜底、且团队没有成熟的一致性治理能力时,外键比「没人写校对任务」更可靠。

常见追问

  • 删了外键,怎么保证不出现孤儿数据? 写入走同一个事务、删除按依赖顺序、加定时校对任务与告警,三者组合比单靠外键更可控。
  • ON DELETE CASCADE 真的危险吗? 危险在「隐式」:一条 DELETE 可能删除大量数据且不经过业务代码,审计日志里只剩一条 SQL。
  • 外键会影响查询性能吗? 读路径几乎无影响;写路径有额外校验与加锁,热点表上非常明显。
  • 分库分表后原外键怎么办? 拆表前先 DROP FOREIGN KEY,否则 DDL 与数据迁移都会被约束顺序卡住。
  • 逻辑外键要不要建索引? 必须。order_item.order_id 上的索引既服务关联查询,也避免删除父记录时的全表扫描。

小结

外键是「用数据库的耦合换应用层的简单」。在单库、低并发、强一致优先的场景它很划算;在分片、高并发、快速迭代的互联网业务里,它把扩展性锁死在单库上,所以普遍被应用层事务 + 逻辑外键 + 离线校对取代。

笔记加载中…