为什么应用层事务代替数据库外键保证一致性¶
🎯 核心思想¶
✅ 应用层用事务(而不是数据库外键)来实现数据的一致性保障,这样做可以同时:
优点 | 解释 |
---|---|
提高插入性能 | 没有外键校验,数据库写入直接落盘,不用多余的查表动作。TPS 提升几十倍到上百倍。 |
支持分库分表 | 分库后(跨库),外键约束根本不能用,只有应用自己控制事务才能跨库一致。 |
防止连锁误删 | 删除父表不会自动 cascade 子表,只能应用逻辑控制,避免一删一片血案。 |
灵活扩展数据模型 | 表之间松耦合,想加新表、改字段,不用担心DDL动一动就影响一堆外键。 |
容易控制幂等和补偿 | 通过业务字段(比如 flow_id 唯一索引)来做幂等校验,补偿机制也好实现。 |
🔥 总结一句话¶
应用层用事务控制一致性,是高性能、高并发、大规模系统的标配做法,彻底摆脱外键的低效和局限。
📚 外键保证一致性 vs 应用事务保证一致性¶
外键保证一致性 | 应用事务保证一致性 |
---|---|
插入时数据库自动查父表验证 | 应用程序先查父表,拿到需要的数据后手动插入子表 |
删除父表时 cascade 自动删子表 | 删除前应用先检查子表是否允许删或逻辑软删 |
跨库无法生效 | 跨库通过分布式事务(Seata/TCC/SAGA)控制 |
写入慢、锁多、吞吐低 | 写入快、事务小、系统抗压能力强 |
🛠 应用层事务示例(详细版)¶
比如 Spring Boot 中的写法:
@Transactional
public void doLottery(User user, Activity activity) {
// 1. 查询用户账户信息,验证合法性
UserTimesAccount account = userTimesAccountMapper.selectByUserId(user.getId());
if (account.getAvailableTimes() <= 0) {
throw new BusinessException("抽奖次数不足");
}
// 2. 扣减账户可用次数
account.setAvailableTimes(account.getAvailableTimes() - 1);
userTimesAccountMapper.updateById(account);
// 3. 插入抽奖流水记录
UserTimesFlow flow = new UserTimesFlow();
flow.setUserId(user.getId());
flow.setActivityId(activity.getId());
flow.setTimesChange(-1);
flow.setFlowId(generateUniqueFlowId());
flow.setFlowChannel("抽奖");
userTimesFlowMapper.insert(flow);
// 4. 扣减活动库存(如果有库存)
activity.setStock(activity.getStock() - 1);
activityMapper.updateById(activity);
}
✅ 整个扣次数、记流水、扣库存是包裹在一个本地事务里的。只要中间任意一步失败,整个操作就会回滚,不会出现扣了次数但没扣库存的问题。
如果这里用了数据库外键会怎样?¶
- 插入
user_times_flow
流水时,数据库会去user_times_account
账户表再次去查询 user_id 是否存在。 - 每次插入流水时,多一次隐藏的查询校验,TPS降低明显。
- 在高并发下(比如双11秒杀),外键校验导致数据库锁表,查询堵塞,吞吐量下降。
- 如果后期把账户表和流水表拆成不同库,外键彻底失效,必须强制去掉,增加迁移风险。
🗂️ 两种数据表设计模式对比¶
设计方式 | 特点 | 适用场景 |
---|---|---|
强外键模式(建外键) | 自动校验完整性,插入写入慢,灵活性差 | 小型项目、数据量小、单库系统 |
应用事务控制模式(无外键) | 逻辑控制,扩展灵活,写入快,支持分库分表 | 中大型项目、高并发、微服务架构 |
✅ 结论:大规模、高并发系统必须采用应用事务控制,无外键设计。
不需要使用外键的其他考虑¶
-
同时还要考虑是否是逻辑删除,当为逻辑删除时,外键失效
-
同时当时微服务的架构时,table不在同一个数据库也无法建立外键; 如用户表、视频表、视频观看记录表(中间表,需要外键),微服务架构下可能table不在同一个数据库中
- 有些中间表如员工绩效表。员工离职了但是绩效记录还要保留,不需要保证外键带来的一致性
✨ 总结¶
正确的做法就是:
- 外键不建,表靠业务逻辑联系;
- 事务控制一批数据的一致性;
- 高性能插入,高并发扩展,系统结构清晰;
- 便于后期水平扩展(分库分表、异地多活)。