老周是“周游旅行社”的金牌导游,他接了一个五天四城的旅行团:北京出发,依次游玩南京、杭州、上海、苏州。这趟行程涉及高铁票、酒店、门票、餐厅四类预订,可没有任何一家供应商能一次承包到底——航空公司只管机票,酒店集团只管客房,景区只管门票……于是老周只能一段一段地预订,每订成一段就踏实一段;可万一后面某段预订失败,他必须立刻打电话退掉前面已订的部分,避免客人白花钱。这就像数据库里的分布式事务:整个行程是一笔“大业务”,却无法靠一个全局锁整体提交。本文将介绍 Saga 模式——一种把长事务拆成“短事务序列 + 逆向补偿”的经典方案,并用这个旅行团的故事,带你理解它的原理、执行模型、编排方式、补偿设计、隔离性难题与工程实践。
理解 Saga 模式是微服务架构下保障数据一致性的核心技能,也是面试高频考点;掌握它,你就能在分布式系统设计中同时兼顾可用性与数据一致性。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
Saga概念与起源
在老周接手前,旅行社的老办法是把所有供应商约到同一间会议室“现场盖章”:只要有一家犹豫,整单作废,所有人白等。这种“两阶段提交”看似严谨,却让每一家都被绑在一起,稍有延迟就全盘崩坏——这也是长事务在大型系统里难以落地的原因。
1987 年,数据库学者 Hector Garcia-Molina 和 Kenneth Salem 在论文中提出 Saga:与其把整趟行程锁成一个超级事务,不如拆成一段段独立的“本地事务”,每段各自提交(落袋为安),并为前 n-1 段各准备一个“补偿事务”(退订方案)。
在技术上,Saga 把分布式长事务拆成 n 个本地事务 T1…Tn 与 n-1 个补偿事务 C1…C(n-1)。每个 Ti 都在自己的数据库内满足 ACID;跨服务的“全局一致性”则靠“失败时逆序执行补偿”来达成。注意与 TCC(Try-Confirm-Cancel)的区别:TCC 要求每步预留资源,隔离性好但侵入性强;Saga 只要求提供补偿动作,更轻量灵活。
老周恍然大悟:与其要求四家供应商同场盖章,不如把行程拆成四张独立订单,每张订单都附上“免费取消”条款。这样旅程长是长,却再也不会因为一家掉链子而全军覆没。
- 正向事务序列T1 到 Tn 表示按顺序执行的 n 个本地事务,例如先订高铁票、再订酒店。
- 补偿事务集合C1 到 C(n-1) 是前 n-1 个事务各自的“退订方案”,例如取消酒店订单、退还票款。
- 确定补偿数量最后一个事务 Tn 是整个 Saga 的收尾,无需为其准备补偿,因此集合取 T 全集与 C 子集的并。
Saga执行模型
定好方案后,老周开始执行。他的原则是“订一段、确认一段”:先订北京到南京的高铁票,出票后立刻确认;再订南京酒店,入住凭证到手才继续……整个执行过程分为两条路:一路顺着往下订(正向执行),一路逆着往回退(逆向补偿)。
在 Saga 的执行模型中,每个 Ti 都是独立提交的本地事务。当某个 Tk 执行失败(例如杭州门票售罄)时,系统不能回滚一个“全局事务”(因为它根本不存在),只能启动补偿链:依次执行 C(k-1)、C(k-2)……直到 C1。
这里的关键是“本地事务已提交不可回退,只能用补偿来抵消”。补偿事务本身也是一个普通事务,做的是语义上的“撤销”:订酒店是正向,退订就是补偿;转账是正向,转回就是补偿。补偿顺序必须与正向顺序严格相反,因为后订的通常依赖先订的(先有高铁票才订酒店)。
老周在随身笔记本上画了两条路线:一条绿线向下订,一条红线向上退。只要红线能完整走回起点,这次“翻车”也能把损失降到最低——这,就是 Saga 的执行模型。
- 正向执行链箭头表示“前一个事务提交成功后,才启动下一个”,串行保证依赖关系。
- 失败定位Tk 是第 k 个本地事务,它失败即中止整条正向链,之前的 T1…T(k-1) 已提交。
- 逆向补偿链从 C(k-1) 开始逆序执行到 C1,逐一抵消已提交事务的副作用。
编排模式
方式一,老周选择“自己当总调度”:他拿着行程单,依次拨打四家供应商的电话,订完一家就在行程单上打个勾;一旦某家说“没票”,他立刻照着行程单倒序打电话退订。所有进度都装在他脑子里——这是“编排模式”(Orchestration)。
编排模式由中央协调器(Orchestrator)统一指挥:协调器向每个服务发送“执行命令”,服务完成后回执;协调器记录当前处于哪个步骤,并决定下一步是继续执行还是转入补偿。所有流程判断都集中在协调器一处,业务方只负责执行和回执。
协调器本质上是一个有限状态机(FSM):状态代表 Saga 当前进度(执行中、成功、失败、补偿中、已补偿);事件是各服务的成功/失败/超时回执;状态转移函数决定下一个动作。它的优点是流程集中、易于理解和维护,缺点是协调器自身可能成为单点和性能瓶颈。
老周的笔记本就是“协调器状态”的化身:每一通电话的结果都是事件,每个勾都是状态转移。只要笔记不乱,整团就能有条不紊地前进或撤退——这就是编排模式的故事。
- 当前状态St 表示第 t 步时 Saga 所处的状态,例如 RUNNING 表示正向执行中。
- 事件输入Et 是触发状态变化的事件,如“T2 提交成功”或“T3 超时失败”。
- 状态转移f 将“状态 + 事件”映射为下一状态,例如 RUNNING + 失败 → FAILED,FAILED + 补偿完成 → COMPENSATED。
协同模式
方式二,老周决定“无为而治”:他把行程单贴在旅行社公告栏上,让每家供应商干完自己的活后,主动通知下一家开工。高铁站出票后,售票系统自动发一封邮件给酒店;酒店确认后,又自动发邮件给景区……老周只负责在终点等结果。
这就是协同模式(Choreography):没有中央协调器,每个服务在本地事务提交后发布领域事件,下一服务订阅该事件并执行自己的事务,形成“事件链”。补偿同样通过反向事件链传播,一个服务的补偿动作也会发布事件,驱动前一个服务做补偿。
协同模式的关键是“事件驱动”:事件必须可靠投递(通常用消息队列持久化),消费者必须幂等(同一事件重复投递只能生效一次)。它的优点是服务间低耦合、无单点,缺点是流程隐晦,跨服务的全局状态难以追踪和调试,出问题时像“断了线的风筝”。
没有总调度,胜似总调度。邮件的接力让每家供应商各司其职,老周省心不少——但万一某封邮件丢了,他得翻遍所有往来记录才能定位断点。这就是协同模式的自由与代价。
- 本地提交Ti 在自身数据库提交成功后,才对外发布事件,保证事件语义真实有效。
- 事件发布ei 携带业务语义(如“票已出”),写入消息队列供下游订阅。
- 消费触发下一服务消费 ei 后启动 T(i+1),正向链由此递进,无需中央调度。
补偿设计策略
无论是自己打电话还是靠邮件接力,退订都是门技术活。老周总结出血泪教训:给酒店打第一个退订电话时客服说“已取消”,他不放心又打了一次,结果酒店重复退款,账目全乱了——退订操作必须“幂等”。
补偿事务的设计直接决定 Saga 的可靠性,三大原则:一是幂等(同一补偿执行多次与一次效果相同);二是空补偿安全(若事务从未成功执行,其补偿应为空操作);三是可逆性分析(区分可补偿操作与不可补偿操作,如“已发送的营销短信”难以撤回,需要正向设计兜底)。
实现幂等通常引入“补偿状态表/幂等键”:补偿执行前先查表,若该事务已补偿过则直接返回成功;补偿与查表写状态在同一本地事务中完成。空补偿则通过对事务状态的判断实现:只有状态为“已提交”才执行补偿,否则跳过。可逆性分析要在设计阶段画出每步的逆向操作,无法逆向的步骤应放在 Saga 末尾或改为可幂等覆盖的写操作。
老周在行程单上给每家供应商备注一行小字:“退订前先问一句是否已退过。”这行字,就是生产环境中千万行补偿代码的朴素原型。
- 当前状态x 表示某一步数据的当前状态,如“已预订 / 已取消”。
- 一次补偿compensate(x) 把已提交状态撤销,例如“已预订 → 已取消”。
- 二次补偿对已取消状态再执行补偿,结果仍为已取消,即重复执行与一次执行等价。
隔离性与MVCC
又一个难题来了。老周的旅行团在南京酒店“已确认入住”,可隔壁另一个团的导游并不知道这间房可能被老周退掉(补偿未完成前不更新状态),他给客人订了同一间房——等到老周真的退房,隔壁团就傻眼了。这就是 Saga 的隔离性缺失。
传统 ACID 事务靠锁与两阶段提交保证隔离,但 Saga 的各个本地事务独立提交,彼此之间没有全局锁,因此“未完成 Saga 的中间状态”对其他事务可见。若另一个事务基于这个终将被补偿的数据做决策,就产生“悬挂更新”(dangling update)——它读到并写入的是一条注定要消失的“幽灵数据”。
缓解手段有三类:语义锁(semantic lock)——先预占资源、把“预占中”作为状态暴露;可交换更新——把扣减设计成可合并的账本操作;MVCC 快照隔离——读事务看到的是 Saga 开始前的快照,避免读到正在补偿中的中间版本。但 MVCC 只能缓解“读”问题,写冲突仍需通过乐观锁(版本号/条件更新)解决。
隔壁导游若能“看快照”而不是“看实时”,就能避开幽灵房;若还要抢占同一间房,就得靠“预占标记”(语义锁)说话。Saga 的世界里没有免费的隔离,只有聪明的缓解。
- 快照时刻t0 是读事务开始的时间点,之后的数据变化对本次读取不可见。
- 版本可见性v 是数据的一个版本,提交时间为 t_commit(v)。
- 快照集合所有满足 t_commit(v) ≤ t0 的版本构成一致性快照,未提交或晚提交的版本不可见。
可靠性保障
老周最怕的不是某个供应商说“没票”,而是电话打通了对方却听不清、或者消息发出去石沉大海。于是他立下规矩:每一通关键电话都要录音存档(消息持久化);没听清就隔一会儿再打一次(超时重试);退订确认必须以对方书面回执为准(幂等键)。
Saga 的可靠性包含两类恢复:前向恢复(forward recovery)——从最近的已知状态继续向前执行,适用于“超时但实际已成功”的场景,重试必须带幂等键;后向恢复(backward recovery)——执行补偿链撤销已提交步骤。消息队列的持久化(或 Outbox 模式)保证事件不丢,为恢复提供依据。
重试策略常用指数退避:第 n 次重试前的等待时间为 t0·bⁿ,避免失败瞬间所有调用方同时重试造成“惊群”。同时要设置最大重试次数与熔断兜底——超过阈值后转为人工介入或降级处理。协调器自身的状态也要持久化,宕机后能从最后记录的已知状态恢复。
老周的“电话录音 + 隔时重打 + 书面回执”三件套,与分布式系统里的“持久化 + 指数退避 + 幂等键”如出一辙——可靠的 Saga,靠的不是运气,而是每一条消息都有着落。
- 基础等待t_base 是首次重试前的等待时间,通常取毫秒级经验值。
- 增长因子b 是退避因子(如 2),n 越大等待越久。
- 总等待两者相乘得到第 n 次重试的等待时长,指数级分散重试压力。
性能与工程实践
经过一个旺季的磨合,老周把旅行团的运作打磨成了流水线:每个城市的预订环节都有专人盯耗时,每周复盘哪一段最容易“翻车”,每月还故意安排一次“假装断网”演练,看团队能否在联系不上供应商时安全退订。
工程实践上,首先要度量 Saga 的整体成功率:它等于各步骤成功率的乘积,因此最差的环节决定整体质量;其次要对“补偿成功率”“平均补偿时长”等指标监控告警;再通过故障注入(如随机丢弃消息、模拟超时)验证补偿链在异常下的表现。
最佳实践清单包括:步骤尽量短小、每个正向与补偿操作都幂等、优先用编排模式管控复杂流程、用协同模式解耦简单线性链路、为 Saga 准备全局唯一的追踪 ID、补偿失败要有重试与告警兜底。性能上 Saga 无需全局锁,吞吐远高于两阶段提交,但“补偿风暴”(大量 Saga 同时失败)需要限流与熔断保护。
老周在季度总结会上写下十六个字:“短步快订,退订幂等;能编则编,可协则协。”——这十六个字,也是 Saga 工程实践最凝练的注脚。
- 单步成功率pi 是第 i 个本地事务的成功率。
- 成功条件所有步骤都成功,整体 Saga 才算成功,因此用连乘。
- 关键启发整体成功率被最弱的步骤钳制,要优先优化成功率最低的环节并设计重试。
AI 应用场景
电商下单与库存扣减
阶段:在线推理服务(个性化下单链路)
解决的问题:传统做法要求下单、扣库存、发券在一个分布式事务里“同生共死”,使用全局锁或两阶段提交会让下单高峰期吞吐暴跌。用 Saga 把链路拆成“创建订单→扣库存→核销优惠券→发消息”四个本地事务,任一失败就逆向补偿(释放库存、退回券),把一致性从“强同步”变为“最终一致 + 补偿”,下单吞吐大幅提升。
具体案例:某电商大促期间,用户下单后系统先写订单(T1),再扣 Redis 库存(T2),再核销优惠券(T3);若 T3 失败,则回滚 T2 的库存预占并取消 T1 的订单。全程无全局锁,库存服务吞吐提升 3 倍,补偿成功率通过幂等键从 96% 提升到 99.99%。
特征平台的跨系统特征更新
阶段:特征工程(特征存储一致性)
解决的问题:推荐系统的用户特征分散在特征存储、样本库、模型缓存三个系统;更新特征时若只写成功部分,训练与推理就会“特征错位”。Saga 把特征写入、样本回放、缓存刷新串成带补偿的链路,任一步失败即回滚,保证训练和推理使用同一版本特征。
具体案例:某短视频推荐平台每天凌晨更新用户兴趣特征,曾因缓存刷新失败导致线上推理使用旧特征、离线训练使用新特征,A/B 指标失真。改用 Saga 后,特征更新任一环节失败都会触发补偿(回滚样本库标记、清除缓存),特征版本一致率从 92% 提升到 99.9%。
实验平台的配额分配
阶段:模型迭代与实验管理
解决的问题:创建 A/B 实验需要同时扣减实验配额、配置流量路由、开通报表服务,跨三个系统;传统“手写重试 + 人工对账”容易遗漏补偿。用编排式 Saga 让协调器依次调用三个服务,失败时反向释放配额并关闭路由,避免实验流量落错模型。
具体案例:某公司实验平台每月创建上千个实验,曾因路由配置失败导致部分实验流量落到旧模型。接入 Saga 后,配额-路由-报表三步任一失败都在 2 秒内自动补偿,误配置率下降 85%。
Python 演示
# -*- coding: utf-8 -*-"""Saga 模式编排器示例:用旅行团分段预订模拟分布式事务补偿"""import random class LocalTxn(object): """一个本地事务:包含正向操作与补偿操作""" def __init__(self, name, do_func, cancel_func): self.name = name self.do_func = do_func self.cancel_func = cancel_func def do(self): self.do_func() def cancel(self): self.cancel_func() class SagaOrchestrator(object): """编排器:顺序执行正向事务,某一步失败则逆序补偿""" def __init__(self, txn_list): self.txn_list = txn_list self.done_stack = [] # 已成功提交的事务(待补偿栈) def run(self): for txn in self.txn_list: try: txn.do() self.done_stack.append(txn) print(">> 步骤完成: {}".format(txn.name)) except Exception as err: print("!! 步骤失败: {},原因: {}".format(txn.name, err)) self.compensate() return False print("== 全部步骤成功,Saga 结束") return True def compensate(self): while self.done_stack: txn = self.done_stack.pop() try: txn.cancel() print("<< 补偿完成: {}".format(txn.name)) except Exception as err: print("XX 补偿失败: {},原因: {}".format(txn.name, err)) orders = {} # 全局订单簿(模拟数据库状态) def book_seat(city): """构造预订某城市高铁票的本地事务(含退票补偿)""" key = "ticket_" + city def do(): # 模拟 70% 概率预订成功 if random.random() < 0.7: orders[key] = "已预订" print("-- 预订 {} 高铁票成功".format(city)) else: raise RuntimeError("{} 余票不足".format(city)) def cancel(): # 幂等:只有已预订状态才执行退款 if orders.get(key) == "已预订": orders[key] = "已取消" print("-- 退订 {} 高铁票,退款完成".format(city)) return LocalTxn("订高铁票_" + city, do, cancel) def main(): random.seed(1) # 固定随机种子,保证每次运行结果可复现 cities = ["北京", "南京", "杭州", "上海"] saga = SagaOrchestrator([book_seat(c) for c in cities]) saga.run() print("最终订单结果: {}".format(orders)) if __name__ == "__main__": main()
class LocalTxn(object):封装一个“本地事务”,相当于旅行团里的一张订单。self.done_stack = [] # 已成功提交的事务(待补偿栈)用栈记录已成功的步骤,为逆序补偿做准备。self.done_stack.append(txn)每成功一步就压入栈顶,后订的先退。self.compensate()捕获到异常即启动补偿链,对应执行模型中的逆向回滚。while self.done_stack:从栈顶弹出最后成功的事务,实现“逆序补偿”。if orders.get(key) == "已预订":幂等判断,只有已预订状态才退款,重复调用不重复退。random.seed(1)固定随机种子,让“第几站失败”可复现,便于演示与测试。if __name__ == "__main__":程序入口,保证模块被导入时不会自动执行。
常见误区
误区我一直以为用了 Saga 就能和单机数据库事务一样,要么全部成功要么全部失败,中间状态别人什么都看不到。
正解Saga 只能保证“最终一致”,不是“实时一致”。每个本地事务独立提交后立刻对其他系统可见,补偿是异步追认的。就像旅行团退订酒店后,房价在退款到账前仍可能显示被占用。要减少中间状态可见,需配合语义锁、预占、MVCC 快照等手段。
为何常见:教科书常把“事务”画成原子开关,容易让人把分布式 Saga 也想象成同一个开关。
误区反正有补偿,正向流程随便写,补偿失败了再人工修就行,不用花太多精力设计。
正解补偿是 Saga 的“安全绳”,必须可重复、可幂等、可重试,并且有监控告警兜底;补偿失败会留下半完成状态,比正向失败更难排查。就像酒店退订必须“先查状态、再退款”,且退订接口要支持重复调用而不重复退款。
为何常见:正向流程往往是开发重点,补偿路径容易被当成“异常分支”潦草处理,等到故障才发现补偿比正向还难写。
误区编排有中央协调器,流程看得清、好维护,所以所有 Saga 都应该用编排。
正解编排集中可控但引入单点和额外依赖;协同低耦合但流程隐晦。实践上,复杂、变更多的流程用编排(相当于老周亲自打电话),简单、稳定的线性链路用协同(邮件接力),很多系统两者混用。
为何常见:直观上“有人管着”更安心,容易忽略集中化带来的性能与可用性代价。
误区数据库开了 MVCC,Saga 的中间状态就不会被别人看到了,隔离问题就解决了。
正解MVCC 快照只保证“读”的一致——读者看不到未提交版本;但 Saga 各步骤是独立提交的“已提交版本”,快照挡不住这类中间状态。写冲突仍需语义锁、乐观锁(版本号/条件更新)解决;补偿账本则用于审计与对账。
为何常见:把 MVCC 的“读隔离”误当成“写隔离”,忽略了快照只对读事务生效、对并发的写者无能为力。
面试题
Q1. Saga 模式中,补偿事务与正向事务的执行顺序关系是?
- A.相同顺序执行
- B.严格逆序执行← 正确答案
- C.随机顺序执行
- D.并行同时执行
答案:B。补偿必须从“最后成功的一步”开始逆序撤销,因为后一步通常依赖前一步(先有票才订酒店)。就像退订时要先退最后订的酒店,再退前面的高铁票。
Q2. 下面哪个业务场景最适合使用 Saga 模式?
- A.单库表内一条记录更新
- B.跨多个微服务的下单流程← 正确答案
- C.单机内存中的缓存读写
- D.本地磁盘文件定期备份
答案:B。Saga 解决的是“跨多个独立数据库/服务的分布式长事务”,下单流程正是典型。单库更新用本地事务即可,缓存读写和文件备份不需要跨系统的一致性保证。
Q3. 编排模式中负责推进流程的核心组件是?
- A.中央流程协调器← 正确答案
- B.全局分布式锁
- C.主从数据库同步
- D.客户端本地缓存
答案:A。编排模式的核心是中央协调器(Orchestrator),它向各服务发命令、收回执,并用状态机管理进度。分布式锁、数据库同步、本地缓存都不具备“统一调度流程”的职责。
Q4. 协同模式中,服务与服务之间通过什么机制联动?
- A.中央协调器统一调度
- B.事件发布与订阅递进← 正确答案
- C.直连对方数据库表
- D.同步远程调用链路
答案:B。协同模式没有中央调度,靠“提交后发布事件、下游订阅事件”形成事件链递进推进。直连对方数据库会破坏服务边界,同步 RPC 是编排式而非协同式的典型做法。
Q5. 设计 Saga 补偿事务时,应满足哪些要求?
- A.幂等可重试← 正确答案
- B.可重复执行← 正确答案
- C.顺序可颠倒
- D.须处理空补偿← 正确答案
答案:ABD。补偿必须幂等且可重复执行(同一退订调用多次结果一致),也要处理“事务从未执行”的空补偿。补偿顺序不可颠倒——必须先退最后成功的步骤,否则依赖关系会断裂。
Q6. 关于 Saga 模式隔离性的描述,正确的是?
- A.具备全局隔离能力
- B.中间状态全局可见← 正确答案
- C.自动避免悬挂更新
- D.与本地事务隔离相同
答案:B。Saga 各步骤独立提交、无全局锁,因此中间状态对其他事务可见,这正是“隔离缺失”的表现。它不具备全局隔离,也不会自动避免悬挂更新,必须靠语义锁、快照等手段缓解。
Q7. 以下哪些做法可以增强 Saga 的可靠性?
- A.消息队列持久化← 正确答案
- B.超时重试与幂等键← 正确答案
- C.定期故障注入演练← 正确答案
- D.关闭事务流水日志
答案:ABC。消息持久化保证事件不丢,超时重试配合幂等键保证“重试不重复生效”,故障注入演练能提前暴露补偿链缺陷。关闭事务日志会让恢复失去依据,属于错误做法。
Q8. 一个 5 步 Saga 在第 3 步执行失败(未提交),编排器应补偿哪些事务?
- A.前三个步骤
- B.前两个步骤← 正确答案
- C.全部五个步骤
- D.仅第三个步骤
答案:B。第 3 步失败即未提交,无需补偿它自己;需要补偿的是已经提交的前两步。这就像第三家供应商拒绝了订单,老周只需退掉前两家已确认的预订。
Q9. 关于 Saga 与隔离性/MVCC 的表述,正确的有?
- A.缺少全局隔离默认← 正确答案
- B.语义锁可缓解冲突← 正确答案
- C.快照彻底消除问题
- D.补偿账本辅助恢复← 正确答案
答案:ABD。Saga 默认缺少全局隔离;语义锁(预占)能缓解并发冲突;补偿账本记录补偿历史,支持审计与对账。MVCC 快照只能缓解读问题,不能彻底消除隔离问题,故 C 错误。
Q10. 两个并发 Saga 操作同一账户:Saga A 转出资金后尚未补偿,Saga B 读到该中间状态并继续扣款;随后 A 执行补偿回滚。这一异常属于典型的?
- A.普通脏读问题
- B.悬挂更新异常← 正确答案
- C.不可重复读
- D.数据库死锁
答案:B。Saga A 提交的转出会在后续被补偿撤销,B 基于这条“终将被消除”的中间状态继续扣款,正是标准的悬挂更新(dangling update)。它与单事务脏读的区别在于:数据本身已提交,只是语义上会被追认撤销。