背景与问题
在单体应用里,事务通常靠数据库的 ACID 就能解决:一段业务逻辑包在一个本地事务里,要么一起成功,要么一起回滚。
但到了微服务架构,事情就没这么简单了。
比如一个典型下单流程:
- 订单服务创建订单
- 库存服务扣减库存
- 支付服务扣款
- 积分服务发放积分
如果这些服务各自都有自己的数据库,那么“一个全局事务”就很难直接成立。很多团队第一反应会想到 2PC/XA,但在真实生产环境里,它常常带来几个问题:
- 服务之间强耦合
- 协调器成为瓶颈
- 长事务拖垮吞吐
- 网络抖动下容易卡死
- 云原生和多语言环境支持并不统一
所以,很多微服务系统最后会走向 最终一致性,而 Saga 模式就是其中非常实用的一种。
不过,Saga 不是“用了就万事大吉”。我见过不少项目,流程图画得很漂亮,线上一出问题就开始人工改单、补库存、对账修数据。原因通常不是 Saga 理论不对,而是实现细节没做好:
- 补偿动作不幂等
- 消息重复消费
- 状态机不完整
- 中间步骤超时后无人兜底
- 失败链路缺少可观测性
这篇文章我就从 “怎么设计、怎么写、怎么排障” 三个角度,带你把 Saga 真正落到地上。
背景案例:一个最容易出事故的下单流程
我们先约定一个简化业务:
OrderService:创建订单InventoryService:预留库存 / 释放库存PaymentService:扣款 / 退款
目标流程:
- 创建订单成功
- 预留库存成功
- 扣款成功
- 最终订单变为
CONFIRMED
如果扣款失败,则:
- 释放库存
- 取消订单
这就是一个典型的 Saga。
为什么这里不用“强一致”?
因为订单、库存、支付本来就是不同领域,不适合共享数据库;而且支付通常还可能是外部系统,根本不支持你做数据库级别分布式事务。
所以在这个场景里,正确的问题不是“怎么做到绝对原子”,而是:
当局部步骤成功、后续步骤失败时,系统如何可靠地执行补偿,并把状态收敛到业务可接受的一致状态?
核心原理
Saga 的核心思想可以概括成一句话:
把一个长事务拆成多个本地事务,每个本地事务都有对应的补偿操作;当前向流程失败时,按相反顺序执行补偿。
Saga 一般有两种实现风格:
1. 编排式(Orchestration)
由一个中心协调者负责驱动整个流程:
- 调订单服务
- 调库存服务
- 调支付服务
- 某一步失败则统一触发补偿
优点:
- 流程清晰
- 状态集中,便于排查
- 对复杂业务更友好
缺点:
- 协调器逻辑会变重
- 中心节点需要高可用设计
2. 协同式(Choreography)
每个服务消费事件并发布下一个事件:
- 订单已创建
- 库存已预留
- 支付已完成
- 失败则发布回滚事件
优点:
- 解耦更强
- 看起来更“事件驱动”
缺点:
- 业务链长时,事件流很难跟踪
- 排障困难
- 容易形成“隐式流程”
对于中级工程师实际落地,我通常建议:
核心交易链路优先用编排式 Saga,尤其是订单、支付、履约这类需要强可观测性的场景。
Saga 的状态流转
stateDiagram-v2
[*] --> PENDING
PENDING --> ORDER_CREATED: 创建订单
ORDER_CREATED --> INVENTORY_RESERVED: 预留库存成功
INVENTORY_RESERVED --> PAYMENT_COMPLETED: 支付成功
PAYMENT_COMPLETED --> CONFIRMED: 订单确认
INVENTORY_RESERVED --> COMPENSATING: 支付失败
ORDER_CREATED --> COMPENSATING: 库存失败
COMPENSATING --> CANCELLED: 补偿完成
COMPENSATING --> FAILED: 补偿失败需人工介入
CONFIRMED --> [*]
CANCELLED --> [*]
FAILED --> [*]
这个状态图有两个重点:
- 业务完成状态 和 补偿中状态 要明确区分
- “失败”不等于“回滚完成”,中间还有一个 COMPENSATING 阶段
很多系统的问题就出在这里:日志里写了“支付失败”,但数据库里订单状态还是“处理中”,过几小时没人管,最终变成脏数据。
一次完整 Saga 的时序
sequenceDiagram
participant Client
participant Orchestrator as Saga协调器
participant Order
participant Inventory
participant Payment
Client->>Orchestrator: 发起下单
Orchestrator->>Order: createOrder()
Order-->>Orchestrator: orderId
Orchestrator->>Inventory: reserve(orderId, sku, count)
Inventory-->>Orchestrator: reserved
Orchestrator->>Payment: charge(orderId, amount)
Payment-->>Orchestrator: failed
Orchestrator->>Inventory: release(orderId, sku, count)
Inventory-->>Orchestrator: released
Orchestrator->>Order: cancelOrder(orderId)
Order-->>Orchestrator: cancelled
Orchestrator-->>Client: 下单失败,已补偿
设计落地时最容易被忽略的 5 个原则
在开始写代码之前,先把几个设计原则说透,不然后面排障会非常痛苦。
1. 补偿不是数据库回滚
补偿是一个新的业务动作。
例如:
- 扣库存的补偿不是“数据库 rollback”
- 而是“新增一条释放库存的业务操作”
这意味着补偿也要:
- 记录日志
- 可重试
- 可幂等
- 可审计
2. 每个步骤都要幂等
因为现实里会发生:
- 消息重复投递
- 协调器重试
- 服务超时但实际已执行成功
- 网络闪断导致响应丢失
所以接口必须支持类似下面的语义:
reserve(orderId)重复调用只生效一次release(orderId)重复调用不会多释放cancelOrder(orderId)多次调用结果一致
3. 前向操作和补偿操作都要持久化状态
不能只靠内存变量判断流程走到哪一步。服务重启后,内存就没了。
至少要持久化:
- Saga 实例 ID
- 当前步骤
- 每步执行结果
- 补偿状态
- 重试次数
- 最后错误信息
4. 超时不等于失败,但必须进入待确认流程
这是线上高频坑。
比如支付服务 3 秒超时,你的协调器把它判为失败,开始退款/回滚;但第 4 秒支付网关其实已经扣款成功了。于是你就会得到:
- 用户支付成功
- 订单却被取消
- 库存被释放
这种情况必须通过 查询确认 或 异步回执 收敛,而不是简单地“超时即补偿”。
5. 人工介入是系统能力的一部分
别把“人工兜底”理解成系统设计失败。真正成熟的系统都会留出:
- 卡单列表
- 补偿重试按钮
- 手工确认入口
- 对账修复任务
因为外部支付、第三方物流、短信、风控这些系统,永远可能出现不可自动收敛的异常。
实战代码(可运行)
下面我们用 Python 写一个 可运行的编排式 Saga 示例。它不依赖外部中间件,但会模拟真实问题:
- 库存预留
- 支付扣款
- 支付失败后触发补偿
- 幂等控制
- 状态记录
你可以直接保存为 saga_demo.py 运行。
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Set
import random
class SagaStatus(str, Enum):
PENDING = "PENDING"
ORDER_CREATED = "ORDER_CREATED"
INVENTORY_RESERVED = "INVENTORY_RESERVED"
PAYMENT_COMPLETED = "PAYMENT_COMPLETED"
CONFIRMED = "CONFIRMED"
COMPENSATING = "COMPENSATING"
CANCELLED = "CANCELLED"
FAILED = "FAILED"
@dataclass
class SagaLog:
saga_id: str
status: SagaStatus = SagaStatus.PENDING
steps: list = field(default_factory=list)
retries: int = 0
last_error: str = ""
class OrderService:
def __init__(self):
self.orders: Dict[str, str] = {}
def create_order(self, order_id: str):
if order_id in self.orders:
return
self.orders[order_id] = "CREATED"
def cancel_order(self, order_id: str):
if order_id not in self.orders:
return
self.orders[order_id] = "CANCELLED"
def confirm_order(self, order_id: str):
if order_id not in self.orders:
raise ValueError("order not found")
self.orders[order_id] = "CONFIRMED"
class InventoryService:
def __init__(self, initial_stock: int):
self.stock = initial_stock
self.reserved_orders: Set[str] = set()
def reserve(self, order_id: str, amount: int):
# 幂等:重复预留不重复扣减
if order_id in self.reserved_orders:
return
if self.stock < amount:
raise ValueError("insufficient stock")
self.stock -= amount
self.reserved_orders.add(order_id)
def release(self, order_id: str, amount: int):
# 幂等:未预留过则直接返回
if order_id not in self.reserved_orders:
return
self.stock += amount
self.reserved_orders.remove(order_id)
class PaymentService:
def __init__(self):
self.paid_orders: Set[str] = set()
self.refunded_orders: Set[str] = set()
def charge(self, order_id: str, amount: int, force_fail: bool = False):
# 幂等:重复扣款不重复收费
if order_id in self.paid_orders:
return
if force_fail:
raise ValueError("payment failed")
self.paid_orders.add(order_id)
def refund(self, order_id: str, amount: int):
# 幂等:未支付过则无需退款;已退款则忽略
if order_id not in self.paid_orders:
return
if order_id in self.refunded_orders:
return
self.refunded_orders.add(order_id)
class SagaOrchestrator:
def __init__(self, order_service, inventory_service, payment_service):
self.order_service = order_service
self.inventory_service = inventory_service
self.payment_service = payment_service
self.logs: Dict[str, SagaLog] = {}
def create_log(self, saga_id: str):
self.logs[saga_id] = SagaLog(saga_id=saga_id)
def execute(self, saga_id: str, order_id: str, stock_amount: int, pay_amount: int, force_payment_fail=False):
self.create_log(saga_id)
log = self.logs[saga_id]
try:
self.order_service.create_order(order_id)
log.status = SagaStatus.ORDER_CREATED
log.steps.append("create_order")
self.inventory_service.reserve(order_id, stock_amount)
log.status = SagaStatus.INVENTORY_RESERVED
log.steps.append("reserve_inventory")
self.payment_service.charge(order_id, pay_amount, force_fail=force_payment_fail)
log.status = SagaStatus.PAYMENT_COMPLETED
log.steps.append("charge_payment")
self.order_service.confirm_order(order_id)
log.status = SagaStatus.CONFIRMED
log.steps.append("confirm_order")
except Exception as e:
log.last_error = str(e)
log.status = SagaStatus.COMPENSATING
self.compensate(log, order_id, stock_amount, pay_amount)
return log
def compensate(self, log: SagaLog, order_id: str, stock_amount: int, pay_amount: int):
try:
# 补偿顺序与前向顺序相反
self.payment_service.refund(order_id, pay_amount)
log.steps.append("refund_payment")
self.inventory_service.release(order_id, stock_amount)
log.steps.append("release_inventory")
self.order_service.cancel_order(order_id)
log.steps.append("cancel_order")
log.status = SagaStatus.CANCELLED
except Exception as e:
log.last_error = f"compensation failed: {e}"
log.status = SagaStatus.FAILED
if __name__ == "__main__":
order_service = OrderService()
inventory_service = InventoryService(initial_stock=10)
payment_service = PaymentService()
orchestrator = SagaOrchestrator(order_service, inventory_service, payment_service)
print("=== 场景1:成功下单 ===")
result1 = orchestrator.execute(
saga_id="saga-001",
order_id="order-001",
stock_amount=2,
pay_amount=100,
force_payment_fail=False
)
print(result1)
print("orders =", order_service.orders)
print("stock =", inventory_service.stock)
print()
print("=== 场景2:支付失败,触发补偿 ===")
result2 = orchestrator.execute(
saga_id="saga-002",
order_id="order-002",
stock_amount=3,
pay_amount=150,
force_payment_fail=True
)
print(result2)
print("orders =", order_service.orders)
print("stock =", inventory_service.stock)
运行结果你应该关注什么
成功场景下:
- 订单状态变成
CONFIRMED - 库存减少
- Saga 状态为
CONFIRMED
失败场景下:
- 支付失败
- Saga 进入
COMPENSATING - 补偿执行成功后,订单状态为
CANCELLED - 库存恢复
- Saga 状态为
CANCELLED
从示例走向生产:状态表设计建议
如果要真正上生产,建议至少有一张 Saga 实例表。
CREATE TABLE saga_instance (
saga_id VARCHAR(64) PRIMARY KEY,
business_id VARCHAR(64) NOT NULL,
saga_type VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
current_step VARCHAR(64),
retries INT NOT NULL DEFAULT 0,
last_error TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
再配一张步骤明细表:
CREATE TABLE saga_step (
id BIGSERIAL PRIMARY KEY,
saga_id VARCHAR(64) NOT NULL,
step_name VARCHAR(64) NOT NULL,
action_type VARCHAR(16) NOT NULL, -- FORWARD / COMPENSATE
status VARCHAR(32) NOT NULL, -- SUCCESS / FAILED / PENDING
idempotency_key VARCHAR(128),
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
为什么要拆两张表?
saga_instance方便查当前流程saga_step方便排查哪个步骤失败、重试了几次、补偿做到哪一步
我个人经验是:
只存总状态不存步骤明细,线上排障会极其痛苦。
现象复现:线上最常见的 4 类故障
troubleshooting 文章不能只讲原理,下面我们直接按“事故现象”来拆。
故障 1:订单取消了,但库存没恢复
常见现象
- 用户下单失败
- 订单状态是
CANCELLED - 库存却少了
- 后台需要人工加库存
可能原因
- 补偿顺序写错
- 释放库存接口调用失败但未重试
- 释放库存接口不是幂等,重试时抛异常
- 协调器先更新订单为取消,再去释放库存,中途崩了
定位路径
建议按下面顺序查:
- 查 Saga 实例状态
- 查 Saga 步骤表里
release_inventory是否执行 - 查库存服务日志是否收到请求
- 查库存服务数据库有没有该
order_id的预留记录 - 查重试任务是否覆盖到该状态
止血方案
- 先做库存对账,把“订单取消但库存仍预留”的记录筛出来
- 批量补发
release_inventory - 临时把卡在
COMPENSATING的实例加入重试队列
故障 2:支付其实成功了,但订单显示失败
常见现象
- 用户投诉扣款成功
- 页面显示下单失败
- 订单被取消
- 财务对账发现多笔“有支付无订单”
根因往往是“超时误判”
协调器调用支付接口超时,于是触发补偿;但支付渠道后来实际成功了。
正确处理思路
对于支付类步骤,不要简单使用:
- “调用失败 = 支付失败”
- “超时 = 支付失败”
而应区分:
- 明确失败:可直接补偿
- 超时未知:进入
PAYMENT_UNKNOWN - 通过支付查询接口确认最终状态
- 或等待异步支付回执再决策
推荐状态机补充
flowchart TD
A[调用支付] --> B{返回结果}
B -->|成功| C[标记支付成功]
B -->|明确失败| D[触发补偿]
B -->|超时/未知| E[进入待确认状态]
E --> F{查询支付结果}
F -->|已支付| G[继续后续流程]
F -->|未支付| D
F -->|仍未知| H[人工介入或延迟重试]
这类问题我真的踩过,教训就是:
对接第三方支付时,“未知”必须单独建模,别偷懒合并到“失败”。
故障 3:补偿执行了两次,库存被多加
常见现象
- 一笔失败订单触发两次补偿
- 库存比实际多
- 日志里有重复的 release 请求
典型原因
- 消息重复消费
- 协调器重试
- 定时任务和手工处理重复触发
- 接口无幂等保护
排查重点
看补偿接口是否使用以下任一机制:
order_id唯一业务键- 幂等表
- 去重 token
- 状态机 CAS 更新
如果没有,基本就能锁定问题。
止血方案
- 先暂停重复消费任务
- 用业务主键对补偿请求去重
- 对已执行补偿的订单建立唯一约束或操作日志
故障 4:Saga 卡在处理中,永远不结束
常见现象
- Saga 状态一直是
PENDING/COMPENSATING - 用户侧查不到明确结果
- 数据库里堆积大量中间态
常见原因
- 服务调用成功但状态未落库
- 状态更新和消息发送不在同一原子操作里
- 重试任务只扫部分状态
- 某个补偿步骤失败后没有再次调度
推荐定位方法
按“三件事”查:
- 有没有执行
- 有没有记录
- 有没有继续推进
如果动作执行了但没记录,属于落库问题;
如果记录了但没推进,属于调度问题;
如果根本没执行,属于调用或消息问题。
常见坑与排查
这一节我不按理论讲,直接按“最容易写错”的点来列。
1. 把补偿当成简单反向操作
不是所有动作都有天然反操作。
例如:
- 发券了,不能简单“删掉券”,可能券已被用了
- 发短信了,无法撤回
- 推送通知了,也无法回滚
这种场景下不能硬套 Saga 的“反向撤销”,而要改成:
- 标记失效
- 发冲正消息
- 增加人工审核
边界条件非常重要:不是所有业务都适合 Saga。
2. 使用数据库事务包住远程调用
有些实现会这样做:
- 开本地数据库事务
- 写订单
- 调库存服务
- 调支付服务
- 最后提交数据库事务
这会导致:
- 本地锁持有时间过长
- 远程超时拖垮数据库连接
- 失败后状态更难判断
正确做法是:
- 本地事务只包本地状态更新
- 远程调用前后用状态机衔接
- 需要时配合 Outbox 模式保证消息可靠
3. 没有统一的幂等键
如果订单、库存、支付各服务都用不同主键,很容易乱。
建议统一使用:
saga_id:流程实例唯一标识business_id:比如order_idstep_name:步骤名idempotency_key:例如order-001:reserve
这样日志、接口、消息、数据库都能串起来。
4. 补偿失败后直接丢弃
补偿也可能失败,而且比前向操作更常见。
比如:
- 库存服务宕机
- 支付退款接口限流
- 订单服务数据已被人工修改
所以补偿失败后至少要有:
- 重试策略
- 退避机制
- 死信队列或失败表
- 人工处理面板
5. 缺少可观测性,排障全靠猜
一个成熟的 Saga 系统,最少要能看到:
- 每个 Saga 当前状态
- 每步耗时
- 每步请求参数摘要
- 重试次数
- 补偿次数
- 最近错误信息
- 关联日志 traceId
不然线上出问题时,只能“翻日志碰运气”。
安全/性能最佳实践
Saga 常被理解成“事务方案”,但它同样涉及安全和性能。
安全最佳实践
1. 补偿接口要有鉴权
补偿动作本质上能修改业务资产:
- 释放库存
- 发起退款
- 取消订单
这些接口绝不能裸奔暴露。至少要做:
- 服务间认证
- 请求签名
- 权限隔离
- 审计日志
2. 避免敏感数据在 Saga 日志里明文落盘
不要把以下内容直接写到 Saga 步骤表:
- 完整银行卡号
- 用户身份证号
- 完整支付凭证
- 明文手机号
建议只保存:
- 脱敏摘要
- 外部流水号
- 哈希或引用 ID
3. 人工补偿操作必须审计
后台“重试补偿”“强制完成”“手工取消”这类按钮,一定要记录:
- 操作人
- 时间
- 目标对象
- 原因
- 前后状态
否则出了资损问题,很难回溯。
性能最佳实践
1. 避免 Saga 步骤过细
有些系统把一个下单流程拆成十几步,导致:
- 状态复杂
- 重试链路变长
- 故障点增加
经验上建议:
- 一步对应一个明确业务子事务
- 不要把纯计算逻辑也拆成 Saga 步骤
- 把真正需要跨服务一致性的动作纳入 Saga
2. 重试要有退避,不要风暴式轰炸
如果支付服务故障,成千上万的 Saga 同时重试,会把下游彻底打死。
建议:
- 指数退避
- 加随机抖动
- 设置最大重试次数
- 熔断期间暂停部分补偿
3. 热点状态表要控制写放大
Saga 步骤很多时,状态表写入会非常频繁。可以考虑:
- 只记录关键状态变更
- 明细日志异步落盘
- 冷热数据分离
- 按时间归档历史记录
4. 补偿任务要支持分片扫描
如果全量扫 COMPENSATING 状态,数据一大就很慢。建议:
- 按时间窗口扫
- 按分库分表键分片
- 按状态 + 更新时间建索引
例如:
CREATE INDEX idx_saga_status_updated_at
ON saga_instance(status, updated_at);
一套实用的生产级落地清单
如果你准备在真实项目里用 Saga,我建议至少做到下面这些。
设计层
- 明确每个步骤的前向动作和补偿动作
- 识别哪些步骤存在“未知态”
- 补偿是否真的可逆
- 定义最终一致性的时间窗口
数据层
- Saga 实例表
- Saga 步骤表
- 幂等键或去重表
- 补偿失败记录表
工程层
- 状态机驱动而不是 if/else 到处飞
- 每步接口天然幂等
- 支持重试和人工介入
- 有 traceId 串联日志
运维层
- 卡单监控
- 补偿失败告警
- 对账任务
- 后台人工处理工具
总结
Saga 模式不是“分布式事务银弹”,但在微服务架构里,它是一个非常现实、可操作、能抗复杂网络环境的方案。
你真正要抓住的不是一句“最终一致性”,而是这几个落地关键点:
- 前向步骤和补偿步骤都要业务化设计
- 所有动作必须幂等
- 超时与失败要区分,未知态要单独建模
- 状态机必须完整,不能只记录成功和失败
- 补偿失败不是结束,必须可重试、可审计、可人工兜底
如果你的场景是订单、库存、支付这类核心链路,我的建议很明确:
- 优先使用 编排式 Saga
- 引入 状态表 + 步骤表
- 给每个步骤设计 幂等键
- 对支付、退款这类外部依赖加入 查询确认机制
- 一开始就建设 卡单监控和人工补偿能力
最后给一个边界判断:
- 如果业务步骤少、补偿清晰、允许短时间最终一致,Saga 很适合
- 如果动作不可逆、需要严格实时强一致、且外部系统不可控,Saga 就不是最佳答案,应该考虑重新拆分业务边界或调整业务规则
一句话收尾:
分布式事务最难的,从来不是“提交”,而是“出故障后还能把系统收回来”。Saga 的价值,正体现在这里。