微服务架构中的分布式事务实战:基于 Saga 模式的设计、落地与避坑-98
在单体应用里,我们习惯了“一个本地事务包打天下”:下单、扣库存、扣余额、写日志,要么一起成功,要么一起回滚。但一旦拆成微服务,这件事马上变复杂了。订单服务、库存服务、支付服务各自有自己的数据库,本地事务边界清清楚楚,跨服务却没有一个天然的 ACID 事务管理器。
很多团队刚开始会想:那是不是上 2PC、XA 就完了?理论上可以,实战里常常不太划算:性能差、耦合高、数据库和中间件支持受限,出了问题排查还很痛苦。于是,Saga 模式成了很多业务系统在“可用性、复杂度、最终一致性”之间的主流折中方案。
这篇文章我会从工程落地角度,带你把 Saga 走一遍:为什么需要它、怎么设计、代码怎么写、常见坑怎么避、性能和安全上怎么补齐。重点不是背概念,而是尽量让你看完就能开工。
背景与问题
先看一个典型业务:用户创建订单。
流程大致是:
- 订单服务创建订单
- 库存服务冻结库存
- 支付服务扣减余额
- 订单服务把状态更新为“已确认”
如果是单体,这就是一个数据库事务;如果是微服务,则会变成:
- 订单库一笔本地事务
- 库存库一笔本地事务
- 支付库一笔本地事务
问题来了:
- 订单创建成功了,但库存冻结失败怎么办?
- 库存冻结成功了,但支付失败怎么办?
- 支付其实成功了,但网络超时导致上游以为失败,又怎么办?
- 服务重试时,重复扣款、重复解冻怎么避免?
这类问题的本质是:跨服务步骤可能部分成功,系统必须有能力“收拾残局”。
为什么 Saga 适合大多数业务系统
Saga 的核心思想不是“全局锁住,一次性提交”,而是:
- 把一个长事务拆成多个本地事务
- 每一步成功后进入下一步
- 如果中间某一步失败,就执行前面步骤对应的补偿动作
也就是说,Saga 追求的是:
- 最终一致性
- 业务可恢复
- 高可用与可扩展
而不是强一致意义上的即时回滚。
核心原理
Saga 通常有两种实现风格:
-
Choreography(事件编排/事件驱动)
- 各服务通过事件自行感知下一步
- 优点:解耦,天然适合事件流
- 缺点:流程分散,链路长时可观测性差
-
Orchestration(中心编排)
- 由一个 Saga Orchestrator 统一驱动流程
- 优点:流程清晰、便于审计和可视化
- 缺点:中心编排器本身要高可用
对于中级工程团队来说,我更建议优先从中心编排式 Saga入手。原因很现实:流程好看、故障好查、改需求也更稳。
Saga 的步骤模型
一个完整 Saga 通常包含:
- 正向动作(Action)
- 补偿动作(Compensation)
- 状态持久化
- 幂等控制
- 超时与重试策略
例如订单场景可以设计成:
| 步骤 | 正向动作 | 补偿动作 |
|---|---|---|
| 1 | 创建订单 | 取消订单 |
| 2 | 冻结库存 | 解冻库存 |
| 3 | 扣减余额 | 退款/回退余额 |
| 4 | 确认订单 | 标记失败或人工介入 |
要注意一个常被忽略的点:补偿不是数据库回滚,而是新的业务操作。
比如“扣款补偿”通常不是简单回滚 SQL,而是生成一笔退款流水。
流程图:Saga 正向与补偿
flowchart TD
A[开始创建订单] --> B[订单服务: 创建订单]
B --> C[库存服务: 冻结库存]
C --> D[支付服务: 扣减余额]
D --> E[订单服务: 确认订单]
E --> F[完成]
D -.失败.-> C1[触发补偿]
C1 --> C2[库存服务: 解冻库存]
C2 --> C3[订单服务: 取消订单]
C -.失败.-> B1[触发补偿]
B1 --> B2[订单服务: 取消订单]
时序图:编排式 Saga
sequenceDiagram
participant Client
participant Orchestrator as Saga编排器
participant OrderSvc as 订单服务
participant InventorySvc as 库存服务
participant PaymentSvc as 支付服务
Client->>Orchestrator: 创建订单请求
Orchestrator->>OrderSvc: createOrder()
OrderSvc-->>Orchestrator: success(orderId)
Orchestrator->>InventorySvc: reserveStock(orderId)
InventorySvc-->>Orchestrator: success
Orchestrator->>PaymentSvc: charge(orderId)
alt 支付成功
PaymentSvc-->>Orchestrator: success
Orchestrator->>OrderSvc: confirmOrder(orderId)
OrderSvc-->>Orchestrator: success
Orchestrator-->>Client: 下单成功
else 支付失败
PaymentSvc-->>Orchestrator: failed
Orchestrator->>InventorySvc: releaseStock(orderId)
Orchestrator->>OrderSvc: cancelOrder(orderId)
Orchestrator-->>Client: 下单失败
end
方案对比与取舍分析
在真正落地前,先把几个常见方案摆平,不然后面很容易“听上去都能做,实际上都没做透”。
1. 本地事务 + 人工补单
适合:
- 非核心链路
- 金额小、偶发一致性可接受
- 业务量不大
问题:
- 一旦出问题靠人兜底,规模上不去
- 没有统一补偿语义
2. 2PC / XA
适合:
- 强一致要求极高
- 参与者少
- 基础设施强约束、统一技术栈
问题:
- 性能损耗明显
- 资源锁持有时间长
- 微服务环境下适配成本高
3. TCC
适合:
- 核心资金链路
- 业务可以明确建模 Try / Confirm / Cancel
问题:
- 侵入性强
- 开发成本高
- 对业务建模能力要求高
4. Saga
适合:
- 订单、库存、履约、营销等长链路业务
- 能接受最终一致性
- 希望高可用、可扩展
问题:
- 补偿设计复杂
- 对幂等、顺序、可观测性要求高
- 不是所有业务都能自然补偿
一句话建议:
- 核心资金强一致:优先考虑 TCC 或账户体系隔离设计
- 普通跨服务业务流程:Saga 往往是更平衡的方案
实战代码(可运行)
下面我用一个Python 版简化可运行示例演示中心编排式 Saga。
这个例子不依赖数据库和消息队列,直接在内存里模拟流程,便于理解核心机制。
你可以把它保存为 saga_demo.py 并直接运行。
from dataclasses import dataclass, field
from typing import Dict, List
import uuid
class SagaError(Exception):
pass
@dataclass
class Order:
order_id: str
user_id: str
product_id: str
amount: int
status: str = "PENDING"
@dataclass
class OrderService:
orders: Dict[str, Order] = field(default_factory=dict)
def create_order(self, user_id: str, product_id: str, amount: int) -> str:
order_id = str(uuid.uuid4())
self.orders[order_id] = Order(
order_id=order_id,
user_id=user_id,
product_id=product_id,
amount=amount,
status="CREATED",
)
print(f"[OrderService] create_order success: {order_id}")
return order_id
def confirm_order(self, order_id: str):
order = self.orders[order_id]
if order.status == "CONFIRMED":
print(f"[OrderService] confirm_order idempotent: {order_id}")
return
order.status = "CONFIRMED"
print(f"[OrderService] confirm_order success: {order_id}")
def cancel_order(self, order_id: str):
order = self.orders.get(order_id)
if not order:
return
if order.status == "CANCELLED":
print(f"[OrderService] cancel_order idempotent: {order_id}")
return
order.status = "CANCELLED"
print(f"[OrderService] cancel_order success: {order_id}")
@dataclass
class InventoryService:
stock: Dict[str, int] = field(default_factory=lambda: {"SKU-1": 10})
reserved: Dict[str, int] = field(default_factory=dict)
def reserve_stock(self, order_id: str, product_id: str, count: int = 1):
if order_id in self.reserved:
print(f"[InventoryService] reserve_stock idempotent: {order_id}")
return
available = self.stock.get(product_id, 0)
if available < count:
raise SagaError("库存不足")
self.stock[product_id] -= count
self.reserved[order_id] = count
print(f"[InventoryService] reserve_stock success: {order_id}")
def release_stock(self, order_id: str, product_id: str):
if order_id not in self.reserved:
print(f"[InventoryService] release_stock idempotent: {order_id}")
return
count = self.reserved.pop(order_id)
self.stock[product_id] = self.stock.get(product_id, 0) + count
print(f"[InventoryService] release_stock success: {order_id}")
@dataclass
class PaymentService:
balances: Dict[str, int] = field(default_factory=lambda: {"U-1": 100})
charged_orders: Dict[str, int] = field(default_factory=dict)
fail_on_charge: bool = False
def charge(self, order_id: str, user_id: str, amount: int):
if order_id in self.charged_orders:
print(f"[PaymentService] charge idempotent: {order_id}")
return
if self.fail_on_charge:
raise SagaError("支付通道异常")
balance = self.balances.get(user_id, 0)
if balance < amount:
raise SagaError("余额不足")
self.balances[user_id] -= amount
self.charged_orders[order_id] = amount
print(f"[PaymentService] charge success: {order_id}")
def refund(self, order_id: str, user_id: str):
if order_id not in self.charged_orders:
print(f"[PaymentService] refund idempotent: {order_id}")
return
amount = self.charged_orders.pop(order_id)
self.balances[user_id] = self.balances.get(user_id, 0) + amount
print(f"[PaymentService] refund success: {order_id}")
class SagaOrchestrator:
def __init__(self, order_svc: OrderService, inventory_svc: InventoryService, payment_svc: PaymentService):
self.order_svc = order_svc
self.inventory_svc = inventory_svc
self.payment_svc = payment_svc
def create_order_saga(self, user_id: str, product_id: str, amount: int) -> str:
completed_steps: List[str] = []
order_id = None
try:
order_id = self.order_svc.create_order(user_id, product_id, amount)
completed_steps.append("create_order")
self.inventory_svc.reserve_stock(order_id, product_id, 1)
completed_steps.append("reserve_stock")
self.payment_svc.charge(order_id, user_id, amount)
completed_steps.append("charge")
self.order_svc.confirm_order(order_id)
completed_steps.append("confirm_order")
print(f"[Saga] success: {order_id}")
return order_id
except Exception as e:
print(f"[Saga] failed: {e}")
self.compensate(order_id, user_id, product_id, completed_steps)
raise
def compensate(self, order_id: str, user_id: str, product_id: str, completed_steps: List[str]):
print(f"[Saga] start compensation: {order_id}, steps={completed_steps}")
if "charge" in completed_steps:
self.payment_svc.refund(order_id, user_id)
if "reserve_stock" in completed_steps:
self.inventory_svc.release_stock(order_id, product_id)
if "create_order" in completed_steps:
self.order_svc.cancel_order(order_id)
print(f"[Saga] compensation done: {order_id}")
def print_state(order_svc, inventory_svc, payment_svc):
print("\n===== CURRENT STATE =====")
print("orders:", order_svc.orders)
print("stock:", inventory_svc.stock)
print("reserved:", inventory_svc.reserved)
print("balances:", payment_svc.balances)
print("charged_orders:", payment_svc.charged_orders)
print("=========================\n")
if __name__ == "__main__":
order_svc = OrderService()
inventory_svc = InventoryService()
payment_svc = PaymentService()
orchestrator = SagaOrchestrator(order_svc, inventory_svc, payment_svc)
print("=== 场景1:成功下单 ===")
try:
orchestrator.create_order_saga("U-1", "SKU-1", 20)
except Exception as e:
print("unexpected error:", e)
print_state(order_svc, inventory_svc, payment_svc)
print("=== 场景2:支付失败,触发补偿 ===")
payment_svc.fail_on_charge = True
try:
orchestrator.create_order_saga("U-1", "SKU-1", 30)
except Exception as e:
print("expected error:", e)
print_state(order_svc, inventory_svc, payment_svc)
运行后你会看到什么
- 场景 1:订单成功创建、库存冻结、支付成功、订单确认
- 场景 2:支付失败后触发补偿,库存解冻、订单取消
这个例子虽然简化,但它已经包含 Saga 落地最关键的几个骨架:
- 正向步骤
- 逆向补偿
- 幂等接口
- 编排器集中控制
从示例走向生产:关键设计补齐
上面的代码能帮助理解机制,但真实系统还需要补上几块基础设施。
1. Saga 状态持久化
编排器不能只靠内存维护流程,否则服务一重启,运行中的 Saga 就丢了。
通常需要一张状态表,例如:
CREATE TABLE saga_instance (
saga_id VARCHAR(64) PRIMARY KEY,
business_key VARCHAR(128) NOT NULL,
saga_type VARCHAR(64) NOT NULL,
state VARCHAR(32) NOT NULL,
current_step VARCHAR(64),
context_json TEXT NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
再配一张步骤执行表:
CREATE TABLE saga_step_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
saga_id VARCHAR(64) NOT NULL,
step_name VARCHAR(64) NOT NULL,
action_status VARCHAR(32) NOT NULL,
compensation_status VARCHAR(32),
idempotency_key VARCHAR(128) NOT NULL,
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
2. 本地事务 + 消息投递一致性
如果你的 Saga 是通过消息驱动各个服务执行,那么要避免:
- 数据库更新成功,但消息没发出去
- 消息发出去了,但数据库事务回滚了
常见做法:
- Outbox Pattern
- 本地事务写业务数据 + outbox 事件表
- 后台任务异步投递消息
示意流程:
flowchart LR
A[业务请求] --> B[本地事务]
B --> C[写业务表]
B --> D[写Outbox事件表]
D --> E[消息投递器]
E --> F[MQ]
F --> G[下游服务消费]
我个人很推荐这套,因为它比“代码里先写库再发 MQ”稳定得多,排查时也有据可查。
3. 幂等键设计
每个步骤都应该支持幂等,至少保证:
- 重试不会重复扣款
- 重试不会重复扣库存
- 补偿重试不会重复退款
典型幂等键可以是:
sagaId + stepNamebusinessId + actionType- 请求头中的
Idempotency-Key
常见坑与排查
这部分是我觉得最值钱的。很多团队不是不会写 Saga,而是写出来之后,线上一复杂就开始失控。
坑 1:把补偿当成“数据库回滚”
这是最常见误区。
错误理解:
- 扣款失败就 rollback
- 库存失败就 rollback
真实情况:
跨服务步骤一旦提交,本地事务已经结束,无法像单库事务那样统一回滚。你只能发起新的业务动作去“抵消影响”。
排查建议
如果你发现补偿逻辑只是简单修改某个状态字段,先问自己:
- 这一步有没有对外部系统产生副作用?
- 有没有账户流水、库存流水、优惠券核销记录?
- 只改状态是否足以恢复业务语义?
如果答案是否定的,说明补偿设计还不完整。
坑 2:补偿接口没做幂等
现实里,超时、重试、消息重复投递是常态,不是异常。
比如支付补偿:
- 编排器第一次调用退款,支付服务其实已经成功退款
- 但响应在网络中丢了
- 编排器超时后再次发起退款
- 结果重复退款
解决办法
在补偿服务里落幂等记录,例如:
CREATE TABLE idempotency_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
idempotency_key VARCHAR(128) NOT NULL UNIQUE,
operation VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
response_json TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
处理逻辑大致是:
- 先查幂等键是否存在
- 存在且成功,直接返回旧结果
- 不存在则执行业务并落库
坑 3:步骤顺序没问题,补偿顺序错了
Saga 的补偿一般要按逆序执行。
例如:
- 创建订单
- 冻结库存
- 扣款
失败后应当:
- 退款
- 解冻库存
- 取消订单
如果你先取消订单再退款,可能会引发:
- 对账系统无法关联
- 用户侧状态与资金状态短暂冲突
- 人工排查链路变乱
排查建议
把每个步骤都当成“有外部观察者的独立业务动作”,问自己:
- 谁依赖这个状态?
- 哪一步最难恢复?
- 哪一步需要最后收口?
坑 4:把所有失败都自动补偿
不是所有失败都适合立即补偿。
举个例子:
- 支付服务超时,究竟是失败了,还是其实成功了但响应丢失?
如果这时直接退款,可能会出现“未扣款先退款”这种荒诞情况。
更稳的做法
引入中间状态:
PAYMENT_PENDINGCOMPENSATINGMANUAL_REVIEW
先查交易结果,再决定是否补偿。
状态机示意
stateDiagram-v2
[*] --> CREATED
CREATED --> STOCK_RESERVED
STOCK_RESERVED --> PAYMENT_PENDING
PAYMENT_PENDING --> CONFIRMED: 支付成功
PAYMENT_PENDING --> COMPENSATING: 支付明确失败
PAYMENT_PENDING --> MANUAL_REVIEW: 超时/结果未知
COMPENSATING --> CANCELLED
MANUAL_REVIEW --> CONFIRMED: 补查成功
MANUAL_REVIEW --> CANCELLED: 补查失败并补偿
坑 5:可观测性不足,出了问题只能翻日志
线上 Saga 最大的问题通常不是失败本身,而是你不知道它卡在哪一步。
至少要做到:
- 每个 Saga 有唯一
sagaId - 每个步骤有状态
- 正向动作和补偿动作都可查询
- 日志、Trace、指标能串起来
最低可用观测面板建议
- 成功率
- 平均耗时
- 每一步失败率
- 补偿次数
- 超时次数
- 人工介入数量
安全/性能最佳实践
Saga 不只是“事务”问题,到了生产环境,安全和性能必须一起考虑。
安全最佳实践
1. 补偿接口必须鉴权
很多团队会把退款、解冻、取消订单这类接口暴露为内部 API,然后默认认为“内网就是安全的”。这是危险的。
建议:
- 服务间调用必须有身份认证
- 对高风险动作增加签名或 mTLS
- 补偿接口只允许编排器或指定系统调用
2. 防止重放攻击
如果补偿请求被重复提交,幂等能解决一部分问题,但还不够。
建议加上:
- 请求时间戳
- 签名
- nonce
- 有效期校验
3. 敏感上下文不要全量落日志
Saga 上下文常常包含:
- 用户 ID
- 订单号
- 支付流水
- 金额信息
日志中要做脱敏,不要把完整支付参数、鉴权头、银行卡标识直接打印出来。
性能最佳实践
1. 缩短单步骤耗时
Saga 本质上是长事务,如果每一步都很慢,积压会快速放大。
建议:
- 步骤接口做瘦身
- 避免同步做非关键操作
- 对通知、埋点、审计走异步
2. 给补偿留容量
很多系统只评估正向 TPS,不评估补偿峰值。
一旦下游故障,补偿流量可能瞬间放大。
简单估算思路:
- 正向峰值:1000 TPS
- 支付步骤失败率:10%
- 每次失败平均触发 2 个补偿调用
那么补偿峰值可能接近:
1000 * 10% * 2 = 200 次/秒
如果失败率短时升到 50%,补偿洪峰就完全不是一个量级。
3. 重试要有退避和熔断
不要无脑立刻重试。
推荐策略:
- 指数退避
- 最大重试次数
- 死信队列
- 熔断保护
- 人工介入兜底
4. 避免跨服务大对象传递
Saga 上下文尽量轻量,只传:
- 业务主键
- 操作类型
- 必要金额/数量
- 幂等键
- Trace 信息
不要把完整订单快照塞进每条消息里,不然链路开销和变更成本都很高。
一套更务实的落地建议
如果你准备在团队内推动 Saga,我建议按下面顺序来,而不是一上来就搞“企业级大而全平台”。
第一步:先挑一个可补偿、边界清晰的业务
比如:
- 下单 + 冻结库存 + 扣余额
- 优惠券锁定 + 订单确认
- 预约单创建 + 资源占用
不要一开始就碰最复杂的资金总账。
第二步:先做编排式,而不是纯事件放飞
对于多数团队,中心编排能更快形成统一标准:
- Saga 定义
- 步骤接口规范
- 幂等规范
- 状态表结构
- 失败告警规则
第三步:先把“失败可见”做出来
早期最重要的不是 100% 自动恢复,而是:
- 失败能看到
- 失败能重试
- 失败能人工接管
这一点我非常有感触。很多系统不是死于补偿做不到,而是死于失败根本没人知道。
第四步:最后再平台化
当你已经有多个业务都在跑 Saga,再去抽象:
- Saga SDK
- 编排 DSL
- 通用状态机
- 重试与补偿框架
- 管控后台
这样成功率更高。
总结
Saga 不是银弹,但它确实是微服务时代处理分布式事务最实用的一类方案。它解决问题的方式,不是追求跨服务强一致,而是承认分布式系统会失败,然后通过本地事务 + 补偿机制 + 幂等控制 + 状态持久化来实现最终一致。
如果你只记住几条,请记这几条:
- 补偿不是回滚,是新的业务动作
- 每个步骤和补偿都必须幂等
- 流程状态必须持久化,不能只放内存
- 结果未知时不要盲目补偿,要先补查
- 先做好可观测性,再谈自动化恢复
- Saga 适合最终一致性业务,不适合所有强一致场景
最后给一个可执行建议:
如果你的系统当前还在“跨服务调用 + 失败靠人工修”的阶段,不必一步到位。先从一个核心但可控的业务流程开始,做一个最小可用的编排式 Saga,把状态表、幂等表、补偿接口、告警面板先建起来。你会发现,很多所谓“分布式事务难题”,其实在工程上是可以逐步拆掉的。
而真正难的,不是写出一段 Saga 代码,而是把它变成一套遇到异常也能稳定收场的业务能力。