一、外键是什么?

在这里插入图片描述

简单说,外键就是用来保证“两张表的数据能对上”。比如订单表的user_id必须指向用户表里存在的id。

1
FOREIGN KEY (user_id) REFERENCES user(id)

加了外键,数据库会自动帮你检查:插入订单时,用户必须存在;删除用户时,要么拒绝,要么连带删订单。

听起来很完美,对吧?那为什么阿里巴巴开发手册明确写着“不得使用外键”?这篇文章讲清楚外键的坑和替代方案

二、为什么不推荐用外键?

在这里插入图片描述

外键像个负责任的保安,每次有人进出都查证件——安全是安全了,但门口排起了长队。

1. 性能损耗

每次插入、更新、删除订单,数据库都要去user表查一下这个user_id存不存在。这条select语句看着简单,但在高并发下,数据库累死。

一个订单操作变成:插入订单 + 查询用户。数据库要多干一份活。

2. 分库分表不友好

外键只能在一个数据库实例内生效。

业务大到一定程度,user表在一台机器,orders表在另一台机器。外键直接废了,代码还得大改——把所有数据完整性检查从数据库搬到代码里。

与其到时候哭着改代码,不如一开始就别用外键。

3. 耦合太紧

想删user表的一条数据?先看看有没有订单引用它。想改表结构?先拆外键。想迁移数据?外键关系像蜘蛛网一样缠着。

一个表的变动,牵一发动全身。

4. 开发测试麻烦

造测试数据的时候,得先插user,再插orders,顺序不能乱。清数据的时候,得先删orders,再删user,顺序也不能乱。

每次测试都得小心翼翼地按顺序来,很烦。


三、不用外键,怎么保证数据完整性?

在这里插入图片描述

大厂的做法是:不设保安,装一套门禁系统。

方案 做法
应用层保证 在代码里写逻辑:插入订单前,先查一下用户存不存在。
事务 用数据库事务保证一系列操作“要么全成功,要么全失败”,防止中间状态不一致。
唯一索引/检查约束 用这些轻量级的约束代替外键,成本低很多。
定期巡检 跑定时任务,扫出“孤儿数据”(比如查无此人的订单),修掉或报警。
1
2
3
4
5
6
7
8
9
10
// 应用层保证示例
public void createOrder(int userId, BigDecimal amount) {
// 先查用户存不存在
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
// 再插入订单
orderMapper.insert(new Order(userId, amount));
}

代码多写几行,但数据库轻松了,系统也能水平扩展了。


四、什么时候还可以用外键?

在这里插入图片描述

场景 判断
小项目、并发低 可以用。外键的便利性大于性能损失。
单机部署、不拆分 可以用。没有分库分表的烦恼。
财务系统、对账系统 可以用。数据一致性要求极高,宁愿慢也不能错。
内部管理系统 可以用。并发不高,外键省事。

阿里那个规范说的是“不得使用”,那是针对他们那种千万级并发的系统。如果需要维护的是小项目,则可以无视这些


五、总结

方案 优点 缺点
用外键 省事、数据库自动保证完整性 性能差、分库分表难、耦合紧
不用外键 性能好、扩展灵活、解耦 代码多写几行、需要自己保证完整性

外键像个负责任的保安,安全但效率低。大厂不要保安,而是装门禁系统,每个人自己刷卡,系统只记录谁没刷。安全靠制度和技术,不靠一个人堵在门口。

不是外键不好,是它的代价超出了它的价值。