摘要:上一节把数据切开了——也就意味着"一个本地事务搞定一切"的路断了。这一节讲透为什么微服务里强一致高不可攀,以及业界用 Saga 怎么靠"逐步前进 + 出错后补偿"把账拉回最终一致。用商城一笔下单做完整演练,把编排式与协同式两种写法都走一遍。
数据拆开的第一道坎是事务:单体里"改订单又扣库存,要么全成要么全退"一条本地事务就解决了。拆成两个库之后,本地事务管不了另一头。怎么办?好多人第一反应是上"分布式事务"——用两阶段提交在多个服务间强一致。我们先讲清这条路为什么难走,再讲 Saga 为什么是更实际的答案。
两阶段提交(2PC)思路很正:先让所有参与者"准备"好,全部同意后统一提交。听起来完美,实务上却很疼:全局协调者成了高可用单点,某个参与者 prepare 后宕机就一直挂起,网络分区时两边僵持,性能和可用性都差。在服务多、环节多、跨机房的微服务里,2PC 的强一致相比它带来的单点风险,往往得不偿失。
记住一个判断:本地事务解决"一个库内的原子性";服务间要的往往不是"每一秒都一致",而是"最终能对得上,且中间状态可解释"。后者就是最终一致性 + Saga。
Saga 不是一个库或框架,是一种把多步业务编排起来的执行模式:每一子操作都是本地事务,整套操作逐个子事务前进;一旦某一步失败,就把前面做过的步骤"反向补偿"回来,让数据回到一个说得通的先验状态。

编排式与协同式只是"谁来安排顺序"的区别,Saga 的灵魂在于每个正向动作都有反向下法。设计下单 Saga 时,你要为每个参与方想清楚"这叫它怎么办就叫补偿":
| 正向子操作 | 对应的补偿操作 |
|---|---|
| 创建订单(订单服务) | 将订单状态改为已取消 |
| 扣减库存(库存服务) | 把扣掉的再归还 |
| 划转支付(支付服务) | 发起退款 |
| 发放积分(积分服务) | 扣回积分(或置负留待对账) |
补偿不是无脑"撤销",因为有可能是"正向成功了但通知丢了"——所以很多补偿还要带幂等判断:归还一次就够了,不能因为重复重试还两次。幂等这个话题在第 6 章重试还会再碰,这里是它最关键的一处落地。
中小团队头一回搭,我更推荐编排式起步——先把"谁先谁后、错了怎么补"这层明牌打好,等边界够清晰、事件可靠性够足时再考虑往协同式演进。一上来就协同式,出事你连从哪查起都不知道。
Saga 只保证"数据最终到达一个一致的状态",不保证"业务一定成功"。如果支付真是退不回来、库存真的回不去,那 Saga 的结束状态可能是一个"挂了待人工处理"的中间态。这不可耻——比起假装"全能事务",接受"需要人兜底"才是工程诚实。所以 Saga 推进中要给每个子操作留可查询的状态、可重放的日志,让对账程序能把账给"算平"。
Saga 最容易被低估的工程点,是它每一步都可能被重复执行——网络重试、协调器重启、消费者重复拉取,都会让同一子操作跑两遍。如果不做防护,后果直接可见:重扣一次库存、重复发一笔退款、同一单被通知两次。所以让 Saga 立得住,下面两件事必须做:
(订单号, 步骤名)),重复执行时先查"这件事办过没有",办过就直接返回已办结果,不再真改数据。归还库存、退款这类动作最容易被重放误伤,幂等键是它们的重甲。记住一个反直觉的事实:Saga 里最怕的不是"某一笔失败"(那有补偿去兜),而是"某笔被重复成功"(它会让数据不真实地多扣或多发)。把幂等和重放做扎实,Saga 前面的辛辛苦苦才算真正闭环。
讲了Saga各种写法,但最后一层冷静的判断反而最值钱:不是所有跨服务都值得上一套 Saga。它有真实成本——要写补偿逻辑、要管幂等重放、要维护协调状态。所以当你评估一个跨服务场景时,先试问三句:
把这三句走完,真正需要 Saga 的场景会少掉一大半。Saga 是当"无法缩窄、又确实要一致"时的最后手段,而不是每个跨服务默认的标配。用得上它再利用,用不上就别为了"显得专业"硬上一个——这和前面"别为了高级而 CQRS、而拆服务"是同一条冷却的清醒。