跳转到内容

Sagas — 长事务拆成一串能"反向走回去"的小事务

是什么

Saga 是把一个跑很久的大事务,拆成一串小事务 T1..Tn;每个 Ti 一旦做完就立刻提交(不再持锁),同时事先准备好一个”反向操作” Ci。如果中途第 k 步崩了,系统按 C_{k-1}、C_{k-2}、…、C1 倒序把已经做过的事一个个补回去

日常类比:你订一次三段式自由行——先订机票、再订酒店、再订租车。不是等三件事全订好才付钱,而是订一件付一件。中途酒店订不到?把已经付好的机票退掉、租车取消,假装这趟旅行从没发生过。每一步退订就是它的”补偿事务(compensating transaction)”。

LLT = T1 ; T2 ; T3 ; ... ; Tn
若 Tk 失败:执行 C_{k-1} ; C_{k-2} ; ... ; C1

每个 Ti 是一个普通的 ACID 短事务,提交后别人能立刻看见它的结果。saga 不再要求”全过程外人看不见中间状态”——它用补偿换吞吐。

换句话说:传统 ACID 让你的整段流程像”一颗原子”,外人要么看到全部要么看到无;saga 让流程变成”一串可见的小步”,靠业务层的补偿来保证最终结果”看起来像原子”。

为什么重要

  • 不理解 saga,你解释不了为什么微服务下单能跨”库存/支付/物流”三个数据库还能”一起成功或一起退回”
  • 不理解 saga,你会以为分布式事务只有 2PC 一条路,结果 2PC 一上线性能直接趴下
  • 不理解 saga,你会把”补偿”和”回滚”混成一回事,写出语义不对的退款逻辑
  • 不理解 saga,你看不懂 Temporal、Camunda、Axon 这些”工作流编排器”在编排什么

核心要点

Saga 的精髓可以拆成 三件事

  1. ——把一个原本要持锁几十分钟的 LLT,切成若干”几百毫秒就能跑完”的本地短事务。类比:把一根 30 米的水管换成 30 段每段 1 米能拧上拧下的接头。

  2. 每段配反向——为每个 Ti 写一个 Ci,使得 “Ti ; Ci” 在业务语义上等价于”什么都没做”。注意是业务语义,不是数据库 rollback——钱已经转出去了再转回来,账本上多了两条互相抵消的流水。

  3. 失败按倒序补——记录哪些 Ti 已提交,崩了就从最近一步开始反向跑 Ci。两类失败模式:backward recovery(一路补到头,整体放弃)和 forward recovery(用 savepoint 重试 Tk,继续往前)。

论文还给出一条隐含约束:Ci 自己也是一个普通短事务,它必须能在数据库当时的状态下成功,否则 saga 卡死成”半态”。所以补偿也要写得能容错。

代价是隔离性丢了——T2 还没跑时,外人能读到 T1 的结果,业务必须能容忍这种”半成品中间态”。

实践案例

案例 1:订机票 + 订酒店 + 订租车

T1 BookFlight() C1 CancelFlight()
T2 BookHotel() C2 CancelHotel()
T3 BookCar() C3 CancelCar()

执行 T1 T2 都成功,T3 找不到合适车型 → 跑 C2 退酒店,再跑 C1 退机票。三家供应商之间没有共享数据库,2PC 根本没法跨它们做。saga 在这种”跨组织边界”场景里几乎是唯一现实选项。

案例 2:银行跨行转账

T1: A 行扣 100 C1: A 行入账 100(标"撤销")
T2: B 行入账 100 C2: B 行扣 100

如果 T2 失败:跑 C1,A 行重新拿回 100。踩坑警告:C1 不是 SQL 的 ROLLBACK,是新写一条入账流水。账本上会留下两条记录(扣 100 + 撤销入 100),余额回到原状。审计能看到这次失败的尝试——这是正资产不是 bug。

案例 3:微服务下单 saga(伪代码)

def place_order(order):
try:
reserve_inventory(order) # T1
try:
charge_payment(order) # T2
try:
create_shipment(order) # T3
except:
refund_payment(order) # C2
release_inventory(order) # C1
raise
except:
release_inventory(order) # C1
raise
except:
order.status = "failed"

工业界两种实现风格:编排型(orchestration)——一个中心 service 当指挥,发命令收回执;协同型(choreography)——每个 service 监听事件,自己决定下一步发什么事件。前者好调试,后者更去中心。

