想象你带着全家五口人周末出游:先在某平台订机票,再在另一个App订酒店,最后还要抢景区门票。每个环节单独看都不难,但它们环环相扣——机票订好了、酒店却没房了,怎么办?老派的办法是:让三个商家同时锁定资源,确认都有货再一起扣款。可一旦中间断网,所有座位和房间都得一直被占着,别人也订不了,全家就在“等待”里干熬。Saga模式给出了另一种思路:不用同时锁定,一项一项来——订完机票订酒店,订完酒店订门票;哪个环节失败了,就把之前订好的逐一退掉,谁也不用干等。这就是分布式事务领域的“旅行团式解决方案”,用拆解替代锁定,用补偿替代回滚,最终让全家依然顺利成行或全身而退。
掌握Saga模式,是微服务架构师处理跨服务数据一致性的核心技能,能帮你在保证系统高可用的前提下优雅解决分布式事务难题。
可视化演示
动态演示Saga模式处理分布式事务的核心机制,通过采集/分析/决策/反馈四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Saga模式处理分布式事务:Saga Saga模式核心理念与分布式事 编排式与协同式Saga对比 核心机制可视化 Saga模式处理分布式事务整体架构与关键组件
Saga概念解析
全家旅行团的第一课,叫“别再开家庭会议”。过去订机票、酒店、门票,总想全家凑齐了一次性同步搞定——这就是传统分布式事务的“举手表决”思路。Saga模式反其道而行:把一段复杂的全家旅行,拆成“订机票→订酒店→订门票”三个独立小项目,每完成一项就签收一项,谁也不用等谁。这种拆解的智慧,正是Saga解决分布式事务的起点。
从技术角度看,Saga模式把一个跨多个微服务的“大事务”,拆分成一系列有先后顺序的“本地事务”(Local Transaction)。每个本地事务都在各自的微服务内独立提交,立刻释放数据库锁资源。整条链路上的事务按顺序执行,一旦某个本地事务执行失败,Saga就会调用之前已完成事务的补偿操作,让业务状态一步步回到起点。
这里的关键在于“Saga只保证最终一致性,而非强一致性”。传统事务要求任何时刻所有节点状态完全一致,Saga则允许中间过程存在短暂的不一致——比如机票已订而酒店未订——只要最终要么全部成功、要么全部撤销即可。下面是Saga的结构化表达:
表示第 $i$ 个正向本地事务
表示第 $i$ 个补偿事务
表示两者的并集组合
回到旅行团的例子:Saga让“订机票”和“订酒店”变成两个独立签约的小合同,而不是一个必须同时生效的生死契约。就算酒店没订上,退掉机票就能重新出发——全家不会被困在“等待所有家人都同意”的僵局里。这就是Saga的核心哲学:用可拆解的行动替代不可退让的承诺。
- Saga整体定义Saga由两组事务共同构成,一组负责前进,一组负责善后。
- 正向事务Ti每个T是微服务内的本地事务,例如“订单服务创建订单”。
- 补偿事务Ci每个C与对应的T配对,执行反向业务操作,例如“取消订单”。
- 并集关系正向与补偿共同构成完整的Saga定义,缺一不可。
编排式Saga
如果说Saga是把旅行拆成了小项目,那“编排式”就是给这个团配了一位全职导游。导游手里攥着一份详细行程单,他的职责是告诉每个家庭成员:“现在你去订机票”“接下来你去订酒店”“最后你去买门票”。所有人只听导游安排,互相之间不需要沟通。这位导游,就是编排式Saga里的中央协调者(Orchestrator)。
在编排式Saga中,Orchestrator是一个独立服务,负责定义并驱动整个事务流程。它通过命令或RPC调用各个参与服务,等待每个服务返回结果后,再决定下一步动作。如果某个服务返回失败,Orchestrator会依据预设的流程,反向调用已成功服务的补偿接口。所有流程状态都集中在协调者内部,因此链路清晰、易于监控和恢复。
编排式Saga的状态流转可以用下面这个公式概括:协调者总是根据“当前状态+最新事件”计算出下一个状态。整个过程像一个有限状态机,每个阶段都由协调者显式推进,而不是让服务之间互相“商量”。
表示第 $k$ 步的流程状态
表示第 $k$ 步收到的服务反馈
是中央协调者的决策逻辑
表示递推得到的下一状态
导游模式的优点是显而易见的:所有决策都集中在一处,团队不会乱。但在技术世界里,这位“导游”也承担了巨大的压力——他必须清楚每一个景点的规则,一旦行程变更,所有调整都压在他一个人身上。这也是编排式Saga的代价:协调者本身成为高可用设计的关键节点。
- 当前状态State_k表示Saga执行到第k步时的进度,比如“已订机票”。
- 输入事件Event_k参与服务返回的结果,如“酒店预订成功”或“支付失败”。
- 协调者决策函数Orchestrator是一个决策器,输入上述两者输出新状态。
- 状态递推新状态成为下一步的输入,形成可追踪的状态链。
协同式Saga
这次不带导游了。全家自由行,爸爸负责订机票,订完在家庭群里发一条消息:“机票搞定!”妈妈看到消息后去订酒店,订完再发一条:“酒店OK!”儿子看到后去买门票。每个人只关心“上一个事件”和“自己的任务”,谁也不需要盯着别人干活。这种靠消息接力推进的方式,就是协同式Saga(Choreography)。
协同式Saga没有中心协调者,各服务之间通过消息队列或事件总线解耦。每个服务在完成自己的本地事务后,发布一个领域事件;下游服务订阅该事件,如果自己的事务执行失败,则发布一个失败事件,之前已完成事务的服务在收到失败事件后触发各自的补偿操作。整个流程“隐藏”在事件流里,天然地实现了服务间的松耦合。
用公式表达,协同式Saga中下一个事务的触发条件,是上一个事务发出的事件。服务之间不依赖“导游”,而是依赖“消息”这封接力信:
表示服务 $i$ 发出的事务事件
表示服务的订阅响应逻辑
表示下一个被触发的本地事务
自由行的好处是每个人都很轻松,谁也不用当总指挥;但坏处是一旦环节变多,链条会变得难以追踪——你想知道全家现在走到哪一步了,得翻遍所有聊天记录。协同式Saga也是如此:它极致灵活、去中心化、扩展性极强,但流程的可观测性较弱,适合参与方少、链路固定的场景。选择导游还是自由行,取决于你的团队规模和可管控性需求。
- 上游事件Event_i第i个服务完成或失败后发布的领域事件。
- 触发函数每个服务内部定义了事件处理逻辑,收到事件后开始执行自己的事务。
- 事务接力第i+1个事务成为新的消息发布者,继续传递接力棒。
补偿操作设计
旅行中最怕什么?怕酒店超卖。到了前台才发现没房,这时候该怎么办?全家老小在酒店大堂干等着,肯定不行——得赶紧启动“善后模式”:取消没住的酒店,退掉可能订好的接送机,如果机票也能退就一起退掉,让全家毫发无损地回到原计划。这一连串的“退订操作”,就是Saga模式中的补偿事务(Compensating Transaction)。
设计补偿操作时,最重要的一条原则是:补偿不是数据库回滚,而是业务层面的反向操作。数据库回滚是把磁盘上的记录“抹掉”,仿佛没发生过;而补偿是执行一个新的业务动作,比如“取消订单”“退款”“释放库存”。这意味着你需要在每个参与服务中,额外开发一套可调用的补偿接口——就像每个景点都要有一个“退票窗口”。
补偿的执行顺序必须与正向顺序严格相反。比如正向是“订机票→订酒店→订门票”,如果订门票失败,补偿顺序就是“退门票(未执行不需要)→退酒店→退机票”,写成序列就是:
表示第 $i-1$ 个事务的补偿操作
表示继续向前回溯的补偿序列
组成完整补偿集合
补偿设计得好不好,决定了Saga在真实世界能不能落地。就像酒店退订,如果对方要求你提供“入住人身份证原件”才能退款,那这个补偿流程就没法自动化,业务上就卡壳了。设计补偿时必须站在业务角度思考:这个反操作在当前业务规则下是否可行、是否需要人工介入、是否可能失败——这些都是在画Saga流程图之前就要想清楚的。
- 失败锚点Ci-1补偿序列从失败节点的前一个事务开始回退。
- 逆序原则补偿严格按正向执行的相反顺序进行,保证业务依赖链条不被破坏。
- 完成的边界只有已经成功提交的事务才需要补偿。
幂等控制策略
旅行团到了酒店前台。退房时,工作人员说押金已退还到你的账户,但你没收到短信。你追问了一句,工作人员又操作了一次退款——结果到账了两笔押金。谁都没做错,但钱多给了。这时候,前台系统最需要什么?需要“幂等”:同一个退款请求,无论执行多少次,结果都只能有一个。
幂等(Idempotency)是Saga中补偿操作和重试机制的安全底座。Saga执行过程中,网络超时、机器宕机都可能导致同一个请求被发送多次;补偿操作也可能因为异常被重复触发。如果接口不幂等,重复的退款、重复的扣库存、重复的取消订单就会污染数据。解决方案通常是:为每个请求分配一个唯一业务键(如订单号、事务ID),服务端记录已处理过的键并直接返回原结果。
用数学语言描述,一个操作f是幂等的,意味着对同一个输入x执行两次和执行一次结果完全相同。这正是重试机制敢放心大胆地“再试一次”的前提。在Saga里,无论是正向事务还是补偿事务,接口设计都应该遵守这个性质:
表示任意一个业务操作
表示请求的唯一标识
表示操作被连续执行两次
说明执行多次等价于执行一次
幂等性就像给每个业务动作装上一道“闸门”:同一个请求进来,第二次直接放行不处理。有了这道闸门,Saga中的补偿操作才敢放心重试、超时恢复时也敢重放消息。记住:没有幂等机制的Saga,就像一个忘记确认退押金次数的酒店前台——不是会不会出错,而是迟早会出错。
- 操作函数f代表某个业务接口,例如“退款接口”。
- 输入x代表请求参数,如唯一的退款单号。
- 执行两次操作重复执行时,第二次会因唯一键冲突而返回第一次的结果。
- 幂等条件两次执行的结果与执行一次完全相同。
协议对比选型
现在你已经见识了三套旅行方案。老派方案“开家庭大会”:全家围坐一圈,必须每个人都拿到确认单才允许行动,有任何一个人犹豫就全部搁置——这是两阶段提交(2PC)。进阶方案“先交定金占位”:每个环节先预留名额,等全都确认后统一转正,有谁掉链子就释放所有预留——这是TCC。今天的主角Saga则是“逐项签约、随时可退”:一项项办、一项项退,绝不长时间占着资源不放。
三套方案各有代价。2PC能提供最强的实时一致性,但需要占用资源直到事务结束,网络分区或节点故障时会长时间阻塞。TCC通过预留资源实现了业务层面的强隔离,但需要业务方实现Try/Confirm/Cancel三个方法,开发成本高、侵入性强。Saga则把长事务分成短事务,每个短事务立即提交并释放资源,用补偿换取可用性,代价是中间状态对外可见、隔离性偏弱。三者的关系可以用一个简明的谱系来表达:
表示一致性强度递减
表示可用性递增
强一致必然牺牲可用性
所以怎么选?如果业务像银行转账一样需要实时强一致、事务链路短,2PC依然是合适的;如果业务链路中等、愿意投入开发实现Try/Confirm/Cancel,TCC能带来更好的隔离性;如果业务链路长、服务多、追求高吞吐和弹性,比如电商下单要经过订单→支付→库存→物流,Saga才是最优解。选型不是攀比谁更先进,而是看谁最能匹配你的业务代价承受力。
- 一致性排序2PC在同一时刻对所有节点可见,TCC通过占位基本保证,Saga只保证最终。
- 可用性排序Saga事务短、不锁资源、不怕长阻塞,所以可用性最高。
- 工程取舍两项排序方向相反,说明不存在“全能方案”,只有匹配场景的方案。
Seata实践
讲了这么多理论,有没有一个工具能把“导游”“补偿”“幂等”三件事都管起来?开源分布式事务框架Seata给出了回答:它内置了Saga状态机引擎,相当于一台“旅行规划App”。你只要在页面上画出行程步骤——第一步做什么、失败了去哪退订——引擎就会自动按图执行,并帮你管理补偿链路的调用。
Seata的Saga实现特别适合编排式场景。开发者通过JSON或Groovy定义状态机,每个状态是一个服务调用,可以配置成功下一个状态、失败的下一个状态以及补偿动作。状态机执行到失败节点时会自动回溯,用户不需要手写“谁来谁退”的流程代码。Seata服务端还负责事务日志、状态的持久化和恢复,配合控制台可以查看每一步的执行情况。
Seata状态机的核心机制可以用状态转移公式理解:每个状态的去向由当前状态和反馈结果共同决定,引擎根据这个转移函数驱动流程前进或退出。这正是前面编排式Saga的“导游”逻辑的工程化实现:
表示状态机当前停留的节点
表示服务执行结果
表示状态转移规则
表示根据规则计算出的目标状态
有了Seata,Saga的落地门槛被大幅降低——你不用从零搭建协调者,也不用自己设计补偿调度。就像旅行App把“退票窗口”搬到了手机里,一切流程可视化、可追踪。当然,工具只是助手,真正的业务补偿逻辑和幂等设计仍然需要每个微服务自己实现。工具帮你把流程串起来,但让流程每一步都走得稳,靠的还是我们今天讲的所有基本功。
- 当前状态S_current例如“订单创建完成”或“支付处理中”。
- 执行结果Result服务调用的返回值,成功或失败。
- 转移函数δ状态机的核心表,定义每种“状态+结果”组合的跳转方向。
- 下一状态S_next可能是下一个正向节点,也可能是补偿节点或结束节点。
AI 应用场景
电商平台订单全链路
阶段:跨服务数据一致性保障
解决的问题:用户在电商App下单后,系统要依次创建订单、扣减库存、扣除优惠券、生成物流单。传统做法要么用数据库强事务锁死所有表(性能差),要么各服务独立提交(数据不一致)。Saga将整个下单链路编排成一系列本地事务,任何一步库存不足,就自动重新释放优惠券并将订单作废,保证全链路最终一致。
具体案例:双十一大促高峰期,用户在购物车提交了5件商品。订单服务创建订单成功,支付服务扣款成功,库存服务发现其中一件已售罄。此时Saga协调者依次触发“释放支付扣款”“撤销订单”两个补偿操作,用户看到“因库存不足,订单已取消并退款”的提示,整个流程在2秒内完成回滚。
跨行转账聚合支付
阶段:分布式事务恢复与幂等控制
解决的问题:聚合支付平台处理“用户A银行账户→平台中间账户→商家B银行账户”的两段式转账,涉及两个不同银行系统的协作,不可能用数据库本地事务。Saga先执行“扣减A行余额”,再执行“增加B行余额”;若B行入账失败,则执行补偿操作“退回A行余额”,同时依赖幂等控制避免重复退钱。
具体案例:某平台用户在6月18日凌晨发起一笔5万元跨行转账,B行系统因升级短暂不可用,入账失败。Saga状态机自动触发补偿,A行余额在3秒内原路退回;用户次日收到“转账未成功,资金已退回”的短信通知,整个回退过程无需人工介入。
Python 演示
class Service: def __init__(self, name): self.name = name def execute(self, tx_id): print("[%s] 执行正向事务 %s" % (self.name, tx_id)) return True def compensate(self, tx_id): print("[%s] 执行补偿操作 %s" % (self.name, tx_id)) class StockService(Service): def execute(self, tx_id): print("[%s] 执行正向事务 %s" % (self.name, tx_id)) # 模拟库存不足导致事务失败 if tx_id == "T002": print("[%s] 库存不足, 事务失败!" % self.name) return False return True class SagaCoordinator: def __init__(self, services): self.services = services self.done = [] def run(self, tx_id): print("=== Saga 编排开始: %s ===" % tx_id) for svc in self.services: if svc.execute(tx_id): self.done.append(svc) else: print("--- %s 失败, 开始逆向补偿 ---" % svc.name) for s in reversed(self.done): s.compensate(tx_id) return False print("所有事务执行成功") return True if __name__ == "__main__": order = Service("订单服务") pay = Service("支付服务") stock = StockService("库存服务") saga = SagaCoordinator([order, pay, stock]) print("\n--- 正常流程演示 ---") saga.run("T001") print("\n--- 失败补偿演示 ---") saga.run("T002")
class SagaCoordinator:定义中央协调者,负责按顺序驱动各服务事务。self.done = []记录已成功执行的服务名单,作为补偿的依据。if svc.execute(tx_id):依次调用每个服务的正向事务,检查是否成功。for s in reversed(self.done):失败时对已完成服务按逆序调用补偿操作。class StockService(Service):继承并重写执行逻辑,模拟库存不足的业务失败。
常见误区
误区用Saga模式后就可以高枕无忧,任何时刻读到的数据都是最新最准的,跟单机数据库事务一样可靠。
正解Saga是最终一致性方案。正向事务执行期间,各服务之间会有一段“账对不上”的窗口期。就像旅行中机票已买而酒店未定时,全家的行程还在变化中。它牺牲了强一致,换来了高可用和低资源占用。如果业务需要实时一致性(如余额查询),Saga并不合适。
为何常见:非专业人士容易把“分布式事务”笼统理解为“保证一致”,忽略了ACID与BASE之间的本质差异。
误区Saga执行失败时,系统会自动把数据库里的数据“还原”成事务开始之前的样子,像什么都没发生过一样。
正解补偿是业务层面的反向操作,不是物理回滚。比如“支付服务扣款成功”,补偿不是删掉那条扣款记录,而是新增一条“退款”记录。就像酒店退订,不会把你的入住记录凭空抹去,而是生成一条“取消预订”的新记录。设计补偿时必须从业务规则出发,确保反向操作在业务上说得通。
为何常见:受数据库事务的"UNDO"概念影响,想当然地认为所有回滚都该由底层机制自动完成。
误区只要给每个请求加上一个唯一ID,接口就自动幂等了,重试和补偿随便发。
正解唯一键只是幂等的前提条件之一。服务端还必须做“已处理记录”的落库校验、状态机流转校验、以及并发场景下的唯一索引兜底。如果服务收到重复请求但还没来得及写入记录就宕机了,重试依然可能重复处理。幂等是一个完整的防御体系,不是一行"SET UNIQUE"就能解决的。
为何常见:把数据库唯一约束的机制等同于完整的幂等方案,忽略了分布式环境下“检查-执行”之间的时间窗问题。
误区既然编排式有中央协调者,流程清晰可控,那所有Saga都应该用编排式实现,协同式过时了。
正解编排式和协同式各有适用边界。编排式把流程集中到一个协调者,协调者可能成为性能瓶颈和单点故障;协同式完全去中心化,新增服务只要订阅事件就能接入,扩展性更强。就像大旅行团需要导游带队,但三五好友自由行时导游反而碍事。选型要结合团队规模、链路长度、可观测性需求综合判断。
为何常见:拿“有无中心控制”这一维度简单判断优劣,忽略了高可用、可扩展、可维护等多个工程维度的权衡。
面试题
Q1. Saga模式将长事务拆分成多个什么来执行?
- A.全局锁任务
- B.本地独立事务← 正确答案
- C.分布式表备份
- D.消息队列任务
答案:B。Saga的核心思想是把跨服务的全局事务拆成一系列微服务内的本地事务,每个本地事务独立提交并释放资源。全局锁任务、表备份、消息队列都不是Saga拆分的基本单位。
Q2. Saga模式最终实现的是哪种一致性?
- A.强一致性
- B.最终一致性← 正确答案
- C.实时一致性
- D.读写一致性
答案:B。Saga允许中间状态短暂不一致,只要最终所有事务成功或全部补偿即可,因此它保证的是最终一致性。强一致性需要2PC或TCC这类资源预留型方案。
Q3. 编排式Saga的典型特征有哪些?
- A.中央协调者统一调度← 正确答案
- B.服务间事件驱动交互
- C.流程定义集中管理← 正确答案
- D.无需额外协调组件
答案:AC。编排式Saga有一个独立的中央协调者,集中定义流程并调度各服务,因此A和C正确。事件驱动交互和无需协调组件是协同式Saga的特征。
Q4. 在Saga补偿流程中,补偿事务的执行顺序是?
- A.正向顺序
- B.逆序执行← 正确答案
- C.随机顺序
- D.并行执行
答案:B。补偿必须严格按正向事务的逆序执行,就像退订酒店要先于退订机票一样。因为后序事务往往依赖前序事务的结果,只有逆序回退才能保证业务链路不被破坏。
Q5. 实现分布式事务接口幂等性的常用策略有?
- A.唯一业务键约束← 正确答案
- B.状态机流转校验← 正确答案
- C.反范式冗余存储
- D.全链路日志追踪
答案:AB。唯一业务键让重复请求命中同一记录,状态机校验确保只有合法状态的请求才会被处理,这两者是幂等的主要手段。冗余存储和日志追踪不直接提供幂等保障。
Q6. 与两阶段提交相比,Saga模式的显著优势是?
- A.更好的全局锁性能
- B.更高的系统可用性← 正确答案
- C.更简单的回滚逻辑
- D.更强的隔离性保证
答案:B。Saga每个本地事务立即提交并释放资源,不长时间占用锁,因此在长事务场景下可用性显著高于2PC。2PC才是真正意义上的全局锁。Saga的回滚逻辑和隔离性通常比2PC更复杂或更弱。
Q7. Seata Saga状态机中,Compensate节点的作用是什么?
- A.提交所有正向事务
- B.执行逆向补偿逻辑← 正确答案
- C.同步阻塞等待结果
- D.分配分布式锁资源
答案:B。Compensate节点是状态机中专门执行补偿操作的节点。当某一步正向服务调用失败时,状态机自动调用之前成功步骤的Compensate节点完成补偿。它负责逆向业务逻辑,而非提交事务或分配锁资源。
Q8. 关于Saga与TCC的区别,下列说法正确的是?
- A.TCC需业务方实现三方法← 正确答案
- B.Saga补偿依赖数据库回滚
- C.TCC通常比Saga隔离性更强← 正确答案
- D.Saga实现一定比TCC更简单
答案:AC。TCC要求业务方实现Try/Confirm/Cancel三个方法,A正确。TCC通过预留资源保证更强隔离性,C正确。Saga的补偿不是数据库回滚而是业务反向操作,B错误。Saga不一定比TCC简单,两者复杂点不同,D中的“一定”过于绝对。
Q9. Saga模式中的“空补偿”问题是指什么?
- A.补偿逻辑未编写就执行
- B.正向未完成却执行补偿操作← 正确答案
- C.补偿操作执行产生新错误
- D.多个补偿事务触发并发冲突
答案:B。空补偿指正向事务尚未执行完毕或根本没执行,却因为消息乱序等原因收到了补偿请求。此时补偿操作面对的是一个“空”的业务状态。工程上需要在补偿逻辑内校验存在性,必要时结合悬挂检测综合处理。
Q10. 下列哪些场景适合使用Saga模式?
- A.跨服务订单创建流程← 正确答案
- B.银行账户实时余额扣减
- C.库存扣减与物流系统协同← 正确答案
- D.单机数据库事务操作
答案:AC。跨服务订单创建流程和库存物流协同是典型的链路长、服务多、允许短暂不一致的业务,适合Saga。银行实时余额扣减需要强一致,更适合TCC或2PC;单机数据库事务直接用本地事务即可,不需要分布式方案。