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 的精髓可以拆成 三件事:
-
拆——把一个原本要持锁几十分钟的 LLT,切成若干”几百毫秒就能跑完”的本地短事务。类比:把一根 30 米的水管换成 30 段每段 1 米能拧上拧下的接头。
-
每段配反向——为每个 Ti 写一个 Ci,使得 “Ti ; Ci” 在业务语义上等价于”什么都没做”。注意是业务语义,不是数据库 rollback——钱已经转出去了再转回来,账本上多了两条互相抵消的流水。
-
失败按倒序补——记录哪些 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 监听事件,自己决定下一步发什么事件。前者好调试,后者更去中心。
踩过的坑
-
把补偿当 rollback 用——补偿不是”回到提交前那一秒”,是”再发一笔抵消的事务”。中间别人可能已经看到、引用了 T1 的结果(比如已发短信”扣款成功”);C1 不会把短信收回来。
-
补偿必须幂等——网络重试是常态,同一个 Ci 可能被触发两次。如果”退款 100”写得不幂等,重发一次就退了 200。给每个补偿带一个唯一 saga_id + step_id,做去重表。
-
不是所有动作都能补偿——发出去的邮件、按下的物理按钮、第三方扣过费且不允许冲正的接口。这种步骤要么放在 saga 最后(成功才发),要么改成”先预留再确认”两阶段。
-
隔离性彻底丢了——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 才真的能跑生产
延伸阅读
- 论文 PDF:Sagas, SIGMOD 1987(9 页,读起来不难)
- 视频:Chris Richardson — Microservices Saga Pattern(30 分钟,编排 vs 协同讲清楚)
- 文档:Microservices.io — Saga pattern(带实例代码)
- aries-1992 —— 单库 ACID 的”金标准”,与 saga 是两条不同路线
- bernstein-1981-cc —— 经典并发控制综述,是 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 数据库