Saga 用一串本地事务加补偿来代替分布式事务。谁来决定下一步做什么?集中在一个编排器里,还是让服务自己看事件接力?这篇用三个服务和真实的 RabbitMQ,把两种写法的成功、失败、服务宕机、加一步的修改成本逐一跑出来。
编排式(一个编排器按顺序调用三个服务)→ CONFIRMED:
0.0 ms [编排器] 调用订单服务:创建订单 o-orch-ok(PENDING)
14.0 ms [编排器] 调用 库存服务 stock_reserve → 成功
21.1 ms [编排器] 调用 支付服务 pay_charge → 成功
21.3 ms [编排器] 订单 → CONFIRMED
订单 {'o-orch-ok': 'CONFIRMED'},库存 99(预留中 1),余额 999920(已扣款 1 笔)
协同式(服务之间通过 RabbitMQ 事件接力)→ CONFIRMED:
0.3 ms [订单服务] 创建订单 o-choreo-ok(PENDING)
0.4 ms [订单服务] 发出 order.created
1.0 ms [库存服务] 收到 order.created
8.1 ms [库存服务] 发出 stock.reserved
8.6 ms [支付服务] 收到 stock.reserved
16.3 ms [支付服务] 发出 payment.succeeded
16.7 ms [订单服务] 收到 payment.succeeded
订单 {'o-choreo-ok': 'CONFIRMED'},库存 99(预留中 1),余额 999920(已扣款 1 笔)
协同式这一单在 RabbitMQ 上走了 3 条消息;编排式没有消息,编排器直接发起了 3 次调用(创建订单、库存、支付),最后一次订单状态更新是本地调用编排式是一个编排器顺序调用服务;协同式是服务之间用 order.created、stock.reserved、payment.succeeded 事件接力。数据结果一样,区别是谁掌握整个流程。
编排式 → CANCELLED:
0.0 ms [编排器] 调用订单服务:创建订单 o-orch-poor(PENDING)
0.4 ms [编排器] 调用 库存服务 stock_reserve → 成功
0.7 ms [编排器] 调用 支付服务 pay_charge → 失败:用户 1 余额不足
1.1 ms [编排器] 补偿:调用 库存服务 stock_release
1.6 ms [编排器] 订单 → CANCELLED
订单 {'o-orch-poor': 'CANCELLED'},库存 100(预留中 0),余额 50(已扣款 0 笔)
协同式 → CANCELLED:
0.2 ms [订单服务] 创建订单 o-choreo-poor(PENDING)
0.3 ms [订单服务] 发出 order.created
0.7 ms [库存服务] 收到 order.created
7.5 ms [库存服务] 发出 stock.reserved
7.8 ms [支付服务] 收到 stock.reserved
14.7 ms [支付服务] 发出 payment.failed
15.0 ms [库存服务] 收到 payment.failed
15.4 ms [库存服务] 发出 stock.released
15.6 ms [订单服务] 收到 stock.released
订单 {'o-choreo-poor': 'CANCELLED'},库存 100(预留中 0),余额 50(已扣款 0 笔)
共 4 条消息;补偿由 payment.failed 事件触发库存服务自己释放,再由 stock.released 触发订单服务取消编排式由编排器主动调用 stock_release;协同式由 payment.failed 事件触发库存服务自己释放。补偿链条隐含在订阅关系里。
编排式 → CANCELLED:
0.0 ms [编排器] 调用订单服务:创建订单 o-orch-empty(PENDING)
0.5 ms [编排器] 调用 库存服务 stock_reserve → 失败:sku-1 库存不足
0.7 ms [编排器] 订单 → CANCELLED
订单 {'o-orch-empty': 'CANCELLED'},库存 0(预留中 0),余额 1000000(已扣款 0 笔)
协同式 → CANCELLED:
0.4 ms [订单服务] 创建订单 o-choreo-empty(PENDING)
0.6 ms [订单服务] 发出 order.created
1.3 ms [库存服务] 收到 order.created
9.3 ms [库存服务] 发出 stock.failed
9.8 ms [订单服务] 收到 stock.failed
订单 {'o-choreo-empty': 'CANCELLED'},库存 0(预留中 0),余额 1000000(已扣款 0 笔)库存不足时没有任何东西要补偿,两种写法都直接取消订单。
编排式(每步最多重试 2 次、间隔 0.3 秒,用完就补偿并取消):订单状态变化 0.05s → PENDING, 0.63s → CANCELLED
0.1 ms [编排器] 调用订单服务:创建订单 o-down-orch(PENDING)
7.8 ms [编排器] 调用 库存服务 stock_reserve → 临时错误(1/3)
312.7 ms [编排器] 调用 库存服务 stock_reserve → 临时错误(2/3)
617.7 ms [编排器] 调用 库存服务 stock_reserve → 临时错误(3/3)
618.5 ms [编排器] 重试用完,订单 → CANCELLED
协同式(库存服务的消费者暂停 2 秒,消息留在队列里):订单状态变化 0.00s → PENDING, 2.06s → CONFIRMED
协同式里没有谁“知道”这单卡住了:订单一直 PENDING,直到库存服务回来、消息被处理;编排式里编排器在第一次失败时就知道,并按自己的重试 / 补偿策略做决定编排器知道失败,按重试与补偿策略决定;协同式靠消息队列缓冲,服务回来后自动完成,但没有谁知道这单卡住了。
协同式:要分别去 3 个服务的库里查: {'订单服务': 'CONFIRMED', '库存服务': 'RESERVED', '支付服务': 'CHARGED'}
编排式:编排器如果把每个 saga 的当前步骤存在自己的表里,查 1 个地方就够(本篇的编排器没有持久化步骤,只在内存里;持久化与观察放到第 30 篇)协同式要拼三个服务的状态;编排式可以把当前步骤放在自己的表里(本篇没有持久化,留到第 30 篇)。
写法 | 中位数 | p99 --------------+--------+------- 编排式 | 0.5 ms | 1.0 ms 协同式(RabbitMQ) | 1.4 ms | 3.0 ms 协同式慢在每一跳都是“发布 → 经 broker → 被消费”,同时它不要求调用方在线,下游挂了消息还在
协同式每一跳多一次 broker 往返,中位数 0.5 ms 对 1.4 ms,换来调用方不必在线。
编排式:流程定义是一张步骤表。改动 1 行,全部在编排器里,库存、支付服务的代码不动:
+积分 points_grant points_release
协同式:已有的 库存服务要改它订阅的事件:-payment.failed;+points.released
协同式:已有的 支付服务要改它订阅的事件:-stock.reserved;+points.granted
协同式:新增 积分服务 订阅 ['stock.reserved', 'payment.failed'];已有服务里有 2 个必须改订阅(库存的补偿触发源、支付的触发源);订单服务不变
四个服务的协同式,支付失败 → CANCELLED,事件链:
0.3 ms [订单服务] 发出 order.created
7.6 ms [库存服务] 发出 stock.reserved
14.6 ms [积分服务] 发出 points.granted
22.1 ms [支付服务] 发出 payment.failed
22.7 ms [积分服务] 发出 points.released
23.3 ms [库存服务] 发出 stock.released编排式只改步骤表;协同式里已有服务通过事件名耦合,2 个已有服务要改订阅。