微服务架构下分布式事务实战:基于 Saga 模式的订单系统一致性设计与落地
在单体时代,订单创建、库存扣减、支付处理、优惠券核销,往往都在一个本地事务里完成。BEGIN 一开,出错就 ROLLBACK,简单直接。
但一旦系统拆成微服务,这套办法就失灵了:订单服务有自己的库,库存服务有自己的库,支付服务又是另一套系统,传统 ACID 很难跨服务、跨数据库、跨消息中间件继续生效。
我在做订单系统时,最常见的问题不是“事务怎么提交”,而是“半成功怎么办”:
- 订单已经创建了,但库存没扣成功
- 库存扣了,支付超时了
- 支付成功了,订单状态却没更新
- 补偿执行了两次,把库存加多了
这篇文章就围绕一个真实的订单场景,讲清楚 为什么 Saga 是微服务里更实际的分布式事务方案,以及 如何把它落到代码和工程细节中。
背景与问题
先看一个典型下单链路:
- 用户提交订单
- 订单服务创建订单
- 库存服务冻结库存
- 支付服务扣款
- 订单服务将订单标记为已支付
- 营销服务核销优惠券
这些步骤分散在多个服务中,每个服务都有本地事务,但整个业务过程需要“最终一致”。
为什么不直接上 2PC?
理论上,两阶段提交(2PC)能保证强一致,但在微服务场景里常常不划算:
- 参与方必须支持 XA,改造成本高
- 协调器成为关键点,复杂度高
- 长事务会锁资源,吞吐量差
- 跨网络、跨异构存储时可用性下降
对于订单系统这类高并发业务,很多团队最后会选择:
- 本地事务 + 可靠消息
- TCC
- Saga
而 Saga 的优势在于:
- 对已有服务侵入相对可控
- 不要求底层数据库支持分布式事务协议
- 适合长流程业务
- 通过补偿机制实现最终一致
当然,它不是银弹。Saga 牺牲的是强一致的即时性,换来更高的可用性和更现实的工程落地性。
方案对比与取舍分析
先把几个常见方案摆在一起看。
| 方案 | 一致性 | 实现难度 | 业务侵入 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| 2PC/XA | 强一致 | 高 | 高 | 较差 | 少量核心强一致场景 |
| TCC | 强一致倾向 | 很高 | 很高 | 中 | 资金、额度等强控制业务 |
| Saga | 最终一致 | 中 | 中 | 较好 | 订单、履约、营销链路 |
| 本地消息表 + 异步重试 | 最终一致 | 中 | 中 | 好 | 事件驱动型流程 |
为什么订单系统更适合 Saga?
因为订单流程天然具备“阶段性”和“可补偿性”:
- 创建订单失败:直接终止
- 冻结库存失败:取消订单
- 支付失败:解冻库存、关闭订单
- 优惠券核销失败:返还优惠券、必要时回滚订单状态
换句话说,订单不是“非黑即白”的一次性提交,而是一个可以拆成多个局部动作的业务编排过程。
核心原理
Saga 的核心思想很简单:
把一个长事务拆成多个本地事务,每一步成功后继续下一步;如果中途失败,就按相反顺序执行补偿操作。
两种实现思路
1. Choreography(事件编排)
各个服务通过事件自行驱动:
- 订单创建后发事件
- 库存服务订阅并冻结库存,再发事件
- 支付服务订阅并扣款,再发事件
优点是解耦,缺点是链路复杂后很难排查,容易形成“事件迷宫”。
2. Orchestration(集中式编排)
由一个 Saga 协调器统一控制流程:
- 先调订单服务
- 再调库存服务
- 再调支付服务
- 某步失败时触发补偿
对于中级工程师落地订单场景,我更建议先从 Orchestration 开始,因为:
- 状态流转清楚
- 便于审计与排查
- 补偿路径统一
- 更适合做超时控制和重试管理
订单 Saga 流程设计
下面以“下单 + 库存冻结 + 支付扣款”为例。
正向流程
- 创建订单,状态为
PENDING - 冻结库存
- 发起支付
- 成功后订单改为
CONFIRMED
失败补偿流程
- 如果库存冻结失败:取消订单
- 如果支付失败:解冻库存 + 取消订单
- 如果订单确认失败:需要重试确认,不能轻易回滚支付
这里有个非常重要的工程判断:
并不是每一步失败都应该立刻全量回滚。
例如支付成功但订单状态更新失败,这种情况更合理的处理是:
- 把事件持久化
- 重试订单确认
- 人工兜底补偿
因为“支付退款”通常比“重试订单状态更新”成本更高、风险更大。
Mermaid:Saga 订单编排流程图
flowchart TD
A[用户提交订单] --> B[订单服务: 创建订单 PENDING]
B --> C[库存服务: 冻结库存]
C --> D[支付服务: 扣款]
D --> E[订单服务: 更新为 CONFIRMED]
C -- 失败 --> C1[补偿: 取消订单]
D -- 失败 --> D1[补偿: 解冻库存]
D1 --> D2[补偿: 取消订单]
E -- 失败 --> E1[记录异常事件并重试确认]
Mermaid:时序图
sequenceDiagram
participant U as 用户
participant O as Saga协调器
participant OS as 订单服务
participant IS as 库存服务
participant PS as 支付服务
U->>O: 提交下单请求
O->>OS: createOrder()
OS-->>O: orderId
O->>IS: reserveStock(orderId)
IS-->>O: success/fail
alt 库存成功
O->>PS: pay(orderId)
PS-->>O: success/fail
alt 支付成功
O->>OS: confirmOrder(orderId)
OS-->>O: success
else 支付失败
O->>IS: releaseStock(orderId)
O->>OS: cancelOrder(orderId)
end
else 库存失败
O->>OS: cancelOrder(orderId)
end
数据模型设计
Saga 要落地,不能只停留在“画流程图”。你至少需要一张 Saga 状态表,来支持:
- 当前执行到了哪一步
- 每一步的执行结果
- 是否需要重试
- 是否已经补偿
- 是否出现人工介入异常
Saga 状态表
CREATE TABLE saga_instance (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
saga_id VARCHAR(64) NOT NULL UNIQUE,
order_id VARCHAR(64) NOT NULL,
state VARCHAR(32) NOT NULL,
current_step VARCHAR(32) NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
last_error TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
订单表
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(64) NOT NULL UNIQUE,
user_id VARCHAR(64) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
库存冻结记录表
这个表非常关键,用于保证冻结/解冻幂等。
CREATE TABLE stock_reservation (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(64) NOT NULL UNIQUE,
product_id VARCHAR(64) NOT NULL,
quantity INT NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
实战代码(可运行)
下面我用 Python 做一个简化版可运行示例,模拟 Saga 协调器如何管理订单、库存、支付和补偿。
它不是生产级框架,但足够帮你把核心机制跑通。
运行环境:Python 3.10+
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict
class OrderStatus(str, Enum):
PENDING = "PENDING"
CONFIRMED = "CONFIRMED"
CANCELLED = "CANCELLED"
class ReservationStatus(str, Enum):
RESERVED = "RESERVED"
RELEASED = "RELEASED"
@dataclass
class Order:
order_id: str
user_id: str
amount: float
status: OrderStatus = OrderStatus.PENDING
@dataclass
class StockReservation:
order_id: str
product_id: str
quantity: int
status: ReservationStatus = ReservationStatus.RESERVED
class OrderService:
def __init__(self):
self.orders: Dict[str, Order] = {}
def create_order(self, user_id: str, amount: float) -> str:
order_id = str(uuid.uuid4())
self.orders[order_id] = Order(order_id=order_id, user_id=user_id, amount=amount)
print(f"[OrderService] 创建订单成功: {order_id}")
return order_id
def confirm_order(self, order_id: str):
order = self.orders[order_id]
if order.status == OrderStatus.CONFIRMED:
print(f"[OrderService] 订单已确认,幂等返回: {order_id}")
return
if order.status == OrderStatus.CANCELLED:
raise Exception("订单已取消,不能确认")
order.status = OrderStatus.CONFIRMED
print(f"[OrderService] 订单确认成功: {order_id}")
def cancel_order(self, order_id: str):
order = self.orders[order_id]
if order.status == OrderStatus.CANCELLED:
print(f"[OrderService] 订单已取消,幂等返回: {order_id}")
return
if order.status == OrderStatus.CONFIRMED:
raise Exception("订单已确认,不能取消")
order.status = OrderStatus.CANCELLED
print(f"[OrderService] 订单取消成功: {order_id}")
class InventoryService:
def __init__(self, initial_stock: int):
self.available_stock = initial_stock
self.reservations: Dict[str, StockReservation] = {}
def reserve_stock(self, order_id: str, product_id: str, quantity: int):
if order_id in self.reservations:
reservation = self.reservations[order_id]
if reservation.status == ReservationStatus.RESERVED:
print(f"[InventoryService] 库存已冻结,幂等返回: {order_id}")
return
if self.available_stock < quantity:
raise Exception("库存不足")
self.available_stock -= quantity
self.reservations[order_id] = StockReservation(
order_id=order_id,
product_id=product_id,
quantity=quantity,
status=ReservationStatus.RESERVED
)
print(f"[InventoryService] 冻结库存成功: {order_id}, 剩余库存={self.available_stock}")
def release_stock(self, order_id: str):
reservation = self.reservations.get(order_id)
if not reservation:
print(f"[InventoryService] 无冻结记录,幂等返回: {order_id}")
return
if reservation.status == ReservationStatus.RELEASED:
print(f"[InventoryService] 库存已解冻,幂等返回: {order_id}")
return
self.available_stock += reservation.quantity
reservation.status = ReservationStatus.RELEASED
print(f"[InventoryService] 解冻库存成功: {order_id}, 剩余库存={self.available_stock}")
class PaymentService:
def __init__(self, should_fail: bool = False):
self.should_fail = should_fail
self.paid_orders = set()
def pay(self, order_id: str, amount: float):
if order_id in self.paid_orders:
print(f"[PaymentService] 支付已处理,幂等返回: {order_id}")
return
if self.should_fail:
raise Exception("支付失败:模拟通道超时")
self.paid_orders.add(order_id)
print(f"[PaymentService] 支付成功: {order_id}, amount={amount}")
@dataclass
class SagaState:
saga_id: str
order_id: str = ""
current_step: str = "INIT"
status: str = "RUNNING"
error: str = ""
class OrderSagaOrchestrator:
def __init__(self, order_service: OrderService, inventory_service: InventoryService, payment_service: PaymentService):
self.order_service = order_service
self.inventory_service = inventory_service
self.payment_service = payment_service
self.sagas: Dict[str, SagaState] = {}
def create_order(self, user_id: str, product_id: str, quantity: int, amount: float) -> str:
saga_id = str(uuid.uuid4())
saga = SagaState(saga_id=saga_id)
self.sagas[saga_id] = saga
try:
saga.current_step = "CREATE_ORDER"
order_id = self.order_service.create_order(user_id, amount)
saga.order_id = order_id
saga.current_step = "RESERVE_STOCK"
self.inventory_service.reserve_stock(order_id, product_id, quantity)
saga.current_step = "PAY"
self.payment_service.pay(order_id, amount)
saga.current_step = "CONFIRM_ORDER"
self.order_service.confirm_order(order_id)
saga.status = "SUCCESS"
print(f"[Saga] 事务完成: saga_id={saga_id}")
return order_id
except Exception as e:
saga.status = "FAILED"
saga.error = str(e)
print(f"[Saga] 事务失败: saga_id={saga_id}, step={saga.current_step}, error={e}")
self.compensate(saga)
raise
def compensate(self, saga: SagaState):
print(f"[Saga] 开始补偿: saga_id={saga.saga_id}, failed_step={saga.current_step}")
order_id = saga.order_id
if not order_id:
return
if saga.current_step == "PAY":
self.inventory_service.release_stock(order_id)
self.order_service.cancel_order(order_id)
elif saga.current_step == "RESERVE_STOCK":
self.order_service.cancel_order(order_id)
elif saga.current_step == "CONFIRM_ORDER":
print("[Saga] 订单确认失败,不立即退款,等待重试或人工处理")
print(f"[Saga] 补偿结束: saga_id={saga.saga_id}")
if __name__ == "__main__":
print("=== 场景1:正常下单 ===")
order_service = OrderService()
inventory_service = InventoryService(initial_stock=10)
payment_service = PaymentService(should_fail=False)
orchestrator = OrderSagaOrchestrator(order_service, inventory_service, payment_service)
order_id = orchestrator.create_order(
user_id="u1001",
product_id="p2001",
quantity=2,
amount=99.9
)
print("订单状态:", order_service.orders[order_id].status)
print()
print("=== 场景2:支付失败触发补偿 ===")
order_service2 = OrderService()
inventory_service2 = InventoryService(initial_stock=10)
payment_service2 = PaymentService(should_fail=True)
orchestrator2 = OrderSagaOrchestrator(order_service2, inventory_service2, payment_service2)
try:
orchestrator2.create_order(
user_id="u1002",
product_id="p2002",
quantity=3,
amount=199.9
)
except Exception as e:
print("捕获异常:", e)
print("库存剩余:", inventory_service2.available_stock)
运行后你会看到什么?
- 正常场景下:订单创建、库存冻结、支付成功、订单确认
- 失败场景下:支付失败后自动触发库存解冻和订单取消
这个例子故意做了两件事:
- 每个服务都实现了幂等
- 补偿动作不是简单地“反向调用”,而是有状态判断
这是 Saga 落地时最容易被忽视、但最决定成败的两个点。
状态机设计建议
如果你的订单流程越来越复杂,靠 if/else 会很快失控。
更好的办法是把 Saga 显式建模为状态机。
stateDiagram-v2
[*] --> INIT
INIT --> ORDER_CREATED
ORDER_CREATED --> STOCK_RESERVED
ORDER_CREATED --> ORDER_CANCELLED: 库存失败
STOCK_RESERVED --> PAYMENT_DONE
STOCK_RESERVED --> STOCK_RELEASED: 支付失败
STOCK_RELEASED --> ORDER_CANCELLED
PAYMENT_DONE --> ORDER_CONFIRMED
PAYMENT_DONE --> MANUAL_RETRY: 订单确认失败
ORDER_CONFIRMED --> [*]
ORDER_CANCELLED --> [*]
MANUAL_RETRY --> ORDER_CONFIRMED
为什么状态机重要?
因为在真实系统中,失败不是只有一种:
- 调用超时,但对方其实成功了
- 消息重复投递
- 补偿执行一半失败
- 服务重启导致 Saga 上下文丢失
有了状态机后,你才能明确回答这些问题:
- 当前 Saga 可以重试吗?
- 当前步骤是否允许补偿?
- 补偿是否已经执行过?
- 是否需要人工介入?
实战落地时的关键设计点
1. 幂等是底线,不是优化项
Saga 场景里,重复调用是常态,不是异常。
你至少要对以下动作做幂等:
- 创建订单
- 冻结库存
- 解冻库存
- 发起支付
- 取消订单
- 确认订单
常用做法:
- 业务唯一键,如
order_id - 幂等表 / 去重表
- 状态机判断
- 乐观锁版本号
例如库存冻结时可以直接用 order_id 作为唯一键,确保同一订单不会冻结两次。
2. 补偿不是“撤销”,而是“业务纠正”
这是我很想强调的一点。
很多人刚接触 Saga,会把补偿理解成数据库里的回滚。但现实不是这样。
比如:
- 优惠券核销后失败,补偿是“返还一张券”,不是回滚历史
- 支付成功后取消,补偿是“发起退款”,不是撤销支付记录
- 库存扣减失败,补偿是“释放冻结”,不是穿越回过去
所以补偿动作必须是显式业务操作,且要能独立审计。
3. 本地事务与消息发送要原子化
如果你要从同步 Saga 演进到“本地事务 + 事件驱动”,一定会遇到一个经典问题:
数据库提交成功了,但消息没发出去怎么办?
这时建议用 Outbox Pattern(本地消息表):
- 在本地事务中同时写业务数据和消息表
- 后台任务扫描消息表并投递到 MQ
- 消费成功后更新消息状态
这样能把“业务变更”和“事件待发送”绑定在同一个本地事务里。
常见坑与排查
下面这些坑,几乎每个做 Saga 的团队都会踩。
坑 1:调用超时就当失败,结果补偿把成功结果冲掉了
现象
支付接口超时,协调器认为失败,执行了解冻库存和取消订单;
但几秒后支付通道回调成功,出现“支付成功但订单已取消”。
根因
“超时”不等于“失败”,它只是“结果未知”。
排查方法
- 查请求日志和 traceId
- 查支付侧是否已落账
- 查回调是否晚于 Saga 超时阈值
- 查补偿是否在“未知状态”下过早执行
建议
对外部系统调用引入三态:
SUCCESSFAILEDUNKNOWN
其中 UNKNOWN 进入查询或延迟重试,而不是立刻补偿。
坑 2:补偿动作非幂等,导致库存越补越多
现象
支付失败后,补偿任务重试两次,库存被解冻两次。
根因
解冻接口没有基于冻结记录状态做判断。
建议
- 冻结记录表按
order_id唯一 - 解冻前校验状态是否为
RESERVED - 更新时使用条件更新
UPDATE stock_reservation
SET status = 'RELEASED'
WHERE order_id = 'o1001' AND status = 'RESERVED';
如果受影响行数为 0,说明已经处理过。
坑 3:补偿顺序错了
现象
先取消订单,再解冻库存;
后续运营系统根据已取消订单做清理,库存解冻消息又晚到,产生状态混乱。
建议
补偿顺序通常按正向操作的逆序执行:
- 先释放外部资源
- 再关闭业务单据
- 最后发出终态事件
坑 4:没有持久化 Saga 上下文
现象
服务重启后,不知道哪些单子执行到一半,哪些该补偿,哪些该重试。
建议
Saga 协调器不能只放内存状态,至少要持久化:
- saga_id
- 业务单号
- 当前步骤
- 步骤结果
- 重试次数
- 最后错误信息
- 更新时间
坑 5:重试机制没有退避策略,雪崩更严重
现象
支付服务短时故障,协调器每秒重试上万次,把下游彻底压垮。
建议
使用指数退避 + 抖动:
- 第 1 次:1s
- 第 2 次:2s
- 第 3 次:4s
- 加随机抖动避免集群同时重试
安全/性能最佳实践
Saga 不只是正确性问题,到了线上,安全和性能同样重要。
安全最佳实践
1. 所有跨服务调用都要带业务身份与签名校验
尤其是补偿接口,如:
/release-stock/cancel-order/refund-payment
这些接口一旦被伪造调用,风险很大。建议:
- 内网零信任鉴权
- 服务间 mTLS
- HMAC 签名
- 细粒度 RBAC
2. 补偿接口要限制来源和重放
对关键补偿请求增加:
- 请求唯一 ID
- 时间戳
- 过期时间
- 防重放 nonce
3. 敏感数据不要写入 Saga 日志
例如:
- 完整银行卡号
- 支付 token
- 用户实名信息
日志里保留必要业务键即可,如 order_id、saga_id、trace_id。
性能最佳实践
1. 缩短同步阻塞链路
能异步的步骤尽量异步:
- 营销积分发放
- 通知消息推送
- 发票申请
- 数据埋点
不要把所有步骤都塞进主交易链路。
2. Saga 步骤要有明确超时
没有超时的分布式调用,本质上就是资源泄漏。
建议按业务分类设置:
- 库存冻结:100~300ms
- 支付发起:1~3s
- 订单确认:200~500ms
3. 容量估算不要只看 QPS,要看“在途 Saga 数”
一个很容易漏掉的指标是:
在途 Saga 数 = 每秒新建 Saga × 平均完成时长
例如:
- 下单峰值 2000 TPS
- 平均 Saga 生命周期 8 秒
那么在途 Saga 数大约是:
2000 × 8 = 16000
这会直接影响:
- 协调器状态存储容量
- 定时扫描任务压力
- 重试队列堆积
- 补偿任务并发度
4. 监控要按 Saga 维度建设
至少监控这些指标:
- Saga 成功率
- 各步骤失败率
- 补偿触发率
- 平均完成时长
- 超时分布
- 人工介入数
- 重试次数分位值
一套更实际的落地建议
如果你准备在生产环境上 Saga,我建议按下面节奏推进,而不是一口吃成胖子。
第一阶段:先做同步编排版
目标:
- 流程清晰
- 状态机明确
- 幂等到位
- 补偿可用
适合先把主流程跑通。
第二阶段:引入本地消息表和异步事件
目标:
- 降低同步耦合
- 缩短主链路时延
- 支持最终一致的异步修复
适合把“非关键同步步骤”迁出去。
第三阶段:建设运维与审计体系
目标:
- Saga 可观测
- 可重试
- 可人工介入
- 可审计复盘
很多团队前两步做得还行,真正出问题时却发现没有控制台、没有状态追踪、没有人工补偿入口,最后只能手改数据库。这种方式在测试环境能活,在生产环境迟早出事故。
边界条件:什么场景不适合 Saga?
Saga 很实用,但并不是所有业务都适合。
以下场景要谨慎:
1. 无法定义补偿动作
如果一个动作成功后不可逆,且没有业务纠正手段,那 Saga 很难成立。
2. 强实时强一致要求极高
例如某些核心账务场景,要求任意时刻都不能短暂不一致,这类更适合 TCC 或更强约束的账务设计。
3. 外部系统不支持查询与幂等
如果支付通道既不支持结果查询,也不支持幂等调用,协调器会非常难做,超时后几乎没法判定真实结果。
总结
在微服务架构里,订单一致性最难的地方,不是“让所有服务像单库事务一样同时成功”,而是:
- 接受分布式环境中的失败与延迟
- 用状态机管理流程
- 用补偿机制修正不一致
- 用幂等和可观测性保证系统可恢复
如果你要把这篇文章里的内容落地,我建议优先抓住这 5 个点:
- 先选 Orchestration 方式实现 Saga
- 每个步骤和补偿都做幂等
- 把 Saga 上下文持久化,别只放内存
- 把超时视为未知,不要轻易直接补偿
- 先保证可恢复,再追求优雅
一句话概括:
Saga 不是让分布式事务“完美无缺”,而是让系统在不完美的现实里,依然能稳定收敛到正确状态。
如果你的订单系统正处在“微服务化后事务开始失控”的阶段,Saga 往往是那个既现实、又足够工程化的答案。