4.2 最终一致性与 Saga 补偿缝合


4.2 最终一致性与 Saga 补偿缝合

摘要:上一节把数据切开了——也就意味着"一个本地事务搞定一切"的路断了。这一节讲透为什么微服务里强一致高不可攀,以及业界用 Saga 怎么靠"逐步前进 + 出错后补偿"把账拉回最终一致。用商城一笔下单做完整演练,把编排式与协同式两种写法都走一遍。

数据拆开的第一道坎是事务:单体里"改订单又扣库存,要么全成要么全退"一条本地事务就解决了。拆成两个库之后,本地事务管不了另一头。怎么办?好多人第一反应是上"分布式事务"——用两阶段提交在多个服务间强一致。我们先讲清这条路为什么难走,再讲 Saga 为什么是更实际的答案。

为什么 2PC 在这样的场景里吃力

两阶段提交(2PC)思路很正:先让所有参与者"准备"好,全部同意后统一提交。听起来完美,实务上却很疼:全局协调者成了高可用单点,某个参与者 prepare 后宕机就一直挂起,网络分区时两边僵持,性能和可用性都差。在服务多、环节多、跨机房的微服务里,2PC 的强一致相比它带来的单点风险,往往得不偿失。

记住一个判断:本地事务解决"一个库内的原子性";服务间要的往往不是"每一秒都一致",而是"最终能对得上,且中间状态可解释"。后者就是最终一致性 + Saga。

先看 Saga 长什么样:向前推进 + 出错补偿

Saga 不是一个库或框架,是一种把多步业务编排起来的执行模式:每一子操作都是本地事务,整套操作逐个子事务前进;一旦某一步失败,就把前面做过的步骤"反向补偿"回来,让数据回到一个说得通的先验状态。

先看 Saga 长什么样:向前推进 + 出错补偿

让事务"有补偿":设计侧最重要的事

编排式与协同式只是"谁来安排顺序"的区别,Saga 的灵魂在于每个正向动作都有反向下法。设计下单 Saga 时,你要为每个参与方想清楚"这叫它怎么办就叫补偿":

正向子操作 对应的补偿操作
创建订单(订单服务) 将订单状态改为已取消
扣减库存(库存服务) 把扣掉的再归还
划转支付(支付服务) 发起退款
发放积分(积分服务) 扣回积分(或置负留待对账)

补偿不是无脑"撤销",因为有可能是"正向成功了但通知丢了"——所以很多补偿还要带幂等判断:归还一次就够了,不能因为重复重试还两次。幂等这个话题在第 6 章重试还会再碰,这里是它最关键的一处落地。

编排式 vs 协同式,怎么选

  • 编排式:有一个中央协调器/状态机,按脚本一步步推进,失败时由它执行补偿序列。好处是顺序清晰、回退回放直观、调试友好;代价是协调器自己也成了需要高可用的点。
  • 协同式:没有总控,各服务"收到事件→处理→发新事件",像一条消息的接力。好处是松耦合、天然支持追加参与者;坏处是出错时链路散在多个服务里,要靠追踪和对账才能理清。

中小团队头一回搭,我更推荐编排式起步——先把"谁先谁后、错了怎么补"这层明牌打好,等边界够清晰、事件可靠性够足时再考虑往协同式演进。一上来就协同式,出事你连从哪查起都不知道。

陷阱:把 Saga 当成"一定成功的保证"

Saga 只保证"数据最终到达一个一致的状态",不保证"业务一定成功"。如果支付真是退不回来、库存真的回不去,那 Saga 的结束状态可能是一个"挂了待人工处理"的中间态。这不可耻——比起假装"全能事务",接受"需要人兜底"才是工程诚实。所以 Saga 推进中要给每个子操作留可查询的状态、可重放的日志,让对账程序能把账给"算平"。

让 Saga 每一步都能"重放"和"幂等"

Saga 最容易被低估的工程点,是它每一步都可能被重复执行——网络重试、协调器重启、消费者重复拉取,都会让同一子操作跑两遍。如果不做防护,后果直接可见:重扣一次库存、重复发一笔退款、同一单被通知两次。所以让 Saga 立得住,下面两件事必须做:

  1. 每步子操作幂等。给每个子操作一个全局唯一的"执行幂等键"(比如 (订单号, 步骤名)),重复执行时先查"这件事办过没有",办过就直接返回已办结果,不再真改数据。归还库存、退款这类动作最容易被重放误伤,幂等键是它们的重甲。
  2. 每一步都有可重放的日志/事件。Saga 所以能纠错,前提是"发生的时间和发生了什么"是可追查的。协调器每一步写下一行"执行了哪个子操作、拿到什么结果",这句日志既是断点续作(协调器重启后从日志接着走)的依据,也是对账工具能"算平账"的凭据。

记住一个反直觉的事实:Saga 里最怕的不是"某一笔失败"(那有补偿去兜),而是"某笔被重复成功"(它会让数据不真实地多扣或多发)。把幂等和重放做扎实,Saga 前面的辛辛苦苦才算真正闭环。

一次"复盘" 出来的结论:什么时候干脆别用 Saga

讲了Saga各种写法,但最后一层冷静的判断反而最值钱:不是所有跨服务都值得上一套 Saga。它有真实成本——要写补偿逻辑、要管幂等重放、要维护协调状态。所以当你评估一个跨服务场景时,先试问三句:

  1. 这笔跨服务业务,是不是真的"必须一起对账"? 如果只是"下单后发一封报表邮件",用事件异步 + 对账即可,根本不需要 Saga 那套事务感。
  2. 有没有可能把边界收窄,让它变成"单服务内的本地事务"? 如果一件事其实可以归到一个写主名下(比如挪一下职责),能缩小到本地事务就别搭 Saga。
  3. 失败代价能接受"中间态 + 人工兜底"吗? Saga 跑完也可能停在"待人工处理",如果你们的业务完全接受不了的这种兜底,那 Saga 也不合适。

把这三句走完,真正需要 Saga 的场景会少掉一大半。Saga 是当"无法缩窄、又确实要一致"时的最后手段,而不是每个跨服务默认的标配。用得上它再利用,用不上就别为了"显得专业"硬上一个——这和前面"别为了高级而 CQRS、而拆服务"是同一条冷却的清醒。

本节要点

  • 数据切开后,强事务在服务间高不可攀,2PC 的两段锁代价大
  • Saga=子事务逐个本地推进 + 失败后反向补偿,目标是最终一致
  • 每个正向子操作都配一个补偿,且补偿要幂等
  • 编排式控制集中、调试友好,协同式更松耦合,中小团队先编排式
  • Saga 不保证"一定成",允许多了一个"待人工兜底"的中间态

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U