想象你正在规划一场四城跨年旅行:订机票、订酒店、买景区门票、订跨年餐厅。四件事分散在四家平台,没有谁能一口气全办妥。更麻烦的是,如果机票订好了,酒店却满房,这趟旅行还没出发就崩了。这时候你最希望系统做什么?自动把机票也退掉,让整趟计划干干净净回到原点。分布式系统里的 Saga 模式,干的就是这件事。 一个跨服务流程里,本地事务就像一个个孤立的预订,谁也管不了谁。Saga 的思路很朴素:先把每站订好,每站配一条“后悔药”(补偿操作);中途某站失败,就沿来路一站站吃后悔药,把已生效的预订全部撤销。至于“谁来安排这一站接一站的动作”,业界有两种玩法:一种由总协调者拿着总行程单逐站指挥,叫编排式(Orchestration);一种没有头儿,每个服务完事就发通知给下一站接力,叫协同式(Choreography)。本文就借这场跨城旅行,把两种玩法的门道讲透。
掌握 Saga 两种实现方式的取舍,是设计可靠跨服务数据一致性的核心技能,也是微服务架构师与后端工程师面试的常见考点。
可视化演示
展示Saga模式通过本地事务链与补偿操作实现最终一致性,含编排式协调器与协同式事件驱动两种实现对比。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Saga正向事务链 每个服务执行本地事务并发布事件 正向链按顺序执行T1→T2→T3→T4 Saga = T1 + T2 + T3 + T4 Saga通过本地事务序列实现分布式最终一致性
1. 分布式事务困局
旅行计划的第一步,是搞清楚“为什么不能一个电话全搞定”。订机票找航空公司,订酒店找酒店集团,买门票找景区,订餐找餐馆——四家平台各管各的系统,数据库互不相通。这在分布式系统里叫“跨服务数据一致性”难题。
单体应用时代,一次操作几个表,一个数据库事务就能锁住全部数据。微服务化之后,数据被拆到不同服务的数据库里,本地事务只能管自己那一亩三分地。于是出现两条经典路线:2PC(两阶段提交)和最终一致性。Saga 属于后者。
2PC 的思路是“先问清楚再动手”:第一阶段所有参与者预提交,第二阶段协调者拍板提交或中止。它有两个硬伤:一是参与者全程锁资源同步等待,网络抖动就卡死一片;二是协调者单点,它一挂,所有参与者僵在半路。更底层地看,CAP 定理告诉我们:网络分区(P)不可避免,此时一致性(C)和可用性(A)只能二选一。2PC 押注了 C 牺牲了 A,所以才需要 Saga 这种“保 A 求最终 C”的路线。跨服务事务的失败概率还会随服务数量指数级上升:
这就像四家平台必须同时给你“有房有票有位”,一家的系统出问题,整个计划就得重排。2PC 是“先全锁住等我确认”,Saga 则是“先订着,不行就退”——后者显然更贴近真实世界。
- 第 i 个节点成功率\(p_i\) 表示第 \(i\) 个微服务节点成功完成本地操作的概率,n 是参与本次跨服务流程的服务数量。
- 整体等于连乘\(\prod_{i=1}^{n} p_i\) 表示把 n 个概率依次相乘,也就是所有服务必须全部成功,整体才成功。
- 规模放大效应每个概率都小于 1,乘的项越多结果越小,服务一多,整体成功率断崖式下降。
2. Saga 模式原理
既然同步锁死行不通,Saga 换了个思路:先走起来,再留后手。你订机票时,航空公司告诉你“可免费退改”;订酒店时,平台写明“入住前 24 小时可取消”。这些退改政策,就是补偿操作的种子。
1987 年 Hector Garcia-Molina 提出 Saga 概念,2017 年前后在微服务工程实践中被发扬光大:把一个大型分布式事务拆成 n 个本地事务(T₁……Tₙ),每个本地事务配一个补偿操作(C₁……Cₙ)。全部成功则任务完成;某个 Tᵢ 失败,就逆序执行 Cᵢ₋₁……C₁,把已生效的副作用“补”回来。Saga 不保证全局强一致,只保证最终一致:过程可以呈现中间状态(机票已出、酒店未确认),但终态只有两种——“全部完成”或“全部补偿完成”。
关键设计点在于:每个 Tᵢ 和 Cᵢ 都是独立提交的本地事务,各自持久化,任何一步失败都可以通过重试恢复。整个 Saga 可以形式化为一组“正向-补偿”对:
行程单上每一站都写好了“后悔药配方”:机票可退、酒店可取消、门票可改签。出了问题不用重新规划,只要按反方向把药吃一遍,旅行计划就干干净净地回到原点。
- 正向事务\(T_i\) 是第 \(i\) 步的正常业务操作,比如“订机票扣款出票”,它必须是一个已提交的本地事务。
- 配套补偿\(C_i\) 是与 \(T_i\) 成对的补偿操作,专门负责撤销 \(T_i\) 已产生的业务影响,比如退票退款。
- 成对有序集合整个 Saga 是 n 对“正向-补偿”的有序组合,按 \(T_1 \to T_2 \to \cdots \to T_n\) 顺序执行,失败时按 \(C_{i-1} \to \cdots \to C_1\) 逆序补偿。
3. 编排式实现
编排式像跟团游:旅行社派出一位总导游,手里攥着完整行程单。导游先给航空公司打电话订票,收到“成功”答复后,再给酒店打电话……每个服务商只跟导游一个人对接,不需要知道其他服务商是谁。
编排式 Saga 的核心是一个中央协调器(Orchestrator)——通常是一个独立服务或状态机。协调器持有整个流程的状态,逐个调用各服务的本地事务接口,根据返回结果决定下一步:成功就调用下一个服务,失败就触发逆序补偿。所有服务被动执行,流程集中可见,日志从协调器一处就能串出全貌。
协调器的本质是一个状态机:当前状态 \(s_t\) 记录“走到哪一步”,收到服务返回的事件 \(e_t\) 后,状态转移函数算出新状态,决策函数再决定下发什么指令。这组关系可以写成:
导游打完一圈电话,每通都有明确答复:“行”或“不行”。不行?导游立刻翻出退改政策,火速打电话把前面几站全部取消。全程你只需要盯住导游一个人——这就是编排式“集中控制”的价值。
- 当前状态\(s_t\) 表示协调器在第 \(t\) 步所处的流程节点,例如“机票已订、待订酒店”。
- 事件输入\(e_t\) 是刚返回的结果事件,取值通常是成功或失败,例如“酒店确认”或“酒店满房”。
- 状态推进状态转移函数 \(f\) 把 \((s_t, e_t)\) 映射到下一个状态 \(s_{t+1}\),决定流程走到了哪。
- 决策下发决策函数 \(g\) 根据当前状态 \(s_t\) 算出动作 \(a_t\),即调用哪个服务、还是启动补偿流程。
4. 协同式实现
协同式则是自由行:没有导游,你订完机票,航空公司系统自动向酒店平台发一封“邮件”(事件):旅客航班已定,请预留房间。酒店确认后,又自动通知景区售票处……每一站只认识下一站,大家靠事件接力推进。
协同式 Saga 的核心是事件驱动与发布/订阅。每个服务完成自己的本地事务后,向消息总线发布一个领域事件(如“机票已出”);关心该事件的下游服务订阅并消费它,继续自己的本地事务。流程没有中央控制器,而是由一串事件自然串联。链条可以表示为:
协同式的最大优点是去中心化:没有单点故障,服务间松耦合,新增一个服务只需订阅相关事件。但代价是流程隐式化——全局状态藏在每个服务各自的事件日志里,想确认“旅行走到哪了”得把所有日志拼起来。接力棒一旦掉在地上(事件丢失),没人能一眼看出问题出在哪一棒。
- 本地完成\(T_i\) 表示第 \(i\) 个服务成功提交了自己的本地事务,比如机票真正出票成功。
- 发布事件\(E_i\) 是 \(T_i\) 完成后向外广播的领域事件,比如“机票已出,航班号 XXX”。
- 事件触发链箭头表示订阅了 \(E_i\) 的 \(T_{i+1}\) 被事件触发而执行,整条流程由事件串成接力链。
5. 对比与选型
跟团游和自由行各有拥趸:跟团省心但行程死板,自由行灵活但全靠自觉。选哪种,取决于你的团队(服务)和路线(流程)的脾气。
对比可以从三个维度看:调用关系复杂度、故障处理方式、流程可见性。编排式有明确的“总导游”,协同式只有模糊的“接力链”。调用链路上,编排式每个服务只跟协调器通信,共 n 条链路;协同式最坏情况下服务间互相引用,链路数可达 \(O(n^2)\)。故障处理上,编排式的补偿逻辑集中在协调器,统一逆序触发;协同式的补偿靠事件往回传,各服务自行实现,状态容易分叉。
选型建议:流程经常变化、涉及团队多、需要全局审计的,选编排式;流程简单线性、团队自治意愿强、想避免单点故障的,选协同式。三五好友自由行毫无压力,上百人的研学团还玩接力传单必然乱套——必须出个总导游。
- 编排式链路\(O(n)\) 读作“线性增长”,n 个服务只需 n 条“服务→协调器”连接,关系一目了然。
- 协同式链路\(O(n^2)\) 说明最坏情况下服务间互相发订阅,连接数随 n 平方膨胀,关系网越织越乱。
- 规模放大差异当 n 很大时,平方级远大于线性级,协同式的关系网会变得难以维护。
6. 落地实践
不管跟团还是自由行,退订都必须干净利落,否则钱会打水漂。落地 Saga,最见功力的其实是“后手”——补偿、重试和状态恢复。
总结一线实践有四个关键词。第一,补偿要业务化:补偿不是数据库回滚,而是业务反向操作(退票、退款、释放库存)。第二,重试要幂等:补偿和正向操作都要设计成幂等,重复执行 n 次结果一致,否则重试一次就把用户的钱退两次。第三,状态要持久化:协调器要把流程状态落库,重启后从断点恢复,而不是从头再来。第四,失败要对账兜底:重试要有上限和指数退避,最终靠定时对账任务发现“僵尸流程”并人工介入。重试 k 次的累计成功率会快速逼近 1:
每一张订单、每一次退改,都记在旅行社的小本本上。小本本在,就算导游半路换了人,新导游也能照着重来,绝不让同一个订单被退两遍钱——这就是状态持久化与幂等设计的意义。
- 单次成功率\(p\) 表示一次补偿操作成功完成的概率,例如一次退票调用的成功率。
- 连续失败概率\((1-p)^k\) 表示连续 k 次都失败的可能性,k 越大这个值越小。
- 累计成功率\(P_k = 1 - (1-p)^k\) 是重试最多 k 次后至少成功一次的概率,随 k 增大趋近 1。
- 幂等性质\(f(f(x)) = f(x)\) 表示同一操作执行两次与执行一次效果相同,这是重试不会造成双倍扣款的前提。
AI 应用场景
电商下单全链路编排
阶段:跨服务业务编排
解决的问题:传统下单要同时扣减库存、创建订单、核销优惠券,这三步分属不同服务,任何一个失败都会造成超卖或脏数据;用编排式 Saga 协调器统一调度,任一环节失败自动逆序补偿。
具体案例:大促期间库存服务超时,协调器收到失败事件后,依次触发“释放优惠券、取消订单”补偿,用户侧最终看到“订单失败已自动退款”,全程无需人工介入。
旅游平台一站式打包预订
阶段:跨服务状态管理
解决的问题:用户一次预订“机票+酒店+门票”套餐,涉及多个外部供应商对接,门票等上游随时可能失败;协同式 Saga 让各服务订阅“订单创建”事件自主处理,失败通过事件链反向取消。
具体案例:用户下单后订单服务发布事件,酒店与门票服务各自订阅完成预订;门票服务因景区关闭取消并发布“取消”事件,酒店服务收到后退订,用户收到全额退款通知。
资金转账与多方记账
阶段:事件驱动集成与最终一致性保障
解决的问题:转账涉及扣款方账户、收款方账户、流水记账三个服务,无法用强一致锁库;用协同式 Saga 以事件串联,靠消息队列持久化保证事件不丢。
具体案例:扣款服务更新余额后发布“扣款成功”事件,记账服务消费后写流水;若记账服务消费失败,通过有限重试和定时对账任务兜底,确保账实相符。
Python 演示
# -*- coding: utf-8 -*-"""编排式 Saga 模拟器:总导游逐站安排,失败则反向补偿""" class SagaCoordinator: """中央协调器:按序执行正向事务,失败时逆序触发补偿""" def __init__(self): self.executed = [] def execute(self, saga): for step in saga: try: step.forward() self.executed.append(step) except Exception as e: print("[失败]" + step.name + ": " + str(e)) print("开始反向补偿:") while self.executed: self.executed.pop().compensate() print("最终状态: 全部撤销, 行程取消") return False print("最终状态: 全部成功, 旅行成行") return True class Step: """一个 Saga 步骤: 正向事务 + 补偿操作""" def __init__(self, name, forward, compensate): self.name = name self._forward = forward self.compensate = compensate def forward(self): print("[执行]" + self.name) self._forward() def book_flight(): print(" 航班出票成功") def book_hotel(): print(" 酒店确认成功") def book_sight(): raise RuntimeError("景区当日闭园, 门票预订失败") def book_restaurant(): print(" 餐厅订位成功") SAGA = [ Step("订机票", book_flight, lambda: print(" 已退机票, 票款原路退回")), Step("订酒店", book_hotel, lambda: print(" 已退酒店, 房费原路退回")), Step("订门票", book_sight, lambda: print(" 本次无需补偿(未成功)")), Step("订餐厅", book_restaurant, lambda: print(" 已退餐厅, 订位取消")),] if __name__ == "__main__": SagaCoordinator().execute(SAGA)
class SagaCoordinator:对应编排式里的“总导游”,持有已执行步骤清单self.executed = []记录已成功的正向步骤,出问题才知道该退谁step.forward()逐一执行正向本地事务self.executed.append(step)每成功一步就记一笔账,对应状态机前进while self.executed:失败时循环弹出已成功步骤self.executed.pop().compensate()“后进先出”逐项撤销,正好对应退改政策的逆序执行class Step:把“正向事务 + 补偿操作”打包成一对,即公式里的 \((T_i, C_i)\)if __name__ == "__main__":只有直接运行脚本时才执行演示,防止被导入时误触发
常见误区
误区学完 Saga 觉得它和数据库事务一样,要么全成功要么全失败,任何时刻都看不到中间状态。机票和酒店应该像被一根线拴着,一荣俱荣一损俱损。
正解Saga 是最终一致性方案,中间状态对外可见——机票可以已出票、酒店还未确认。它的保证是“终态确定”:要么全部正向成功,要么全部补偿完成。就像旅行可以先出机票再慢慢订酒店,只要最终行程确定或全部退款到账即可。
为何常见:人们对 ACID 事务“原子提交”的直觉根深蒂固,容易把 Saga 的成功/补偿误当成同时发生的原子操作,忽略了它的精髓是“最终收敛”。
误区补偿操作就是调用 ROLLBACK,把数据库恢复到操作前的状态,删掉那条订房记录就行。
正解补偿是业务级的反向操作,不是数据库回滚。机票扣款已发生并可能已出票,“退票并退款”是一次全新的业务交易,要调用航司的退款接口,而不是直接删库里的订单行。酒店取消也要走退房费流程,账目上要留下清晰流水。
为何常见:“补偿”这个词容易让人联想到数据库 undo 日志,忽略了分布式环境下每个服务的数据已经独立落库,只能用业务动作去对冲,不能物理撤销。
误区协同式不用写协调器,每个服务发个事件就行,代码量明显更少,所以怎么算都比编排式简单。
正解开发初期协同式确实少写一个协调器,但流程是隐式的,散落在每个服务的订阅逻辑里。排障时要逐个服务翻日志才能拼出全貌,测试也要构造完整事件链。编排式代码虽多,但流程在协调器里一目了然,长期维护成本反而低。
为何常见:只看到“少写一个协调器”的表面收益,忽略了可观测性、排障和测试这些隐性成本,属于“省了开头的钱,欠下后期的债”。
误区消息发到队列里就万事大吉,下游服务总会收到并处理,事件不会丢也不会乱。
正解事件可能丢失、重复、乱序,下游消费也可能有 bug。协同式 Saga 必须配套事件持久化、消费幂等、重试与定时对账。接力棒掉在地上,必须有人能捡起来重新传递,否则整条流程会永远卡死。
为何常见:把消息队列默认成“绝对可靠”的送达管道,忽略了分布式环境下的消息语义(至少一次/恰好一次)问题,这是协同式事故的高发区。
误区补偿失败就一遍遍重试,多试几次总能成功,反正调接口又不用花钱。
正解无限重试会浪费资源、放大流量,还可能带来副作用。正确做法是:有限次重试 + 指数退避 + 人工介入/对账兜底;且重试必须幂等,否则重复退票会把用户的钱退两次。旅行中的“改签失败”也不会无限尝试,而是转到人工柜台处理。
为何常见:直觉上“多试几次总能行”没错,但没意识到重试会重复执行业务动作,忘了给操作加上幂等锁。
面试题
Q1. Saga 模式解决的核心问题是什么?
- A.提高微服务查询性能
- B.跨服务的最终一致性← 正确答案
- C.跨服务的强一致性
- D.简化服务接口设计
答案:B。Saga 是微服务里做跨服务数据一致性的经典模式,核心是“本地事务+补偿”实现最终一致性,而不是强一致(那是 2PC 的目标)。就像跨城旅行:票都能退(补偿),最终要么全成行要么全取消,中途允许部分确定。
Q2. 编排式 Saga 中,由谁负责协调各服务的执行顺序?
- A.各服务自行通过事件通信
- B.中央协调器统一调度下发← 正确答案
- C.消息队列自动完成分发
- D.数据库触发器自动执行
答案:B。编排式 Saga 由中央协调器(总导游)统一调度,各服务被动执行、不感知全局。选项 A 是协同式的特征,C 和 D 与编排式的协调机制无关。
Q3. 下列哪一项是协同式 Saga 的典型特征?
- A.依赖中央状态机驱动流程
- B.服务间通过事件驱动衔接← 正确答案
- C.需要额外的协调器节点
- D.所有服务共享同一数据库
答案:B。协同式没有中央控制器,靠服务间发布/订阅事件推进,像自由行的接力传单。A 和 C 是编排式特征,D 违背了微服务“数据自治”的基本原则。
Q4. 补偿事务 Cᵢ 与正向事务 Tᵢ 的正确关系是?
- A.在前向事务执行之前启动
- B.撤销前向事务已生效的影响← 正确答案
- C.与前向事务做完全相同的事
- D.只在并发冲突发生时才执行
答案:B。Cᵢ 是 Tᵢ 配套的反向业务操作,撤销 Tᵢ 已生效的业务影响,就像退票是出票的“后悔药”。它不是预先执行、不是重复执行,也不是只处理并发冲突。
Q5. 关于 2PC 与 Saga 的对比,下列说法正确的有哪些?
- A.二阶段提交要求参与者全程同步等待← 正确答案
- B.Saga 通过补偿实现最终一致性← 正确答案
- C.二阶段提交在分区故障时依然可用
- D.Saga 允许中间状态短暂不一致← 正确答案
答案:ABD。2PC 第一阶段预提交时参与者要锁资源同步等待(A 对);Saga 用补偿换最终一致性(B 对);2PC 在分区时为保证一致性牺牲可用性,并非“依然可用”(C 错);Saga 过程允许中间状态(D 对)。
Q6. 协同式 Saga 中事件丢失可能导致什么后果?
- A.自动触发下一服务继续推进
- B.流程停滞且无人知晓原因← 正确答案
- C.协调器重启即可自动恢复
- D.数据库自动回滚全部事务
答案:B。协同式靠事件接力,事件丢失后下游毫不知情,流程卡死且难以发现——接力棒掉地上没人捡。A 是正常情况而非丢失后果;协同式没有协调器(C 错);事件丢失也不会自动回滚数据库(D 错)。
Q7. 关于编排式与协同式的适用场景,下列判断正确的有哪些?
- A.业务规则频繁变化适合编排式← 正确答案
- B.服务团队高度自治适合协同式← 正确答案
- C.简单直链流程适合编排式
- D.避免单点故障宜选协同式← 正确答案
答案:ABD。流程频繁变化、多团队协作时编排式集中可控(A 对);团队自治、想避免协调器单点选协同式(B、D 对);简单直链流程通常更适合协同式而非编排式(C 错),少一个协调器反而更轻快。
Q8. 为支持安全重试,补偿操作必须满足的关键性质是什么?
- A.原子性:补偿要么全做要么不做
- B.幂等性:重复执行结果完全相同← 正确答案
- C.可逆性:恢复到事务执行前状态
- D.一致性:补偿结果立即全局可见
答案:B。补偿会被重试反复触发,必须幂等——重复执行 n 次与执行一次结果一致,否则会把用户的钱退两次。可逆/原子/可见都不是补偿的核心要求;补偿是业务反向操作而非状态回滚。
Q9. 关于 Saga 状态管理,下列正确做法有哪些?
- A.只需将状态存在协调器内存
- B.将流程状态持久化到数据库← 正确答案
- C.重启后从持久化状态恢复← 正确答案
- D.状态不落盘以换取更高性能
答案:BC。协调器状态必须持久化,重启后从断点恢复流程(B、C 对)。只放内存(A)一重启就丢,不落盘(D)无法保证可靠恢复——就像行程单必须写在小本本上,不能只靠导游脑子记。
Q10. 一个包含 n 个服务的 Saga,编排式协调器维护的调用关系复杂度通常是什么?
- A.常数级:永远只需保持一条连接
- B.线性级:与每个服务各建一条链路← 正确答案
- C.平方级:服务之间全部互相直连
- D.对数级:随服务数量对数增长
答案:B。编排式每个服务只与协调器一条链路,共 n 条,复杂度 O(n);协同式最坏情况下服务间互相发布订阅,链路可达 O(n²)。这是两者耦合度差异的量化体现,也是选型的重要依据。