一、一个跨5个服务的订单流程跑了40分钟
2020年,我们的电商系统订单流程涉及5个服务:订单、库存、优惠券、积分、物流。
最初用的是TCC模式,每个服务都要实现try/confirm/cancel三个接口。开发量巨大,而且每次新增一个步骤,所有相关的补偿逻辑都要改。
有一次订单流程执行到一半,积分服务超时了,补偿逻辑又触发了库存服务的bug,导致库存数据错乱。排查了3天。
后来我们改用Saga模式,事务编排变得清晰很多,补偿逻辑也更容易维护。
二、Saga模式概述
2.1 什么是Saga
Saga模式:
将一个长事务拆分成多个本地事务,每个本地事务完成后,
通过消息或事件触发下一个本地事务。
如果某个步骤失败,则反向执行之前所有步骤的补偿操作。
正向执行:T1 → T2 → T3 → T4 → 完成
补偿回滚:T1 → T2 → T3(失败) → C2 → C1
vs TCC:
- Saga:只有正向操作和补偿操作,没有try阶段
- TCC:有try/confirm/cancel三个阶段
- Saga更适合长事务,TCC更适合短事务
2.2 两种编排方式
┌─────────────────────────────────────────────────────────────────┐
│ Saga编排方式 │
│ │
│ 1. 编排式(Choreography) │
│ - 每个服务监听事件,自己决定下一步 │
│ - 去中心化,没有协调者 │
│ - 适合简单的Saga │
│ │
│ 2. 协调式(Orchestration) │
│ - 有一个协调者(Saga Manager) │
│ - 协调者决定下一步执行什么 │
│ - 中心化,逻辑清晰 │
│ - 适合复杂的Saga │
│ │
└──────────────────────────────────────────────────────────────────┘
三、编排式Saga实现
3.1 事件驱动
java
/**
* 订单创建Saga(编排式)
*/
@Service
@Slf4j
public class OrderCreateSagaChoreography {
@Autowired
private ApplicationEventPublisher eventPublisher;
/**
* 启动Saga
*/
public void startSaga(CreateOrderRequest request) {
// Step 1: 创建订单
Order order = createOrder(request);
// 发布事件:订单已创建
eventPublisher.publishEvent(new OrderCreatedEvent(order));
}
// ========= 库存服务 =========
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
try {
// Step 2: 扣减库存
inventoryService.deduct(event.getOrder());
// 发布事件:库存已扣减
eventPublisher.publishEvent(new InventoryDeductedEvent(event.getOrder()));
} catch (Exception e) {
// 补偿:取消订单
orderService.cancelOrder(event.getOrder().getId());
log.error("库存扣减失败,补偿取消订单", e);
}
}
// ========= 优惠券服务 =========
@EventListener
public void onInventoryDeducted(InventoryDeductedEvent event) {
try {
// Step 3: 使用优惠券
if (event.getOrder().getCouponId() != null) {
couponService.useCoupon(event.getOrder().getUserId(),
event.getOrder().getCouponId());
}
// 发布事件:优惠券已使用
eventPublisher.publishEvent(new CouponUsedEvent(event.getOrder()));
} catch (Exception e) {
// 补偿:恢复库存
inventoryService.restore(event.getOrder());
// 补偿:取消订单
orderService.cancelOrder(event.getOrder().getId());
log.error("优惠券使用失败,补偿回滚", e);
}
}
// ========= 积分服务 =========
@EventListener
public void onCouponUsed(CouponUsedEvent event) {
try {
// Step 4: 扣减积分
if (event.getOrder().getPoints() > 0) {
pointsService.deduct(event.getOrder().getUserId(),
event.getOrder().getPoints());
}
// 发布事件:积分已扣减
eventPublisher.publishEvent(new PointsDeductedEvent(event.getOrder()));
} catch (Exception e) {
// 补偿:恢复优惠券
if (event.getOrder().getCouponId() != null) {
couponService.restoreCoupon(event.getOrder().getUserId(),
event.getOrder().getCouponId());
}
// 补偿:恢复库存
inventoryService.restore(event.getOrder());
// 补偿:取消订单
orderService.cancelOrder(event.getOrder().getId());
log.error("积分扣减失败,补偿回滚", e);
}
}
}
四、协调式Saga实现
4.1 Saga定义
java
/**
* Saga定义
*/
@Data
@Builder
public class SagaDefinition {
private String sagaName;
private List<SagaStep> steps;
@Data
@Builder
public static class SagaStep {
private String name;
private String service;
private String action;
private String compensateAction;
private Map<String, Object> params;
}
}
/**
* 订单创建Saga定义
*/
@Component
public class OrderCreateSagaDefinition {
public SagaDefinition getSagaDefinition() {
return SagaDefinition.builder()
.sagaName("order-create")
.steps(Arrays.asList(
SagaStep.builder()
.name("create-order")
.service("order-service")
.action("create")
.compensateAction("cancel")
.build(),
SagaStep.builder()
.name("deduct-inventory")
.service("inventory-service")
.action("deduct")
.compensateAction("restore")
.build(),
SagaStep.builder()
.name("use-coupon")
.service("coupon-service")
.action("use")
.compensateAction("restore")
.build(),
SagaStep.builder()
.name("deduct-points")
.service("points-service")
.action("deduct")
.compensateAction("restore")
.build(),
SagaStep.builder()
.name("create-shipment")
.service("logistics-service")
.action("create")
.compensateAction("cancel")
.build()
))
.build();
}
}
4.2 Saga引擎
java
/**
* Saga引擎
*/
@Service
@Slf4j
public class SagaEngine {
@Autowired
private SagaInstanceRepository sagaRepository;
@Autowired
private ServiceInvoker serviceInvoker;
/**
* 执行Saga
*/
public SagaResult execute(SagaDefinition definition, Map<String, Object> context) {
String sagaId = UUID.randomUUID().toString();
// 创建Saga实例
SagaInstance instance = SagaInstance.builder()
.sagaId(sagaId)
.sagaName(definition.getSagaName())
.status(SagaStatus.RUNNING)
.currentStep(0)
.context(context)
.startTime(LocalDateTime.now())
.build();
sagaRepository.save(instance);
// 逐步执行
try {
for (int i = 0; i < definition.getSteps().size(); i++) {
SagaStep step = definition.getSteps().get(i);
log.info("Saga执行步骤: sagaId={}, step={}/{}",
sagaId, i + 1, definition.getSteps().size());
// 执行步骤
Object result = serviceInvoker.invoke(
step.getService(),
step.getAction(),
context
);
// 更新上下文
context.put(step.getName() + "Result", result);
// 更新实例状态
instance.setCurrentStep(i + 1);
instance.setContext(context);
sagaRepository.update(instance);
}
// Saga完成
instance.setStatus(SagaStatus.COMPLETED);
instance.setEndTime(LocalDateTime.now());
sagaRepository.update(instance);
log.info("Saga执行完成: sagaId={}", sagaId);
return SagaResult.success(sagaId);
} catch (Exception e) {
log.error("Saga执行失败: sagaId={}, step={}",
sagaId, instance.getCurrentStep(), e);
// 补偿回滚
compensate(definition, instance);
return SagaResult.failure(sagaId, e.getMessage());
}
}
/**
* 补偿回滚
*/
private void compensate(SagaDefinition definition, SagaInstance instance) {
instance.setStatus(SagaStatus.COMPENSATING);
sagaRepository.update(instance);
int currentStep = instance.getCurrentStep();
// 从当前步骤开始,反向执行补偿
for (int i = currentStep - 1; i >= 0; i--) {
SagaStep step = definition.getSteps().get(i);
try {
log.info("Saga补偿步骤: sagaId={}, step={}", instance.getSagaId(), step.getName());
serviceInvoker.invoke(
step.getService(),
step.getCompensateAction(),
instance.getContext()
);
} catch (Exception e) {
log.error("Saga补偿失败: sagaId={}, step={}",
instance.getSagaId(), step.getName(), e);
// 记录补偿失败,需要人工介入
}
}
instance.setStatus(SagaStatus.COMPENSATED);
instance.setEndTime(LocalDateTime.now());
sagaRepository.update(instance);
}
}
五、Seata Saga模式
5.1 Seata Saga状态机
json
{
"Name": "order-create-saga",
"Comment": "订单创建Saga",
"StartState": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "create",
"CompensateState": "CancelOrder",
"Next": "DeductInventory",
"Input": ["$.orderId", "$.userId", "$.items"],
"Output": {"orderId": "$.orderId"},
"Status": {"#root.success": "SU", "#root.fail": "FA"}
},
"DeductInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "deduct",
"CompensateState": "RestoreInventory",
"Next": "UseCoupon",
"Input": ["$.orderId", "$.items"],
"Catch": [{"Exceptions": ["java.lang.Exception"], "Next": "CompensationTrigger"}]
},
"UseCoupon": {
"Type": "ServiceTask",
"ServiceName": "couponService",
"ServiceMethod": "use",
"CompensateState": "RestoreCoupon",
"Next": "Succeed",
"Input": ["$.userId", "$.couponId"]
},
"CancelOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "cancel",
"Input": ["$.orderId"]
},
"RestoreInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "restore",
"Input": ["$.orderId", "$.items"]
},
"RestoreCoupon": {
"Type": "ServiceTask",
"ServiceName": "couponService",
"ServiceMethod": "restore",
"Input": ["$.userId", "$.couponId"]
},
"CompensationTrigger": {
"Type": "CompensationTrigger",
"Next": "Fail"
},
"Succeed": {"Type": "Succeed"},
"Fail": {"Type": "Fail"}
}
}
六、踩坑实录
坑1:补偿操作不是幂等的
补偿操作被执行了两次,导致数据错误。
解决:补偿操作必须幂等,使用唯一标识防止重复执行。
坑2:缺少隔离性
两个Saga同时操作同一资源,导致数据不一致。
解决:使用悲观锁或乐观锁,保证资源互斥。
坑3:补偿失败
补偿操作也失败了,数据处于不一致状态。
解决:记录补偿失败的任务,人工介入处理。
坑4:Saga太长
一个Saga有10个步骤,任何一步失败都要补偿9步。
解决:拆分成多个小Saga,每个Saga不超过5步。
坑5:调试困难
Saga执行到一半失败,不知道当前状态。
解决:记录每一步的执行状态,提供可视化界面。
七、总结
Saga模式选型:
| 场景 | 推荐 |
|---|---|
| 简单流程(3步以内) | 编排式 |
| 复杂流程 | 协调式 |
| 需要可视化 | Seata Saga |
| 超长事务 | Saga > TCC |
最佳实践:
- 补偿操作必须幂等
- 控制Saga的步骤数量
- 做好补偿失败的处理
- 记录每一步的状态
- 提供可视化监控
血的教训:
Saga不是银弹。它解决了长事务的问题,但引入了补偿的复杂度。在决定用Saga之前,先想想能不能用更简单的方案。
思考题: 你的系统有没有跨服务的事务?用的什么方案?
个人观点,仅供参考