引言:微服务拆分的代价
当单体应用被拆分为微服务后,一个业务请求往往需要跨越多个服务边界。传统数据库的ACID事务在分布式环境下鞭长莫及——跨服务、跨数据库、跨网络的两次提交(2PC)在高并发场景下几乎是不可接受的延迟元凶。
2017年,Hector Garcia-Molina和Kenneth Salem在经典论文中首次提出了Saga的概念。经过十多年的演进,Saga已成为微服务架构下管理长生命周期事务的事实标准模式。
本文将从原理到实战,带你彻底掌握Saga模式的两种实现范式,并通过一个完整的电商下单场景,吃透补偿事务、幂等性保障、空补偿处理等核心工程难题。
一、核心原理:Saga的基本模型
Saga由一系列本地事务(T1, T2, ..., Tn)组成,每个本地事务执行后会发布事件或消息触发下一个事务。如果Ti失败,Saga将按逆序执行补偿事务(Ci-1, ..., C1)来回滚已完成的工作。
// Saga 正向执行流程
T1 → T2 → T3 → T4 → T5
成功 | 成功 | 成功 | 失败 | (未执行)
// 逆序补偿执行流程
C3 → C2 → C1
回滚 | 回滚 | 回滚
核心设计原则:
- 补偿先行:每个正向事务必须提前定义对应的补偿操作
- 最终一致性:不追求强一致,但保证在故障恢复后达到一致状态
- 可交换性:补偿操作必须幂等,可重试不产生副作用
二、两种编排范式深度对比
2.1 编排(Orchestration)——中央指挥官
Saga Orchestrator作为中央协调者,按预定义流程依次调用各服务的API。它掌控全局状态,决定下一步执行哪个服务。
// Orchestrator 伪代码示例
public class CreateOrderSaga {
public void execute(OrderRequest request) {
try {
// 步骤1: 创建订单(待支付状态)
Order order = orderService.createPendingOrder(request);
// 步骤2: 扣减库存
inventoryService.deduct(order.getItems());
// 步骤3: 预扣资金(冻结用户余额)
paymentService.freeze(order.getUserId(), order.getTotalAmount());
// 步骤4: 扣减优惠券
couponService.useCoupon(order.getUserId(), request.getCouponId());
// 步骤5: 确认订单
orderService.confirm(order.getId());
} catch (Exception e) {
// 任何步骤失败 → Orchestrator 自动触发补偿流程
compensate(order);
throw new SagaException("订单创建失败,已执行补偿", e);
}
}
private void compensate(Order order) {
// 逆序补偿:先回滚最后成功的步骤
if (order.isCouponUsed()) couponService.refundCoupon(order.getCouponRecordId());
if (order.isAmountFrozen()) paymentService.unfreeze(order.getUserId(), order.getFrozenRecordId());
if (order.isInventoryDeducted()) inventoryService.restore(order.getItems());
orderService.cancel(order.getId());
}
}
优点:流程清晰、易调试、便于添加新步骤;缺点:存在单点风险,协调者本身可能成为瓶颈。
2.2 舞蹈(Choreography)——去中心化协作
没有中央协调者。每个服务监听上游事件,产生下游事件,通过事件驱动自动触发整个流程。
// 服务间通过事件异步协作
┌─────────────┐ OrderCreated ┌─────────────┐
│ Order │─────────────────────→│ Inventory │
│ Service │ │ Service │
└─────────────┘ └──────┬──────┘
│ InventoryReserved
▼
┌──────────────┐
│ Payment │
│ Service │
└──────┬───────┘
│ AmountFrozen
▼
┌──────────────┐
│ Coupon │
│ Service │
└──────────────┘
// 失败时反向传播补偿事件
InventoryService --InsufficientStockEvent→ OrderService(取消订单)
优点:无单点、天然松耦合、弹性好;缺点:流程分散难追踪、循环依赖风险、调试困难。
2.3 选型决策矩阵
| 维度 | Orchestration | Choreography |
|---|---|---|
| 流程复杂度 | 适合多步骤(>5)、条件分支多 | 适合简单线性流程(≤4) |
| 团队规模 | >3个服务团队,需统一管控 | 小团队,各服务自治 |
| 运维要求 | 需监控协调者运行状态 | 依赖事件总线可靠性 |
| 典型框架 | Seata、Axon、Camunda | 基于Kafka/RabbitMQ自实现 |
三、电商下单实战:完整代码实现(Orchestration方案)
3.1 架构全景
以下基于Spring Boot 3.x + Seata Saga的完整电商下单实现:
# application-saga.yml
seata:
enabled: true
application-id: order-saga-service
tx-service-group: my_tx_group
saga:
state-machine:
table-prefix: seata_saga_
async-thread-pool:
core-size: 8
max-size: 32
3.2 状态机定义(JSON)
{
"startState": "CreateOrder",
"version": "0.1",
"states": {
"CreateOrder": {
"type": "serviceTask",
"serviceName": "orderServiceImpl",
"methodName": "createPendingOrder",
"compensateState": "CancelOrder",
"next": "DeductInventory",
"input": ["$.[request]"],
"output": {"orderId": "$.#orderId"}
},
"DeductInventory": {
"type": "serviceTask",
"serviceName": "inventoryServiceImpl",
"methodName": "deductStock",
"compensateState": "RestoreInventory",
"next": "FreezePayment",
"input": ["$.[request]", "[orderId]"]
},
"FreezePayment": {
"type": "serviceTask",
"serviceName": "paymentServiceImpl",
"methodName": "freezeAmount",
"compensateState": "UnfreezePayment",
"next": "UseCoupon"
},
"UseCoupon": {
"type": "serviceTask",
"serviceName": "couponServiceImpl",
"methodName": "consumeCoupon",
"compensateState": "RefundCoupon",
"next": "ConfirmOrder"
},
"ConfirmOrder": {
"type": "serviceTask",
"serviceName": "orderServiceImpl",
"methodName": "confirmOrder",
"end": true
},
"CancelOrder": { "type": "serviceTask", "methodName": "cancelOrder", "end": true },
"RestoreInventory":{ "type": "serviceTask", "methodName": "restoreStock", "end": true },
"UnfreezePayment": { "type": "serviceTask", "methodName": "unfreezeAmount", "end": true },
"RefundCoupon": { "type": "serviceTask", "methodName": "refundCoupon", "end": true }
}
}
3.3 幂等性保障——解决空补偿和悬挂
// 关键:每个补偿操作必须检查前面步骤是否真正执行过
// 否则会引发"空补偿"问题
@Transactional
public void restoreStock(Long orderId) {
// 查询正向扣减记录
StockOpLog opLog = stockOpLogRepo.findByOrderIdAndType(orderId, OperationType.DEDUCT);
if (opLog == null) {
// 空补偿:正向操作从未执行 → 直接返回成功
log.info("空补偿检测:orderId={} 未执行过库存扣减,跳过", orderId);
return;
}
if (opLog.isReversed()) {
// 已补偿 → 幂等返回
log.info("幂等检测:orderId={} 已执行过库存恢复,跳过", orderId);
return;
}
// 真正执行恢复
inventoryRepo.increase(opLog.getSkuId(), opLog.getQuantity());
opLog.markReversed();
stockOpLogRepo.save(opLog);
}
关键陷阱与对策:
| 陷阱 | 场景 | 对策 |
|---|---|---|
| 空补偿 | 先触发补偿再执行正向 → 补偿发现无事可做 | 正向表记录存在性判断 |
| 悬挂 | 补偿完全完成,正向操作才姗姗来迟 | 正向操作前检查补偿状态,已补偿则拒绝执行 |
| 幂等失效 | 网络超时重试导致同一操作执行多次 | 唯一约束 + 版本号乐观锁 |
3.4 Saga事务日志持久化
// Seata Saga 自动持久化状态机执行日志
// 关键表结构(简化)
CREATE TABLE seata_saga_state_machine_inst (
id VARCHAR(100) PRIMARY KEY,
business_key VARCHAR(100) UNIQUE, -- 幂等键(订单号)
machine_id VARCHAR(100),
start_time DATETIME,
end_time DATETIME,
status VARCHAR(10), -- RUNNING/FAILED/SUCCESSED/COMPENSATED
compensation_status VARCHAR(10),
running_result JSON -- 失败时的上下文快照
);
// 通过business_key实现天然幂等:同一订单号的重复请求直接返回历史结果
四、高级模式:Saga与其他分布式事务方案的对比
| 方案 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|
| 2PC/XA | 强一致 | 低(阻塞等待) | 跨库且要求绝对一致(银行核心) |
| TCC | 最终一致 | 中高 | 需要预留资源的场景(积分+余额) |
| AT模式 | 最终一致 | 高 | 简单CRUD,数据库直连代理 |
| Saga | 最终一致 | 高 | 长流程、跨服务、可补偿性操作 |
| 本地消息表 | 最终一致 | 中 | 异步操作,无需实时协调 |
Saga的独特优势在于:天然适配长事务。如果业务流程需要等待外部事件(如人工审核、第三方回调),Saga可以在中间态安全地"暂停"并稍后恢复,而TCC的Try阶段长时间持有预留资源是不现实的。
五、生产级最佳实践
- 事务粒度最小化:每个正向事务尽可能短小,降低补偿概率和成本
- 补偿操作前置设计:在设计确定阶段就定义好每步的补偿逻辑,而非事后补救
- 幂等贯穿始终:所有参与Saga的服务接口必须保证幂等性
- 超时与死锁检测:为Saga实例设置最大执行时间,防止"僵尸事务"
- 可观测性三板斧:日志聚合 + 状态机状态大盘 + 补偿告警(空补偿率异常升高往往是上游bug的信号)
- 对账兜底:定时任务扫描不一致状态(如订单已创建但库存未扣),自动修复或告警人工介入
六、总结
Saga模式的本质是用"最终一致性+补偿"替代"即时一致性+锁定"。它不追求ACID的强一致承诺,但让长生命周期事务在分布式环境下变得可控。
选择Orchestration还是Choreography,取决于你的团队规模和流程复杂度。如果你的订单流程涉及超过5个服务且需要频繁变更,Orchestrator + Seata Saga的组合是一个稳妥的工程选择。
记住,没有银弹——最适合你业务场景的方案才是好方案。先从补偿操作的设计开始,把每一步的可逆性想透,Saga就成功了一大半。

发表评论 取消回复