本地数据库事务无法同时保证“业务数据提交”和“消息一定发送成功”。Transactional Outbox 解决可靠发消息,Saga 解决多个服务已经各自提交后如何继续或补偿,两者可以组合使用。

双写为什么不可靠

先写数据库再发消息:

1
订单提交成功 -> 进程崩溃 -> 消息没有发出

先发消息再写数据库:

1
消息发出 -> 数据库回滚 -> 消费者处理了不存在的订单

问题来自两个独立系统之间没有同一个原子提交边界。

Transactional Outbox

在同一个数据库事务中写业务表和消息表:

1
2
3
4
5
6
7
8
9
START TRANSACTION;

INSERT INTO orders (order_no, status)
VALUES ('O20240525001', 'created');

INSERT INTO outbox_events (event_id, topic, payload, status)
VALUES ('evt-001', 'order.created', '{"orderNo":"O20240525001"}', 'pending');

COMMIT;

后台发布器持续读取 pending 事件,发送到 Broker,成功后更新为 published。发布器可能重复发送,因此消费者仍需幂等。

1
本地事务 -> Outbox -> 发布器/CDC -> Broker -> 幂等消费者

消息表需要索引、批量读取、重试次数、下次重试时间和归档策略,不能无限增长。

Saga

订单流程可能跨越订单、库存和支付服务:

1
创建订单 -> 预留库存 -> 创建支付单 -> 完成

若创建支付单失败,Saga 不会让三个数据库同时回滚,而是执行补偿:

1
支付失败 -> 释放库存 -> 取消订单

补偿不是数据库回滚。退款、释放优惠券和撤销发货都有自己的业务规则,也可能再次失败,所以每一步都要可重试、幂等并记录状态。

编排还是协同

方式 工作方式 适用方向
编排式 一个协调器决定下一步和补偿 流程清晰、便于观察
协同式 服务通过事件触发下一步 解耦,但全局流程更难追踪

小型关键流程通常更适合显式状态机或编排器。无论哪种方式,都应保留关联 ID、步骤状态、超时、人工处理入口和审计日志。

什么时候不需要

如果业务可以放在同一个数据库和同一个服务中,优先使用本地事务。不要为了“架构先进”引入分布式一致性成本。

延伸阅读

站内搜索

没有找到内容!