如何设计业务数据库事务、幂等与并发
不要先背 ACID。先问:重复提交会不会多记一笔账。
这页只讲一件事:业务写接口怎样不重复、不覆盖、不半成功。
| 事故 | 根因 | 设计动作 |
| 用户点一次,系统记两笔。 | 弱网、刷新、重试都会重复提交。 | 幂等键 + 请求 hash + 原结果引用。 |
| 幂等表成功,账目没写。 | 幂等记录和业务写入不在同一事务。 | 幂等记录、账目、审计同事务提交。 |
| 两个 worker 导入同一流水。 | 都先查不到,再同时插入。 | 唯一约束做最后防线。 |
| 两个窗口同时改账。 | 后提交静默覆盖前提交。 | version 乐观锁或状态前置条件。 |
| 事务卡很久。 | 事务里等通知、搜索、外部接口。 | 主事务只保护事实,副作用走 Outbox / CDC。 |
判断:事务边界是“必须一起成功的事实”,不是把所有动作都塞进一个长事务。
transaction:
1. insert idempotency record if absent
2. same key + same hash + succeeded -> return original entry_id
3. same key + different hash -> return conflict
4. insert entry
5. insert audit / outbox if needed
6. commit
| 工具 | 挡什么 |
| 唯一约束 | 重复导入、重复月结、同外部流水重复写。 |
乐观锁 version | 两个窗口同时编辑同一行。 |
| 状态前置条件 | worker 抢任务、订单推进、导入批次推进。 |
| 行锁 / 条件更新 | 预算、余额、库存类原子判断。 |
| Outbox / CDC | 主事务成功后的缓存、搜索、报表和通知。 |
| 样本 | 通过标准 |
| 同一新增请求发 10 次。 | 只生成一条账目,重复请求返回同一结果。 |
| 两个 worker 同时导入同一流水。 | 最多一个成功,另一个转成已存在。 |
| 两个窗口同时编辑。 | 不会静默覆盖,必须提示版本冲突。 |
| 转账成功但响应丢失。 | 重试不会再生成一组转账。 |
| Outbox 消费失败后重启。 | 主账目不重复,副作用可重放。 |
记住:幂等挡重复意图,唯一约束挡并发重复,版本检查挡静默覆盖,Outbox 把外部副作用移出主事务。