老周是“周游旅行社”的金牌导游,他接了一个五天四城的旅行团:北京出发,依次游玩南京、杭州、上海、苏州。这趟行程涉及高铁票、酒店、门票、餐厅四类预订,可没有任何一家供应商能一次承包到底——航空公司只管机票,酒店集团只管客房,景区只管门票……于是老周只能一段一段地预订,每订成一段就踏实一段;可万一后面某段预订失败,他必须立刻打电话退掉前面已订的部分,避免客人白花钱。这就像数据库里的分布式事务:整个行程是一笔“大业务”,却无法靠一个全局锁整体提交。本文将介绍 Saga 模式——一种把长事务拆成“短事务序列 + 逆向补偿”的经典方案,并用这个旅行团的故事,带你理解它的原理、执行模型、编排方式、补偿设计、隔离性难题与工程实践。

岗位关联

理解 Saga 模式是微服务架构下保障数据一致性的核心技能,也是面试高频考点;掌握它,你就能在分布式系统设计中同时兼顾可用性与数据一致性。

目录
  1. 可视化演示
  2. Saga概念与起源
  3. Saga执行模型
  4. 编排模式
  5. 协同模式
  6. 补偿设计策略
  7. 隔离性与MVCC
  8. 可靠性保障
  9. 性能与工程实践
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 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 只要求提供补偿动作,更轻量灵活。

老周恍然大悟:与其要求四家供应商同场盖章,不如把行程拆成四张独立订单,每张订单都附上“免费取消”条款。这样旅程长是长,却再也不会因为一家掉链子而全军覆没。

S={T1,T2,…,Tn}∪{C1,C2,…,Cn−1}\displaystyle S = \{ T_1, T_2, \ldots, T_n \} \cup \{ C_1, C_2, \ldots, C_{n-1} \}
Saga 由 n 个正向本地事务和 n-1 个补偿事务组成,最后一个正向事务无需补偿。
  1. 正向事务序列T1 到 Tn 表示按顺序执行的 n 个本地事务,例如先订高铁票、再订酒店。
    T1,T2,…,TnT_1, T_2, \ldots, T_n
  2. 补偿事务集合C1 到 C(n-1) 是前 n-1 个事务各自的“退订方案”,例如取消酒店订单、退还票款。
    C1,C2,…,Cn−1C_1, C_2, \ldots, C_{n-1}
  3. 确定补偿数量最后一个事务 Tn 是整个 Saga 的收尾,无需为其准备补偿,因此集合取 T 全集与 C 子集的并。
    S={Ti}∪{Cj}S = \{ T_i \} \cup \{ C_j \}

Saga执行模型

定好方案后,老周开始执行。他的原则是“订一段、确认一段”:先订北京到南京的高铁票,出票后立刻确认;再订南京酒店,入住凭证到手才继续……整个执行过程分为两条路:一路顺着往下订(正向执行),一路逆着往回退(逆向补偿)。

在 Saga 的执行模型中,每个 Ti 都是独立提交的本地事务。当某个 Tk 执行失败(例如杭州门票售罄)时,系统不能回滚一个“全局事务”(因为它根本不存在),只能启动补偿链:依次执行 C(k-1)、C(k-2)……直到 C1。

这里的关键是“本地事务已提交不可回退,只能用补偿来抵消”。补偿事务本身也是一个普通事务,做的是语义上的“撤销”:订酒店是正向,退订就是补偿;转账是正向,转回就是补偿。补偿顺序必须与正向顺序严格相反,因为后订的通常依赖先订的(先有高铁票才订酒店)。

老周在随身笔记本上画了两条路线:一条绿线向下订,一条红线向上退。只要红线能完整走回起点,这次“翻车”也能把损失降到最低——这,就是 Saga 的执行模型。

