Saga模式入门与实战:编排与协同模式、补偿设计原则与Java生产级实现
在分布式系统中,Saga模式是一种通过将长事务拆解为多个可补偿的本地事务来维护数据最终一致性的架构模式。本文从核心概念讲起,深入对比编排与协同两种实现方式,并给出完整的Java/Spring Boot实战示例与选型建议。
博客:https://blog.csdn.net/badao_liumang_qizhi
一、为什么需要Saga模式
1.1 分布式事务的困境
在单体应用中,事务管理非常简单——Spring的@Transactional就能搞定一切。但当我们把系统拆分为微服务后,每个服务拥有独立的数据库,传统的ACID事务不再适用。一个跨越订单服务、支付服务、库存服务的业务流程,如何保证“要么全成功、要么全失败”?
类比:Saga就像跨国旅行的行李转运。你从北京出发,经上海转机到巴黎。每段航程(本地事务)独立完成并立即确认。但如果上海到巴黎的航班取消了(某一步失败),你不能“撤销”已经飞完的北京到上海段——你只能让航空公司把行李从上海运回北京(补偿操作)。这就是Saga的核心理念:不追求实时一致性,而是通过补偿操作实现最终一致性。
1.2 Saga的起源与核心思想
Saga这个概念来源于三十多年前的一篇数据库论文,一个Saga事务是一个有多个短时事务组成的长时的事务。在分布式事务场景下,我们把一个Saga分布式事务看作是一个由多个本地事务组成的事务,每个本地事务都有一个与之对应的补偿事务。
Saga通过一系列有序的本地事务和相应的补偿事务来保证最终一致性,每个本地事务完成后立即提交,如果发生错误,则按照顺序执行相应的补偿事务以回滚之前的操作。
关键要点:Saga不像2PC那样“先锁定再提交”,而是“先提交再说”——先用着,如果失败再作补偿回去。这种设计避免了2PC的同步阻塞和单点故障问题,代价是牺牲了隔离性。
1.3 Saga vs TCC:核心区别
| 维度 | TCC | Saga |
|---|---|---|
| 一致性 | 强一致(业务层) | 最终一致 |
| 开发成本 | 高,每个操作要写Try/Confirm/Cancel | 中,每个操作写正向+补偿 |
| 侵入性 | 非常强,每个业务操作需拆为三个阶段 | 中,主要增加补偿逻辑 |
| 适用场景 | 资金等对一致性要求极高的场景 | 长流程、可补偿业务 |
| 隔离性 | 通过资源预留保证 | 不保证,需要额外的隔离性应对策略 |
选型建议:核心链路用TCC保证强一致性,外围流程用Saga提升性能。Saga的优势很明显——最终一致性足以满足大多数业务场景,对现有业务代码侵入小,主要是增加补偿逻辑。
二、Saga的两种实现模式
Saga的执行模式主要有两种:编排模式和协同模式。
2.1 编排模式(Orchestration)
由一个集中式的协调者(Orchestrator)管理和控制Saga的执行,负责触发每个本地事务并处理失败后的补偿事务。
┌─────────────────────────────┐ │ Saga 协调者 (Orchestrator) │ └──────────────┬──────────────┘ │ ┌──────────────────┼──────────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 创建订单 │─────▶│ 扣减库存 │─────▶│ 扣款支付 │ │ (T1) │ │ (T2) │ │ (T3) │ └─────────┘ └─────────┘ └─────────┘ │ 失败 ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 取消订单 │◀─────│ 恢复库存 │◀─────│ 退款 │ │ (C1) │ │ (C2) │ │ (C3) │ └─────────┘ └─────────┘ └─────────┘优点:中央控制,协调者统一管理事务和补偿逻辑,简化了事务管理;清晰的事务流,事务和补偿步骤的执行顺序明确。
缺点:单点故障,协调者成为单点故障,需要保证其高可用性;复杂度集中,协调者需要处理所有事务和补偿逻辑。
适用场景:业务流程明确、步骤较少;事务失败后需要严格执行补偿逻辑;中央协调能够简化业务逻辑。
2.2 协同模式(Choreography)
每个服务自己决定何时开始下一个本地事务,通过事件驱动的方式进行协调。把Saga的决策和执行顺序逻辑分布在每一个参与方中,它们通过交换事件的方式来进行沟通。
┌─────────┐ 事件:订单已创建 ┌─────────┐ 事件:库存已扣减 ┌─────────┐ │ 订单服务 │─────────────────▶│ 库存服务 │─────────────────▶│ 支付服务 │ │ │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ ▲ │ │ 事件:支付失败 → 触发补偿流程 │ └───────────────────────────────────────────────────────────┘优点:松耦合,服务之间不需要直接依赖;适合简单流程,对于参与方较少的场景更简洁。
缺点:紧耦合(服务之间通过事件隐式耦合)、分布式事实源、故障排查困难、测试环境复杂、维护成本高。协同式会存在循环依赖的问题,因此编排式被《微服务架构设计模式》作者所推荐。
2.3 两种模式的选择决策
选编排模式:流程步骤较多(>5步)、需要集中监控和追踪、团队希望清晰的事务流控制。
选协同模式:流程简单(2-3步)、服务之间已经通过事件驱动架构解耦、团队对分布式事件流有丰富经验。
三、补偿事务的设计原则
3.1 三个核心原则
Saga的事务性补偿是关键,补偿事务需要保证以下三点:
原则一:幂等性—— 补偿事务需要幂等,以确保多次执行的结果一致。在分布式事务执行过程中,原服务与补偿服务都需要保证幂等性。由于网络连接可能超时,可以设置重试策略,重试发生时要通过幂等控制避免业务数据重复更新。
类比:幂等性就像按电梯按钮。按一次电梯来,按五次电梯还是来一次——结果不会因为操作次数而改变。补偿操作必须设计为“按多少次都一样”。
原则二:允许空补偿—— 空补偿指的是原服务未执行,补偿服务已执行。针对该问题,在服务设计时,需要允许空补偿,即在没有找到要补偿的业务主键时,返回补偿成功,并将原业务主键记录下来,标记该业务流水已补偿成功。
原则三:防止悬挂—— 悬挂指的是补偿服务比原服务先执行。此时需要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝执行该笔服务,以免造成数据不一致。
3.2 隔离性问题与应对
Saga模式由于一阶段已经提交本地数据库事务,且没有进行预留动作,因此无法保证隔离性。在极端情况下,可能由于脏写无法完成回滚操作。
应对策略:在实践中,一般可以采取以下方法应对该问题:
- “宁可长款,不可短款”原则:业务流程设计时,始终遵循这个原则。长款指的是客户少了钱机构多了钱,以机构信誉可以给客户退款;反之则是短款,少的钱可能追不回来了。所以在业务流程设计上,一定是先扣款。
- 向前恢复能力:有些业务场景可以允许让业务最终成功,在回滚不了的情况下,可以继续重试完成后面的流程。状态机引擎除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力,让业务最终执行成功,达到最终一致性的目的。
3.3 补偿操作的重试
补偿操作本身也可能失败,需要重试。但重试就要保证幂等,不然会出问题。Saga补偿操作必须加幂等检查。建议为补偿操作配置独立的重试策略和死信队列,当补偿操作多次重试仍失败时,触发人工介入告警。
四、Java实战:Spring Boot实现Saga模式
4.1 依赖配置
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency></dependencies>4.2 编排模式实现:Saga协调者
@ServicepublicclassOrderSagaOrchestrator{privatefinalOrderServiceorderService;privatefinalPaymentServicepaymentService;privatefinalInventoryServiceinventoryService;/** * 编排模式Saga执行 * 正向操作按顺序执行,失败时反向补偿 */publicSagaResultexecuteOrder(OrderRequestrequest){List<SagaStep>executedSteps=newArrayList<>();SagaContextcontext=newSagaContext(request);try{// Step 1: 创建订单orderService.createOrder(context);executedSteps.add(newSagaStep("createOrder",()->orderService.cancelOrder(context)));// Step 2: 扣减库存inventoryService.reserveStock(context);executedSteps.add(newSagaStep("reserveStock",()->inventoryService.releaseStock(context)));// Step 3: 扣款支付paymentService.charge(context);executedSteps.add(newSagaStep("charge",()->paymentService.refund(context)));returnSagaResult.success(context);}catch(Exceptionerror){// 反向补偿Collections.reverse(executedSteps);for(SagaStepstep:executedSteps){try{step.compensate();}catch(ExceptioncompError){// 补偿失败,记录到死信队列,人工介入deadLetterQueue.add(newCompensationFailure(step.getName(),context,compError));}}returnSagaResult.failure(error);}}}4.3 协同模式实现:事件驱动
// OrderService.java - 发布事件@Service@TransactionalpublicclassOrderService{privatefinalKafkaTemplate<String,OrderEvent>kafkaTemplate;publicOrdercreateOrder(OrderRequestrequest){Orderorder=newOrder();order.setStatus(OrderStatus.PENDING);order=orderRepository.save(order);// 发布事件,触发下一个服务的库存扣减kafkaTemplate.send("order-created",newOrderEvent(order.getId(),request.getProductId(),request.getQuantity(),request.getAmount()));returnorder;}}// InventoryService.java - 监听事件@ServicepublicclassInventoryService{@KafkaListener(topics="order-created")publicvoidonOrderCreated(OrderEventevent){try{inventoryRepository.reserveStock(event.productId(),event.quantity());// 库存扣减成功,触发支付kafkaTemplate.send("stock-reserved",event);}catch(Exceptione){// 库存不足,触发补偿kafkaTemplate.send("order-cancelled",event);}}}4.4 补偿操作配置
@ConfigurationpublicclassSagaCompensationConfig{@BeanpublicMap<String,CompensationAction>compensationMapping(){Map<String,CompensationAction>mapping=newHashMap<>();mapping.put("createOrder",ctx->orderService.cancelOrder(ctx.getOrderId()));mapping.put("reserveStock",ctx->inventoryService.releaseStock(ctx.getProductId(),ctx.getQuantity()));mapping.put("charge",ctx->paymentService.refund(ctx.getPaymentId()));returnmapping;}}五、生产运维与最佳实践
5.1 故障处理
| 故障类型 | 处理策略 |
|---|---|
| 正向操作超时 | 重试(指数退避,最多3次),仍失败则触发补偿 |
| 补偿操作失败 | 进入死信队列,发送告警,人工介入 |
| 协调者宕机 | 基于状态机的检查点恢复,从最后成功步骤继续 |
| 网络分区 | 基于幂等性重试,防止重复执行 |
5.2 监控指标
| 指标 | 目标值 | 说明 |
|---|---|---|
| Saga成功率 | > 95% | 正向操作全部成功的比例 |
| 补偿成功率 | > 99% | 补偿操作成功回滚的比例 |
| 平均Saga耗时 | < 30秒 | 端到端执行时间 |
| 死信队列积压 | = 0 | 需要人工介入的补偿失败数 |
| 补偿触发率 | < 5% | 触发补偿的Saga占比 |
5.3 最佳实践清单
- 补偿操作必须幂等:使用事务ID去重,防止重复执行
- 允许空补偿:补偿操作找不到原业务记录时返回成功
- 防止悬挂:补偿先于正向执行时拒绝处理
- 记录完整审计日志:每次正向和补偿操作都记录到数据库
- 补偿操作独立重试:与正向操作分离的重试策略和死信队列
- “宁可长款,不可短款”:业务流程设计先扣款后加款
- 控制Saga步骤数:建议不超过5步,步骤越多补偿越复杂
六、总结
Saga模式是分布式事务中一种实用且低侵入的解决方案。它通过“拆分本地事务+补偿事务”的思路,实现了低侵入、高适配的最终一致性。
| 维度 | 编排模式 | 协同模式 |
|---|---|---|
| 控制方式 | 中央协调者 | 事件驱动 |
| 复杂度 | 集中在协调者 | 分散在各服务 |
| 适合步骤数 | 多步骤(>5步) | 简单流程(2-3步) |
| 可维护性 | 高 | 低 |
| 推荐度 | ★★★★★ | ★★★ |
推荐学习路径:
- 先理解Saga与TCC、2PC的本质区别
- 掌握编排模式(推荐首选)的实现方式
- 深入理解幂等、空补偿、悬挂三大设计原则
- 在Spring Boot中实践电商订单的Saga流程
- 学习Seata Saga、ServiceComb Pack等生产级框架
参考资源:
- Apache ServiceComb Pack
- Seata SAGA 模式
- Saga模式解析分布式长事务原理与实现 - 阿里云
- Saga 模式服务设计 - SOFAStack
- How to Implement Saga Pattern in Spring Boot
- 微服务架构中的Saga模式详解 - Baeldung中文网
- 分布式事务最佳实践:TCC空回滚、Saga补偿风暴与选型建议 - 阿里云
博客: https://blog.csdn.net/badao_liumang_qizhi