【分布式利器:事务】4、SAGA模式:长事务的最佳选择?

如果说TCC模式适合"短平快"的分布式事务(如电商下单的"创建订单+扣库存+支付"三步流程),那么SAGA模式就是为"长事务"而生的------当一个业务流程需要跨多个服务、经历多个步骤(甚至耗时几小时、几天),比如"物流订单从创建到签收""供应链从采购到入库",SAGA能通过"分步执行+反向补偿"保证最终一致性,且全程无锁阻塞。

今天我们就从"长事务的痛点"出发,拆解SAGA的两种实现方式(编排式vs协同式),用物流订单案例讲透"补偿事务设计"和"中间状态处理",并附上Seata框架的实战代码。

一、为什么需要SAGA?长事务的"TCC困境"

先看一个典型的长事务场景:物流订单流程。一个完整的物流订单需要经过5个步骤,跨4个服务:

  1. 订单服务:创建物流订单(状态:待确认);
  2. 库存服务:扣减商品库存(从仓库A扣10件);
  3. 支付服务:扣减用户运费(从账户扣50元);
  4. 仓库服务:安排发货(生成物流单,状态:已发货);
  5. 物流服务:用户签收(更新订单状态:已完成)。

这个流程的特点是步骤多、周期长(从创建到签收可能需要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的落地门槛不低:补偿事务的幂等性、中间状态的精细设计、流程的可视化管理,都需要开发者对业务有深刻理解。

下一篇,我们将讲解"本地消息表"和"事务消息",看看这两种基于消息的方案如何用"异步通信"解决分布式事务问题。

(觉得有用的话,欢迎点赞收藏,关注后续系列文章~)

相关推荐
lang201509282 小时前
Kafka延迟操作机制深度解析
分布式·python·kafka
zl9798998 小时前
RabbitMQ-下载安装与Web页面
linux·分布式·rabbitmq
zl97989914 小时前
RabbitMQ-Work Queues
分布式·rabbitmq
回家路上绕了弯16 小时前
日增千万数据:数据库表设计与高效插入存储方案
分布式·后端
Code_Artist16 小时前
robfig/cron定时任务库快速入门
分布式·后端·go
稚辉君.MCA_P8_Java16 小时前
通义千问 SpringBoot 性能优化全景设计(面向 Java 开发者)
大数据·hadoop·spring boot·分布式·架构
q***4641 天前
RabbitMQ高级特性----生产者确认机制
分布式·rabbitmq
百***48071 天前
RabbitMQ 客户端 连接、发送、接收处理消息
分布式·rabbitmq·ruby
喵手1 天前
【探索实战】Kurator打造一栈式分布式云原生平台的实践与前瞻!
分布式·云原生·kurator·云原生平台