正向: T1→T2→⋯→Tn\text{正向: } T_1 \to T_2 \to \cdots \to T_n
失败于 Tk: T1→⋯→Tk⇒Ck−1→Ck−2→⋯→C1\text{失败于 } T_k \text{: } T_1 \to \cdots \to T_k \Rightarrow C_{k-1} \to C_{k-2} \to \cdots \to C_1
正向按序推进,失败则从失败点的前一步开始逆序补偿。
  1. 正向执行链箭头表示“前一个事务提交成功后,才启动下一个”,串行保证依赖关系。
    T1→T2→⋯→TnT_1 \to T_2 \to \cdots \to T_n
  2. 失败定位Tk 是第 k 个本地事务,它失败即中止整条正向链,之前的 T1…T(k-1) 已提交。
    T1→⋯→TkT_1 \to \cdots \to T_k
  3. 逆向补偿链从 C(k-1) 开始逆序执行到 C1,逐一抵消已提交事务的副作用。
    Ck−1→Ck−2→⋯→C1C_{k-1} \to C_{k-2} \to \cdots \to C_1

编排模式

方式一,老周选择“自己当总调度”:他拿着行程单,依次拨打四家供应商的电话,订完一家就在行程单上打个勾;一旦某家说“没票”,他立刻照着行程单倒序打电话退订。所有进度都装在他脑子里——这是“编排模式”(Orchestration)。

编排模式由中央协调器(Orchestrator)统一指挥:协调器向每个服务发送“执行命令”,服务完成后回执;协调器记录当前处于哪个步骤,并决定下一步是继续执行还是转入补偿。所有流程判断都集中在协调器一处,业务方只负责执行和回执。

协调器本质上是一个有限状态机(FSM):状态代表 Saga 当前进度(执行中、成功、失败、补偿中、已补偿);事件是各服务的成功/失败/超时回执;状态转移函数决定下一个动作。它的优点是流程集中、易于理解和维护,缺点是协调器自身可能成为单点和性能瓶颈。

老周的笔记本就是“协调器状态”的化身:每一通电话的结果都是事件,每个勾都是状态转移。只要笔记不乱,整团就能有条不紊地前进或撤退——这就是编排模式的故事。

St+1=f(St,Et),St∈{START,RUNNING,SUCCEEDED,FAILED,COMPENSATED}\displaystyle S_{t+1} = f(S_t, E_t), \quad S_t \in \{ \text{START}, \text{RUNNING}, \text{SUCCEEDED}, \text{FAILED}, \text{COMPENSATED} \}
协调器根据当前状态和收到的事件,计算出下一个状态,推动 Saga 前进或后退。
  1. 当前状态St 表示第 t 步时 Saga 所处的状态,例如 RUNNING 表示正向执行中。
    StS_t
  2. 事件输入Et 是触发状态变化的事件,如“T2 提交成功”或“T3 超时失败”。
    EtE_t
  3. 状态转移f 将“状态 + 事件”映射为下一状态,例如 RUNNING + 失败 → FAILED,FAILED + 补偿完成 → COMPENSATED。
    St+1=f(St,Et)S_{t+1} = f(S_t, E_t)

协同模式

方式二,老周决定“无为而治”:他把行程单贴在旅行社公告栏上,让每家供应商干完自己的活后,主动通知下一家开工。高铁站出票后,售票系统自动发一封邮件给酒店;酒店确认后,又自动发邮件给景区……老周只负责在终点等结果。

这就是协同模式(Choreography):没有中央协调器,每个服务在本地事务提交后发布领域事件,下一服务订阅该事件并执行自己的事务,形成“事件链”。补偿同样通过反向事件链传播,一个服务的补偿动作也会发布事件,驱动前一个服务做补偿。

协同模式的关键是“事件驱动”:事件必须可靠投递(通常用消息队列持久化),消费者必须幂等(同一事件重复投递只能生效一次)。它的优点是服务间低耦合、无单点,缺点是流程隐晦,跨服务的全局状态难以追踪和调试,出问题时像“断了线的风筝”。

没有总调度,胜似总调度。邮件的接力让每家供应商各司其职,老周省心不少——但万一某封邮件丢了,他得翻遍所有往来记录才能定位断点。这就是协同模式的自由与代价。

