本地数据库事务无法同时保证“业务数据提交”和“消息一定发送成功”。Transactional Outbox 解决可靠发消息,Saga 解决多个服务已经各自提交后如何继续或补偿,两者可以组合使用。
双写为什么不可靠
先写数据库再发消息:
1 | 订单提交成功 -> 进程崩溃 -> 消息没有发出 |
先发消息再写数据库:
1 | 消息发出 -> 数据库回滚 -> 消费者处理了不存在的订单 |
问题来自两个独立系统之间没有同一个原子提交边界。
Transactional Outbox
在同一个数据库事务中写业务表和消息表:
1 | START TRANSACTION; |
后台发布器持续读取 pending 事件,发送到 Broker,成功后更新为 published。发布器可能重复发送,因此消费者仍需幂等。
1 | 本地事务 -> Outbox -> 发布器/CDC -> Broker -> 幂等消费者 |
消息表需要索引、批量读取、重试次数、下次重试时间和归档策略,不能无限增长。
Saga
订单流程可能跨越订单、库存和支付服务:
1 | 创建订单 -> 预留库存 -> 创建支付单 -> 完成 |
若创建支付单失败,Saga 不会让三个数据库同时回滚,而是执行补偿:
1 | 支付失败 -> 释放库存 -> 取消订单 |
补偿不是数据库回滚。退款、释放优惠券和撤销发货都有自己的业务规则,也可能再次失败,所以每一步都要可重试、幂等并记录状态。
编排还是协同
| 方式 | 工作方式 | 适用方向 |
|---|---|---|
| 编排式 | 一个协调器决定下一步和补偿 | 流程清晰、便于观察 |
| 协同式 | 服务通过事件触发下一步 | 解耦,但全局流程更难追踪 |
小型关键流程通常更适合显式状态机或编排器。无论哪种方式,都应保留关联 ID、步骤状态、超时、人工处理入口和审计日志。
什么时候不需要
如果业务可以放在同一个数据库和同一个服务中,优先使用本地事务。不要为了“架构先进”引入分布式一致性成本。