
如果说TCC模式适合"短平快"的分布式事务(如电商下单的"创建订单+扣库存+支付"三步流程),那么SAGA模式就是为"长事务"而生的------当一个业务流程需要跨多个服务、经历多个步骤(甚至耗时几小时、几天),比如"物流订单从创建到签收""供应链从采购到入库",SAGA能通过"分步执行+反向补偿"保证最终一致性,且全程无锁阻塞。
今天我们就从"长事务的痛点"出发,拆解SAGA的两种实现方式(编排式vs协同式),用物流订单案例讲透"补偿事务设计"和"中间状态处理",并附上Seata框架的实战代码。
一、为什么需要SAGA?长事务的"TCC困境"
先看一个典型的长事务场景:物流订单流程。一个完整的物流订单需要经过5个步骤,跨4个服务:
- 订单服务:创建物流订单(状态:待确认);
- 库存服务:扣减商品库存(从仓库A扣10件);
- 支付服务:扣减用户运费(从账户扣50元);
- 仓库服务:安排发货(生成物流单,状态:已发货);
- 物流服务:用户签收(更新订单状态:已完成)。
这个流程的特点是步骤多、周期长(从创建到签收可能需要1-3天),如果用TCC模式会遇到两个致命问题:
- 资源预留成本过高:TCC的Try阶段需要冻结库存、冻结运费,而长事务中资源可能被冻结几天,导致资源利用率极低(比如库存被冻结后,其他订单无法使用);
- 补偿逻辑复杂:长流程中中间状态多(如"已发货但未签收"),TCC的Cancel接口难以覆盖所有异常场景(比如发货后取消,需要召回货物,这在TCC中很难用简单的"释放资源"处理)。
SAGA模式的出现就是为了解决长事务的痛点:它将分布式事务拆分为"一系列本地事务的有序执行",每个本地事务对应一个"补偿事务",若某步失败,按相反顺序执行补偿事务,最终达成一致。
二、SAGA的两种实现方式:编排式vs协同式
SAGA的核心是"定义正向事务序列+反向补偿序列",根据"谁来决定下一步执行什么",分为两种实现方式:
1. 编排式SAGA:中央协调器主导流程
核心思想:由一个"编排器(Orchestrator)"统一管理整个事务流程,编排器知道所有步骤的执行顺序,直接调用各服务的正向/补偿接口。
物流订单的编排式流程示例:
- 正向事务序列 :
编排器→订单服务(创建订单)→库存服务(扣减库存)→支付服务(扣减运费)→仓库服务(安排发货)→物流服务(签收)。 - 若支付服务失败 (比如用户余额不足),补偿序列 :
编排器→支付服务(补偿:退款,虽然没扣成功,但幂等处理)→库存服务(补偿:恢复库存)→订单服务(补偿:取消订单)。
优点:
- 流程清晰:所有步骤由编排器定义,可视化强(比如用流程图能直接画出执行顺序);
- 易于调试:出问题时,直接查编排器的执行日志即可定位哪步失败;
- 灵活调整:修改流程只需改编排器,无需改动各服务。
缺点:
- 编排器成"单点":所有流程依赖编排器,若编排器宕机,整个事务中断;
- 编排器复杂度高:长流程中,编排器需要处理所有分支逻辑(如"发货失败怎么办""签收超时怎么办"),代码可能臃肿。
2. 协同式SAGA:服务间自主推进流程
核心思想:没有中央编排器,每个服务只知道"自己的下一个步骤"和"自己的补偿步骤",通过消息(或事件)通知下一个服务执行,失败时通过消息触发上一个服务的补偿。
物流订单的协同式流程示例:
- 正向流程 :
订单服务创建订单后,发送"订单创建成功"消息→库存服务接收消息,扣减库存后发送"库存扣减成功"消息→支付服务接收消息,扣减运费后发送"支付成功"消息→......以此类推。 - 若支付服务失败 :
支付服务发送"支付失败"消息→库存服务接收消息,执行补偿(恢复库存)后发送"库存补偿成功"消息→订单服务接收消息,执行补偿(取消订单)。
优点:
- 去中心化:无单点风险,服务独立运行;
- 可扩展性好:新增步骤只需修改相邻服务的消息交互,无需改动全局;
- 适合微服务架构:符合"服务自治"的设计原则。
缺点:
- 流程不直观:事务流程分散在各服务的消息处理中,难以全局把控;
- 调试困难:出问题时需要追踪多个服务的消息日志,定位问题耗时;
- 消息一致性依赖高:若消息丢失或重复,可能导致流程中断或错乱。
两种方式怎么选?
- 中小规模、流程固定的场景(如标准化物流订单):选编排式(开发简单,易维护);
- 大规模、流程灵活的场景(如定制化供应链):选协同式(去中心化,可扩展)。
三、SAGA的核心设计:补偿事务与中间状态
无论是编排式还是协同式,SAGA的落地都离不开两个核心设计:补偿事务 (保证回滚能力)和中间状态(处理长流程中的临时状态)。
1. 补偿事务设计:"正向做什么,反向就undo什么"
补偿事务是SAGA的"回滚机制",每个正向事务必须有对应的补偿事务,设计需遵循三个原则:
原则1:补偿事务必须"可执行"(即使正向事务部分成功)
例如:库存服务的正向事务是"扣减10件库存",补偿事务应为"增加10件库存"------即使正向事务只扣减了5件(因异常中断),补偿事务增加10件会导致库存多5件?这时候需要在补偿事务中"先查实际扣减量,再补偿对应量"。
示例代码(库存服务补偿事务):
java
// 正向事务:扣减库存,记录扣减日志
@Transactional
public void deductStock(Long orderId, Long goodsId, int num) {
// 扣减库存
Stock stock = stockRepo.findByGoodsId(goodsId);
stock.setNum(stock.getNum() - num);
stockRepo.save(stock);
// 记录扣减日志(用于补偿时确认实际扣减量)
stockLogRepo.save(new StockLog(orderId, goodsId, num, "DEDUCT"));
}
// 补偿事务:根据扣减日志恢复库存
@Transactional
public void compensateDeductStock(Long orderId) {
// 查实际扣减量
StockLog log = stockLogRepo.findByOrderIdAndType(orderId, "DEDUCT");
if (log == null) {
return; // 正向事务未执行,无需补偿
}
// 恢复库存
Stock stock = stockRepo.findByGoodsId(log.getGoodsId());
stock.setNum(stock.getNum() + log.getNum());
stockRepo.save(stock);
// 记录补偿日志(幂等用)
stockLogRepo.save(new StockLog(orderId, log.getGoodsId(), log.getNum(), "COMPENSATE"));
}
原则2:补偿事务必须"幂等"(避免重复执行导致错误)
分布式系统中,补偿事务可能被重复调用(如消息重试),需保证"重复执行结果不变"。通常通过"事务日志+唯一标识"实现,如上面代码中记录"COMPENSATE"类型的日志,再次调用时先检查日志,若已补偿则直接返回。
原则3:补偿事务最好"无副作用"(不引入新的不一致)
例如:支付服务的正向事务是"扣减50元运费",补偿事务应为"退还50元",而不是"删除支付记录"(删除可能导致与其他系统的对账不一致)。
2. 中间状态处理:让用户和系统"理解"临时状态
长事务中,流程可能在某一步停留很久(比如"已发货但未签收"可能持续1天),需要设计清晰的中间状态,避免:
- 用户困惑(如用户看到"订单处理中"却不知道具体进度);
- 系统误判(如超时未签收时,系统需要知道当前处于"发货中"状态,才能触发"召回"补偿)。
物流订单的状态机设计示例:
| 状态 | 含义 | 触发条件 | 下一步状态 |
|---|---|---|---|
| PENDING | 待确认 | 订单创建成功 | DEDUCTING_STOCK |
| DEDUCTING_STOCK | 库存扣减中 | 开始调用库存服务 | PAYING |
| PAYING | 支付中 | 库存扣减成功 | DELIVERING |
| DELIVERING | 发货中 | 支付成功 | SIGNING |
| SIGNING | 签收中 | 仓库发货成功 | COMPLETED |
| COMPLETED | 已完成 | 用户签收成功 | 无 |
| CANCELED | 已取消 | 任一环节失败并完成补偿 | 无 |
状态设计原则:
- 细粒度:状态足够细化,能反映流程进度(如"支付中"比"处理中"更清晰);
- 可追溯:每个状态转换都记录日志(谁、何时、从什么状态到什么状态),方便问题排查;
- 易理解:用户能通过状态明确知道当前进度(如"发货中"→用户知道商品在运输)。
四、实战:用Seata实现编排式SAGA
Seata是阿里开源的分布式事务框架,原生支持SAGA模式,通过"JSON定义流程+注解标记补偿方法"实现编排式SAGA,下面以物流订单为例实战。
1. 环境准备
- 引入Seata依赖(Spring Boot项目):
xml
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-saga-starter</artifactId>
<version>1.6.1</version>
</dependency>
2. 定义服务接口(正向+补偿)
以订单服务和库存服务为例,用@SagaTcc标记补偿方法:
java
// 订单服务
@Service
public class OrderService {
// 正向事务:创建订单
public OrderVO createOrder(OrderDTO orderDTO) {
Order order = new Order();
order.setId(orderDTO.getOrderId());
order.setUserId(orderDTO.getUserId());
order.setStatus("PENDING"); // 初始状态:待确认
orderRepo.save(order);
return new OrderVO(order.getId(), order.getStatus());
}
// 补偿事务:取消订单(用@SagaTcc标记,与正向方法配对)
@SagaTcc(compensateMethod = "cancelOrder")
public OrderVO createOrderTcc(OrderDTO orderDTO) {
return createOrder(orderDTO); // 实际调用正向方法
}
// 补偿实现:取消订单
public void cancelOrder(OrderDTO orderDTO, OrderVO result) {
Order order = orderRepo.findById(orderDTO.getOrderId()).orElse(null);
if (order != null) {
order.setStatus("CANCELED");
orderRepo.save(order);
}
}
}
// 库存服务(类似,定义deductStockTcc正向方法和compensateDeduct补偿方法)
@Service
public class StockService {
@SagaTcc(compensateMethod = "compensateDeduct")
public StockVO deductStock(StockDTO stockDTO) {
// 扣减库存逻辑(同前面示例)
return new StockVO(stockDTO.getGoodsId(), "DEDUCTED");
}
public void compensateDeduct(StockDTO stockDTO, StockVO result) {
// 补偿逻辑(恢复库存,同前面示例)
}
}
3. 用JSON定义SAGA流程(编排器配置)
在resources/saga目录下创建logistics_order_saga.json,定义正向步骤和补偿步骤:
json
{
"name": "logisticsOrderSaga",
"comment": "物流订单SAGA流程",
"startState": "createOrder",
"states": {
// 步骤1:创建订单
"createOrder": {
"type": "ServiceTask",
"serviceName": "orderService",
"serviceMethod": "createOrderTcc",
"parameter": "${orderDTO}", // 入参:订单DTO
"nextState": "deductStock", // 下一步:扣减库存
" compensationState": "cancelOrder" // 补偿步骤:取消订单
},
// 步骤2:扣减库存
"deductStock": {
"type": "ServiceTask",
"serviceName": "stockService",
"serviceMethod": "deductStockTcc",
"parameter": {
"orderId": "${orderDTO.orderId}",
"goodsId": "${orderDTO.goodsId}",
"num": "${orderDTO.num}"
},
"nextState": "payFreight", // 下一步:支付运费
"compensationState": "compensateDeduct" // 补偿步骤:恢复库存
},
// 步骤3:支付运费(类似定义,略)
"payFreight": { ... },
// 步骤4:安排发货(类似定义,略)
"deliver": { ... },
// 步骤5:签收(类似定义,略)
"sign": { ... },
// 补偿步骤:取消订单
"cancelOrder": {
"type": "ServiceTask",
"serviceName": "orderService",
"serviceMethod": "cancelOrder",
"parameter": "${orderDTO}"
},
// 补偿步骤:恢复库存
"compensateDeduct": {
"type": "ServiceTask",
"serviceName": "stockService",
"serviceMethod": "compensateDeduct",
"parameter": {
"orderId": "${orderDTO.orderId}",
"goodsId": "${orderDTO.goodsId}",
"num": "${orderDTO.num}"
}
}
}
}
4. 启动SAGA流程
在业务代码中调用Seata的SagaEngine执行流程:
java
@Service
public class LogisticsOrderService {
@Autowired
private SagaEngine sagaEngine;
@Autowired
private StateMachineEngine stateMachineEngine;
public String createLogisticsOrder(OrderDTO orderDTO) {
// 初始化参数
Map<String, Object> context = new HashMap<>();
context.put("orderDTO", orderDTO);
// 执行SAGA流程(流程名对应JSON中的name)
StateMachineInstance instance = stateMachineEngine.start("logisticsOrderSaga", null, context);
if (instance.getStatus() == StateMachineInstanceStatus.SUCCESS) {
return "物流订单创建成功,ID:" + orderDTO.getOrderId();
} else {
return "创建失败,原因:" + instance.getErrorMessage();
}
}
}
5. 关键配置(application.yml)
yaml
seata:
enabled: true
application-id: logistics-order-service
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default
saga:
state-machine-mode: json # 用JSON定义流程
json:
resources: classpath:saga/logistics_order_saga.json # 流程文件路径
五、SAGA的优缺点与适用场景
优点:
- 适合长事务:支持跨多个服务、多步骤的长流程(如物流、供应链),无资源长期锁定问题;
- 性能优异:每个步骤是独立本地事务,执行完即释放资源,无同步阻塞;
- 灵活性高:可通过状态机设计适配复杂分支流程(如"发货失败则换仓库重发")。
缺点:
- 一致性弱:存在明显的中间状态(如"已支付但未发货"),可能持续数小时,需业务容忍;
- 补偿复杂:长流程中补偿事务可能嵌套(如"发货后取消"需要先召回货物再退款),逻辑复杂;
- 状态管理难:需设计大量中间状态,且要保证状态转换的正确性(避免状态错乱)。
适用场景:
- 长流程业务(如物流订单、供应链采购、审批流程);
- 步骤多且各步骤独立的场景(如"下单→生产→物流→安装→验收");
- 可容忍短暂不一致,更关注流程最终完成的场景。
总结:SAGA是长事务的"务实选择"
SAGA模式不追求"瞬间一致",而是通过"分步执行+反向补偿"接受"中间不一致",最终达成一致。它的设计贴合长事务的特性------步骤多、周期长、资源不能长期锁定,因此成为物流、供应链等领域的首选方案。
但SAGA的落地门槛不低:补偿事务的幂等性、中间状态的精细设计、流程的可视化管理,都需要开发者对业务有深刻理解。
下一篇,我们将讲解"本地消息表"和"事务消息",看看这两种基于消息的方案如何用"异步通信"解决分布式事务问题。
(觉得有用的话,欢迎点赞收藏,关注后续系列文章~)