Ti→commitei→consumeTi+1\displaystyle T_i \xrightarrow{\text{commit}} e_i \xrightarrow{\text{consume}} T_{i+1}
第 i 个事务提交后发布事件 ei,被第 i+1 个服务消费并触发 T(i+1)。
  1. 本地提交Ti 在自身数据库提交成功后,才对外发布事件,保证事件语义真实有效。
    Ti→commiteiT_i \xrightarrow{\text{commit}} e_i
  2. 事件发布ei 携带业务语义(如“票已出”),写入消息队列供下游订阅。
    eie_i
  3. 消费触发下一服务消费 ei 后启动 T(i+1),正向链由此递进,无需中央调度。
    ei→consumeTi+1e_i \xrightarrow{\text{consume}} T_{i+1}

补偿设计策略

无论是自己打电话还是靠邮件接力,退订都是门技术活。老周总结出血泪教训:给酒店打第一个退订电话时客服说“已取消”,他不放心又打了一次,结果酒店重复退款,账目全乱了——退订操作必须“幂等”。

补偿事务的设计直接决定 Saga 的可靠性,三大原则:一是幂等(同一补偿执行多次与一次效果相同);二是空补偿安全(若事务从未成功执行,其补偿应为空操作);三是可逆性分析(区分可补偿操作与不可补偿操作,如“已发送的营销短信”难以撤回,需要正向设计兜底)。

实现幂等通常引入“补偿状态表/幂等键”:补偿执行前先查表,若该事务已补偿过则直接返回成功;补偿与查表写状态在同一本地事务中完成。空补偿则通过对事务状态的判断实现:只有状态为“已提交”才执行补偿,否则跳过。可逆性分析要在设计阶段画出每步的逆向操作,无法逆向的步骤应放在 Saga 末尾或改为可幂等覆盖的写操作。

老周在行程单上给每家供应商备注一行小字:“退订前先问一句是否已退过。”这行字,就是生产环境中千万行补偿代码的朴素原型。

compensate(compensate(x))=compensate(x)\displaystyle \mathrm{compensate}(\mathrm{compensate}(x)) = \mathrm{compensate}(x)
补偿操作满足幂等性,重复执行不会改变最终状态。
  1. 当前状态x 表示某一步数据的当前状态,如“已预订 / 已取消”。
    xx
  2. 一次补偿compensate(x) 把已提交状态撤销,例如“已预订 → 已取消”。
    compensate(x)\mathrm{compensate}(x)
  3. 二次补偿对已取消状态再执行补偿,结果仍为已取消,即重复执行与一次执行等价。
    compensate(compensate(x))=compensate(x)\mathrm{compensate}(\mathrm{compensate}(x)) = \mathrm{compensate}(x)

隔离性与MVCC

又一个难题来了。老周的旅行团在南京酒店“已确认入住”,可隔壁另一个团的导游并不知道这间房可能被老周退掉(补偿未完成前不更新状态),他给客人订了同一间房——等到老周真的退房,隔壁团就傻眼了。这就是 Saga 的隔离性缺失。

传统 ACID 事务靠锁与两阶段提交保证隔离,但 Saga 的各个本地事务独立提交,彼此之间没有全局锁,因此“未完成 Saga 的中间状态”对其他事务可见。若另一个事务基于这个终将被补偿的数据做决策,就产生“悬挂更新”(dangling update)——它读到并写入的是一条注定要消失的“幽灵数据”。

缓解手段有三类:语义锁(semantic lock)——先预占资源、把“预占中”作为状态暴露;可交换更新——把扣减设计成可合并的账本操作;MVCC 快照隔离——读事务看到的是 Saga 开始前的快照,避免读到正在补偿中的中间版本。但 MVCC 只能缓解“读”问题,写冲突仍需通过乐观锁(版本号/条件更新)解决。

隔壁导游若能“看快照”而不是“看实时”,就能避开幽灵房;若还要抢占同一间房,就得靠“预占标记”(语义锁)说话。Saga 的世界里没有免费的隔离,只有聪明的缓解。

