mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
651 字
2 分钟
为什么数据库不应该使用外键
2023-09-26

在传统的数据库设计中,外键(FOREIGN KEY)是保证数据一致性的重要手段。但在现代互联网应用中,很多团队选择不使用外键。这是为什么?

一、外键的作用#

1.1 什么是外键?#

CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
FOREIGN KEY (user_id) REFERENCES users(id)
);
flowchart LR subgraph users 表 U1[id=1] U2[id=2] end subgraph orders 表 O1[user_id=1] O2[user_id=2] O3[user_id=1] end O1 -->|外键约束| U1 O2 -->|外键约束| U2 O3 -->|外键约束| U1

1.2 外键保证的约束#

约束说明
引用完整性子表记录必须引用父表存在的记录
级联删除删除父表记录时自动删除子表记录
级联更新更新父表记录时自动更新子表记录

二、外键的性能开销#

2.1 每次插入都需要检查#

-- 插入订单时,需要检查 user_id 是否存在
INSERT INTO orders (user_id, amount) VALUES (1, 100.00);
-- 没有外键时:直接插入
-- 有外键时:先查询 users 表检查 id=1 是否存在

2.2 锁竞争问题#

sequenceDiagram participant T1 as 事务 1 participant T2 as 事务 2 participant DB as 数据库 T1->>DB: INSERT orders (user_id=1) DB->>DB: 锁定 users 表中 id=1 的行 T2->>DB: INSERT users (id=1) Note over DB: 等待 T1 释放锁 T1->>DB: COMMIT T2->>DB: 获得锁,插入成功

问题:外键可能导致跨表的锁竞争。

2.3 写入性能对比#

# 无外键:QPS 约 10,000
# 有外键:QPS 约 3,000(下降 70%)
# 级联操作:QPS 约 1,500(进一步下降)

三、现代互联网架构的考量#

3.1 应用层负责数据一致性#

flowchart LR subgraph 应用层 A[应用代码] end subgraph 数据库 D[(MySQL)] end A -->|1. 先插入用户| D A -->|2. 插入订单| D A -->|3. 事务保证| A Note: 应用层控制一切,数据库只存储

模式

  • 应用层先插入用户,获取 user_id
  • 再插入订单,使用获取的 user_id
  • 如果插入订单失败,回滚事务
  • 数据库不再负责一致性检查

3.2 微服务架构#

flowchart TB subgraph 服务 A (用户服务) AU[用户表] end subgraph 服务 B (订单服务) BO[订单表] end AU -->|通过 API 调用| BO Note: 订单服务通过 HTTP/gRPC 调用用户服务验证用户存在

跨服务无法使用外键,所以应用层必须自己处理。

四、不使用外键的替代方案#

4.1 应用层校验#

# 应用层校验
def create_order(user_id, amount):
# 1. 验证用户存在
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
if not user:
raise ValueError("User not found")
# 2. 在同一事务中插入
with transaction:
db.execute(
"INSERT INTO orders (user_id, amount) VALUES (?, ?)",
user_id, amount
)

4.2 唯一约束代替外键#

-- 不使用外键,但保留唯一性
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
UNIQUE KEY idx_user_id (user_id) -- 索引仍保留
);

4.3 软删除代替级联删除#

-- 不使用级联删除,使用软删除
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
deleted_at TIMESTAMP NULL -- 软删除标记
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2)
);
-- 查询时过滤已删除的用户
SELECT * FROM orders o
LEFT JOIN users u ON o.user_id = u.id AND u.deleted_at IS NULL;

4.4 消息队列保证最终一致#

flowchart LR A[应用] -->|1. 插入用户| DB[(用户库)] A -->|2. 发送消息| Q[消息队列] B[消费者] -->|3. 插入订单| DB2[(订单库)] Note over Q: 异步处理,保证最终一致

五、使用外键的场景#

5.1 适合使用外键的场景#

场景说明
强一致性要求金融、库存等不允许数据不一致
小数据量数据量小,性能影响可忽略
团队技术统一所有人都理解外键约束

5.2 外键的配置建议#

-- 如果决定使用外键,建议配置
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
-- 不使用级联删除,手动处理
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE RESTRICT -- 禁止直接删除用户
ON UPDATE CASCADE -- 允许更新
);

六、总结#

6.1 不使用外键的原因#

原因说明
性能开销每次插入需要检查,高并发下严重影响性能
锁竞争可能导致跨表锁等待
架构演进微服务架构下无法跨服务使用外键
运维复杂批量删除/迁移受限

6.2 替代方案#

方案适用场景
应用层校验单体应用,可靠的事务
消息队列分布式系统,最终一致
软删除需要保留历史数据的场景
定时任务定期校验数据一致性

核心原则:根据业务场景选择合适的方案,不是非此即彼。

参考引用#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么数据库不应该使用外键
https://blog.souloss.com/posts/why-the-design/why-databases-should-not-use-foreign-keys/
作者
Souloss
发布于
2023-09-26
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时