聊聊Saga模式——微服务长事务的救星
说实话,分布式事务这个话题,我在过去十年里踩过的坑比写过的代码还多。尤其是微服务架构流行起来之后,传统的ACID事务基本就废了——你想想看,一个业务操作要跨好几个服务,每个服务都有自己的数据库,怎么保证一致性?
Saga模式就是解决这个问题的经典方案。它不是新鲜东西,早在1987年就被提出来了,但直到微服务火了之后才真正被广泛使用。我个人习惯把Saga理解为“拆解大事务为一系列本地事务,并通过补偿机制来兜底”。
Saga的核心思想
说白了,Saga就是把一个长事务拆成多个小步骤,每个步骤都是一个独立的本地事务。如果某一步失败了,就反向执行之前步骤的补偿操作,把数据回滚到初始状态。
举个例子:一个电商下单流程,涉及订单服务、库存服务、支付服务、物流服务。如果用传统XA事务,所有服务得锁资源,性能差得要命。用Saga的话,每个服务各自提交,最后如果物流失败了,就依次取消支付、释放库存、取消订单。
关键点:Saga不要求所有步骤同时成功或同时失败,它允许中间状态存在,但最终要么全部成功,要么通过补偿回到初始状态。这就是所谓的“最终一致性”。
两种实现方式:编排 vs 协同
我在项目中两种方式都用过,各有适用场景。先看个对比表格:
| 特性 | 编排Saga(Choreography) | 协同Saga(Orchestration) |
|---|---|---|
| 控制中心 | 无,服务间通过事件通信 | 有,一个协调器负责调度 |
| 耦合度 | 低,服务间通过消息解耦 | 高,协调器依赖所有服务 |
| 复杂度 | 简单场景好用,复杂场景难追踪 | 适合复杂流程,但协调器可能成为瓶颈 |
| 典型实现 | 事件总线、消息队列 | 状态机、工作流引擎 |
| 我的建议 | 步骤少于5个时优先考虑 | 步骤多、补偿逻辑复杂时用 |
编排Saga实战
先看编排模式。每个服务在完成自己的事务后,发布一个事件,其他服务监听并触发下一步。如果某步失败,就发布失败事件,触发补偿。
我记得有一次做订单系统,用了编排Saga。订单服务创建订单后发“OrderCreated”事件,库存服务收到后扣库存,发“InventoryDeducted”事件,支付服务收到后扣款……
// 编排Saga示例:订单服务
public class OrderService {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 创建订单
orderRepository.save(event.getOrder());
// 发布事件,触发下一步
eventPublisher.publish(new InventoryCheckEvent(event.getOrderId()));
}
@EventListener
public void onPaymentFailed(PaymentFailedEvent event) {
// 补偿:取消订单
orderRepository.cancel(event.getOrderId());
// 通知库存释放
eventPublisher.publish(new InventoryReleaseEvent(event.getOrderId()));
}
}
避坑指南:我曾经在编排Saga里遇到过一个死循环——服务A发布事件,服务B处理后又发布事件,服务A又处理……后来我加了个事件去重机制,每个事件带唯一ID,消费前先查是否处理过。
协同Saga实战
协同模式就简单粗暴了——一个协调器(Orchestrator)负责告诉每个服务该做什么。协调器内部通常是一个状态机,记录当前执行到哪一步了。
我个人比较喜欢协同模式,因为流程清晰,出了问题也好排查。你想想看,所有步骤都在一个地方记录着,哪个步骤失败了,补偿逻辑怎么走,一目了然。
// 协同Saga示例:订单协调器
public class OrderSagaOrchestrator {
private SagaState state;
public void execute(OrderRequest request) {
state = SagaState.CREATED;
try {
// 步骤1:创建订单
orderService.createOrder(request);
state = SagaState.ORDER_CREATED;
// 步骤2:扣库存
inventoryService.deduct(request.getProductId(), request.getQuantity());
state = SagaState.INVENTORY_DEDUCTED;
// 步骤3:支付
paymentService.charge(request.getUserId(), request.getAmount());
state = SagaState.PAYMENT_COMPLETED;
// 步骤4:安排物流
logisticsService.schedule(request.getOrderId());
state = SagaState.COMPLETED;
} catch (Exception e) {
// 执行补偿
compensate(state, request);
}
}
private void compensate(SagaState failedState, OrderRequest request) {
// 根据失败时的状态,反向执行补偿
switch (failedState) {
case PAYMENT_COMPLETED:
paymentService.refund(request.getUserId(), request.getAmount());
case INVENTORY_DEDUCTED:
inventoryService.restore(request.getProductId(), request.getQuantity());
case ORDER_CREATED:
orderService.cancel(request.getOrderId());
}
}
}
注意:补偿操作本身也可能失败!我曾经遇到过支付退款接口超时,导致补偿卡住了。解决方案是:补偿操作也要支持重试,并且要有幂等性设计。说白了,同一个补偿请求执行多次,结果要一样。
补偿事务的设计要点
补偿事务是Saga的灵魂。设计补偿时,有几个原则我建议你记住:
- 幂等性:补偿操作可以重复执行,不会产生副作用。比如退款接口,同一个订单退款请求发两次,不能退两次钱。
- 可逆性:每个正向操作都要有对应的反向操作。扣库存对应释放库存,扣款对应退款。
- 空补偿:如果正向操作还没执行完就触发了补偿,补偿操作要能正确处理。比如订单还没创建成功,补偿时取消订单要能处理“订单不存在”的情况。
- 防悬挂:补偿操作先于正向操作到达时,要能正确处理。我一般用状态机+超时机制来解决。
Saga的适用场景
不是所有场景都适合用Saga。我总结了几条经验:
- 适合:业务流程长、跨多个服务、对实时一致性要求不高、可以接受短暂的不一致状态。
- 不适合:需要强一致性的场景(比如银行转账)、单个服务内的事务、步骤特别多且补偿逻辑极其复杂。
嗯,这里要注意一点:Saga不是银弹。如果业务对一致性要求极高,比如金融核心系统,我建议还是用TCC(Try-Confirm-Cancel)或者Seata AT模式。Saga更适合那些“最终一致就行”的业务。
一张图看懂Saga
下面这张图展示了编排Saga和协同Saga的核心流程对比:
总结一下
Saga模式是微服务长事务管理的利器。编排模式适合简单场景,服务间通过事件解耦;协同模式适合复杂场景,协调器统一调度。补偿事务的设计是重中之重,幂等性、可逆性、空补偿、防悬挂这四个点一定要处理好。
我在实际项目中,一般先用协同模式把流程跑通,等业务稳定了再考虑是否改成编排模式。毕竟协同模式好调试、好排查问题,对于线上故障处理来说,这一点太重要了。
我的建议:刚开始接触Saga的同学,先从协同模式入手。写一个状态机,把每个步骤和对应的补偿都列清楚。等你对流程烂熟于心了,再尝试编排模式。别一上来就搞事件驱动,容易把自己绕进去。