snapshot⁡(t0)={v∣tcommit(v)≤t0}\displaystyle \operatorname{snapshot}(t_0) = \{ v \mid t_{\text{commit}}(v) \le t_0 \}
MVCC 快照在时刻 t0 只可见提交时间不晚于 t0 的数据版本。
  1. 快照时刻t0 是读事务开始的时间点,之后的数据变化对本次读取不可见。
    t0t_0
  2. 版本可见性v 是数据的一个版本,提交时间为 t_commit(v)。
    v, tcommit(v)v, \ t_{\text{commit}}(v)
  3. 快照集合所有满足 t_commit(v) ≤ t0 的版本构成一致性快照,未提交或晚提交的版本不可见。
    snapshot⁡(t0)={v∣tcommit(v)≤t0}\operatorname{snapshot}(t_0) = \{ v \mid t_{\text{commit}}(v) \le t_0 \}

可靠性保障

老周最怕的不是某个供应商说“没票”,而是电话打通了对方却听不清、或者消息发出去石沉大海。于是他立下规矩:每一通关键电话都要录音存档(消息持久化);没听清就隔一会儿再打一次(超时重试);退订确认必须以对方书面回执为准(幂等键)。

Saga 的可靠性包含两类恢复:前向恢复(forward recovery)——从最近的已知状态继续向前执行,适用于“超时但实际已成功”的场景,重试必须带幂等键;后向恢复(backward recovery)——执行补偿链撤销已提交步骤。消息队列的持久化(或 Outbox 模式)保证事件不丢,为恢复提供依据。

重试策略常用指数退避:第 n 次重试前的等待时间为 t0·bⁿ,避免失败瞬间所有调用方同时重试造成“惊群”。同时要设置最大重试次数与熔断兜底——超过阈值后转为人工介入或降级处理。协调器自身的状态也要持久化,宕机后能从最后记录的已知状态恢复。

老周的“电话录音 + 隔时重打 + 书面回执”三件套,与分布式系统里的“持久化 + 指数退避 + 幂等键”如出一辙——可靠的 Saga,靠的不是运气,而是每一条消息都有着落。

twait(n)=tbase⋅bn\displaystyle t_{\text{wait}}(n) = t_{\text{base}} \cdot b^{n}
第 n 次重试前的等待时间按指数增长,避免压力瞬时集中。
  1. 基础等待t_base 是首次重试前的等待时间,通常取毫秒级经验值。
    tbaset_{\text{base}}
  2. 增长因子b 是退避因子(如 2),n 越大等待越久。
    bnb^{n}
  3. 总等待两者相乘得到第 n 次重试的等待时长,指数级分散重试压力。
    twait(n)=tbase⋅bnt_{\text{wait}}(n) = t_{\text{base}} \cdot b^{n}

性能与工程实践

经过一个旺季的磨合,老周把旅行团的运作打磨成了流水线:每个城市的预订环节都有专人盯耗时,每周复盘哪一段最容易“翻车”,每月还故意安排一次“假装断网”演练,看团队能否在联系不上供应商时安全退订。

工程实践上,首先要度量 Saga 的整体成功率:它等于各步骤成功率的乘积,因此最差的环节决定整体质量;其次要对“补偿成功率”“平均补偿时长”等指标监控告警;再通过故障注入(如随机丢弃消息、模拟超时)验证补偿链在异常下的表现。

最佳实践清单包括:步骤尽量短小、每个正向与补偿操作都幂等、优先用编排模式管控复杂流程、用协同模式解耦简单线性链路、为 Saga 准备全局唯一的追踪 ID、补偿失败要有重试与告警兜底。性能上 Saga 无需全局锁,吞吐远高于两阶段提交,但“补偿风暴”(大量 Saga 同时失败)需要限流与熔断保护。

老周在季度总结会上写下十六个字:“短步快订,退订幂等;能编则编,可协则协。”——这十六个字,也是 Saga 工程实践最凝练的注脚。

Psaga=∏i=1npi\displaystyle P_{\text{saga}} = \prod_{i=1}^{n} p_i
无重试时 Saga 整体成功率等于各步骤成功率的乘积。
  1. 单步成功率pi 是第 i 个本地事务的成功率。
    pip_i
  2. 成功条件所有步骤都成功,整体 Saga 才算成功,因此用连乘。
    ∏i=1npi\prod_{i=1}^{n} p_i
  3. 关键启发整体成功率被最弱的步骤钳制,要优先优化成功率最低的环节并设计重试。
    Psaga=∏i=1npiP_{\text{saga}} = \prod_{i=1}^{n} p_i

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 演示