踩过的坑

  1. 把补偿当 rollback 用——补偿不是”回到提交前那一秒”,是”再发一笔抵消的事务”。中间别人可能已经看到、引用了 T1 的结果(比如已发短信”扣款成功”);C1 不会把短信收回来。

  2. 补偿必须幂等——网络重试是常态,同一个 Ci 可能被触发两次。如果”退款 100”写得不幂等,重发一次就退了 200。给每个补偿带一个唯一 saga_id + step_id,做去重表。

  3. 不是所有动作都能补偿——发出去的邮件、按下的物理按钮、第三方扣过费且不允许冲正的接口。这种步骤要么放在 saga 最后(成功才发),要么改成”先预留再确认”两阶段。

  4. 隔离性彻底丢了——T1 提交到 T2 开始之间,外人能读到”半个 saga”的脏状态。业务上要么接受脏读,要么用语义锁(业务字段 status=‘pending’)告诉别人”这个还没定”。

适用 vs 不适用场景

适用

  • 长流程业务(订单/审批/批量结算),每一步都能找到补偿动作
  • 跨多个独立系统/数据库,没法做 2PC 的场景(旅行预订、跨行转账、微服务)
  • 吞吐和可用性优先,能接受短暂脏读
  • 流程步骤之间松耦合,可以单步重试或单步退回

不适用

  • 强隔离性要求的金融核心账务、证券撮合 → 用真正的分布式事务(2PC + 共识协议,或 spanner 这种 TrueTime 方案)
  • 中间步骤完全不可补偿(核武发射按钮)→ saga 救不了,得改业务流程
  • 短事务(毫秒级),ACID 单库就能搞定 → 不要为了用 saga 而 saga,bernstein-1981-cc 给你的串行化已经够了
  • 业务上不能容忍”中间脏态被外部读到”——saga 的隔离性比 ACID 弱,硬塞会出客户投诉

历史小故事(可跳过)

  • 1980 年代初:数据库圈在被”长事务”折磨——月结、报表、批处理一上锁,OLTP 整晚停摆。
  • 1987 年:Hector Garcia-Molina(普林斯顿)和 Kenneth Salem 在 SIGMOD 发表 9 页的 “Sagas”,提出补偿事务的形式定义。
  • 1990s 至 2000s:论文一直比较冷门——主流数据库走 aries-1992 这种重 redo/undo 的 ACID 路线。
  • 2014 年前后:微服务热起来,Chris Richardson 等人重新挖出 saga 模式作为”跨服务事务”的官方答案。
  • 今天:Temporal、Camunda、Axon Framework 都把 saga 作为一等公民工作流原语;几乎所有”分布式事务”教程都从 saga 讲起。

学到什么

  • 隔离性可以换出来——用一致性最终性 + 业务可补偿性,换吞吐和可用
  • 补偿是新事务,不是 rollback——这个区分决定了你能不能正确写出退款代码
  • 能不能用 saga 是业务问题不是技术问题——技术上 saga 总能写,但业务上得有”反向语义”
  • 27 年的论文今天才大火——好想法可能要等到工程现实赶上来才被人需要
  • 写补偿时多想”如果重发会怎样 / 如果别人已经看见会怎样”——这两个问题答得清,saga 才真的能跑生产

延伸阅读

关联

  • aries-1992 —— 单数据库重事务恢复的标杆,saga 是它在跨服务场景的”反义词”
  • bernstein-1981-cc —— 串行化与并发控制综述;saga 主动放弃了其中”隔离性”换工程现实
  • gray-1981-transaction —— Jim Gray 1981 的事务概念奠基论文,saga 是它的扩展变体
  • gray-1978-notes —— Gray 的事务实现笔记;saga 论文里的实现讨论延续这一脉
  • brewer-cap-2000 —— CAP 理论;saga 是 AP 路线在事务层的具体落地手段
  • spanner —— Google 用 TrueTime + 2PC 做强一致跨区事务,与 saga 形成两端对照
  • paxos —— 共识协议;saga 不靠共识靠补偿,理解二者区别才理解分布式事务谱系

反向链接

  • aries-1992 —— ARIES 1992 — 数据库崩溃后怎么把账目对回来
  • bernstein-1981-cc —— Bernstein 1981 并发控制综述 — 把分布式数据库的 20+ 算法整成两条主线
  • brewer-cap-2000 —— Brewer CAP — 网络一断电,一致性和可用性只能留一个
  • gray-1978-notes —— Gray 1978 — 数据库操作系统讲义,事务/2PL/2PC/恢复一次讲完
  • gray-1981-transaction —— Gray 1981 — 把”事务”提升为通用抽象
  • helland-2007 —— Life Beyond Distributed Transactions — 大规模系统下放弃跨机事务的宣言
  • paxos —— Paxos — 分布式共识算法
  • spanner —— Spanner — 全球分布式 SQL 数据库