python84 行
# -*- 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()
目的:用“订四段高铁票”模拟一个编排式 Saga,某一站订票失败时自动逆序退订前面已成功的车票。
  1. class LocalTxn(object):封装一个“本地事务”,相当于旅行团里的一张订单。
  2. self.done_stack = [] # 已成功提交的事务(待补偿栈)用栈记录已成功的步骤,为逆序补偿做准备。
  3. self.done_stack.append(txn)每成功一步就压入栈顶,后订的先退。
  4. self.compensate()捕获到异常即启动补偿链,对应执行模型中的逆向回滚。
  5. while self.done_stack:从栈顶弹出最后成功的事务,实现“逆序补偿”。
  6. if orders.get(key) == "已预订":幂等判断,只有已预订状态才退款,重复调用不重复退。
  7. random.seed(1)固定随机种子,让“第几站失败”可复现,便于演示与测试。
  8. if __name__ == "__main__":程序入口,保证模块被导入时不会自动执行。

常见误区

误区我一直以为用了 Saga 就能和单机数据库事务一样,要么全部成功要么全部失败,中间状态别人什么都看不到。

正解Saga 只能保证“最终一致”,不是“实时一致”。每个本地事务独立提交后立刻对其他系统可见,补偿是异步追认的。就像旅行团退订酒店后,房价在退款到账前仍可能显示被占用。要减少中间状态可见,需配合语义锁、预占、MVCC 快照等手段。

为何常见:教科书常把“事务”画成原子开关,容易让人把分布式 Saga 也想象成同一个开关。

误区反正有补偿,正向流程随便写,补偿失败了再人工修就行,不用花太多精力设计。

正解补偿是 Saga 的“安全绳”,必须可重复、可幂等、可重试,并且有监控告警兜底;补偿失败会留下半完成状态,比正向失败更难排查。就像酒店退订必须“先查状态、再退款”,且退订接口要支持重复调用而不重复退款。

为何常见:正向流程往往是开发重点,补偿路径容易被当成“异常分支”潦草处理,等到故障才发现补偿比正向还难写。

误区编排有中央协调器,流程看得清、好维护,所以所有 Saga 都应该用编排。

正解编排集中可控但引入单点和额外依赖;协同低耦合但流程隐晦。实践上,复杂、变更多的流程用编排(相当于老周亲自打电话),简单、稳定的线性链路用协同(邮件接力),很多系统两者混用。

为何常见:直观上“有人管着”更安心,容易忽略集中化带来的性能与可用性代价。

误区数据库开了 MVCC,Saga 的中间状态就不会被别人看到了,隔离问题就解决了。

正解MVCC 快照只保证“读”的一致——读者看不到未提交版本;但 Saga 各步骤是独立提交的“已提交版本”,快照挡不住这类中间状态。写冲突仍需语义锁、乐观锁(版本号/条件更新)解决;补偿账本则用于审计与对账。

为何常见:把 MVCC 的“读隔离”误当成“写隔离”,忽略了快照只对读事务生效、对并发的写者无能为力。

面试题

Q1. Saga 模式中,补偿事务与正向事务的执行顺序关系是?

单选题 · easy

  • A.相同顺序执行
  • B.严格逆序执行← 正确答案
  • C.随机顺序执行
  • D.并行同时执行

答案:B。补偿必须从“最后成功的一步”开始逆序撤销,因为后一步通常依赖前一步(先有票才订酒店)。就像退订时要先退最后订的酒店,再退前面的高铁票。

Q2. 下面哪个业务场景最适合使用 Saga 模式?

单选题 · easy

  • A.单库表内一条记录更新
  • B.跨多个微服务的下单流程← 正确答案
  • C.单机内存中的缓存读写
  • D.本地磁盘文件定期备份

答案:B。Saga 解决的是“跨多个独立数据库/服务的分布式长事务”,下单流程正是典型。单库更新用本地事务即可,缓存读写和文件备份不需要跨系统的一致性保证。

Q3. 编排模式中负责推进流程的核心组件是?

单选题 · easy

  • A.中央流程协调器← 正确答案
  • B.全局分布式锁
  • C.主从数据库同步
  • D.客户端本地缓存

答案:A。编排模式的核心是中央协调器(Orchestrator),它向各服务发命令、收回执,并用状态机管理进度。分布式锁、数据库同步、本地缓存都不具备“统一调度流程”的职责。

Q4. 协同模式中,服务与服务之间通过什么机制联动?

单选题 · mid

  • A.中央协调器统一调度
  • B.事件发布与订阅递进← 正确答案
  • C.直连对方数据库表
  • D.同步远程调用链路

答案:B。协同模式没有中央调度,靠“提交后发布事件、下游订阅事件”形成事件链递进推进。直连对方数据库会破坏服务边界,同步 RPC 是编排式而非协同式的典型做法。

Q5. 设计 Saga 补偿事务时,应满足哪些要求?

多选题 · mid

  • A.幂等可重试← 正确答案
  • B.可重复执行← 正确答案
  • C.顺序可颠倒
  • D.须处理空补偿← 正确答案

答案:ABD。补偿必须幂等且可重复执行(同一退订调用多次结果一致),也要处理“事务从未执行”的空补偿。补偿顺序不可颠倒——必须先退最后成功的步骤,否则依赖关系会断裂。

Q6. 关于 Saga 模式隔离性的描述,正确的是?

单选题 · mid

  • A.具备全局隔离能力
  • B.中间状态全局可见← 正确答案
  • C.自动避免悬挂更新
  • D.与本地事务隔离相同

答案:B。Saga 各步骤独立提交、无全局锁,因此中间状态对其他事务可见,这正是“隔离缺失”的表现。它不具备全局隔离,也不会自动避免悬挂更新,必须靠语义锁、快照等手段缓解。

Q7. 以下哪些做法可以增强 Saga 的可靠性?

多选题 · mid

  • A.消息队列持久化← 正确答案
  • B.超时重试与幂等键← 正确答案
  • C.定期故障注入演练← 正确答案
  • D.关闭事务流水日志

答案:ABC。消息持久化保证事件不丢,超时重试配合幂等键保证“重试不重复生效”,故障注入演练能提前暴露补偿链缺陷。关闭事务日志会让恢复失去依据,属于错误做法。

Q8. 一个 5 步 Saga 在第 3 步执行失败(未提交),编排器应补偿哪些事务?

单选题 · hard

  • A.前三个步骤
  • B.前两个步骤← 正确答案
  • C.全部五个步骤
  • D.仅第三个步骤

答案:B。第 3 步失败即未提交,无需补偿它自己;需要补偿的是已经提交的前两步。这就像第三家供应商拒绝了订单,老周只需退掉前两家已确认的预订。

Q9. 关于 Saga 与隔离性/MVCC 的表述,正确的有?

多选题 · hard

  • A.缺少全局隔离默认← 正确答案
  • B.语义锁可缓解冲突← 正确答案
  • C.快照彻底消除问题
  • D.补偿账本辅助恢复← 正确答案

答案:ABD。Saga 默认缺少全局隔离;语义锁(预占)能缓解并发冲突;补偿账本记录补偿历史,支持审计与对账。MVCC 快照只能缓解读问题,不能彻底消除隔离问题,故 C 错误。

Q10. 两个并发 Saga 操作同一账户:Saga A 转出资金后尚未补偿,Saga B 读到该中间状态并继续扣款;随后 A 执行补偿回滚。这一异常属于典型的?

单选题 · extreme

  • A.普通脏读问题
  • B.悬挂更新异常← 正确答案
  • C.不可重复读
  • D.数据库死锁

答案:B。Saga A 提交的转出会在后续被补偿撤销,B 基于这条“终将被消除”的中间状态继续扣款,正是标准的悬挂更新(dangling update)。它与单事务脏读的区别在于:数据本身已提交,只是语义上会被追认撤销。