168|Saga 模式:编排式/协同式、补偿事务和长事务管理

分布式理论、事务与一致性约 10 分钟阅读5,509 字符
知识库文档 170 / 251

正文

1. 引言:分布式事务的困局与 Saga 的诞生

2007 年,eBay 的工程师们面临一个棘手的问题:一笔订单需要跨越支付、库存、物流三个独立服务,每个服务都有自己的数据库。传统的两阶段提交(2PC)在单体时代或许还能勉强应付,但在分布式环境下,2PC 的同步阻塞、单点故障和性能瓶颈让它几乎不可用。

更关键的是,eBay 的业务场景有一个特点:订单的生命周期可能长达数小时甚至数天。用户下单后,支付可能是异步的,库存可能在几分钟后才被确认,物流可能在几小时后才创建运单。你不可能让数据库一直持有锁等上几个小时。

正是在这种背景下,Saga 模式被正式提出。它的核心思想简单得令人意外:把一个长事务拆成多个本地事务,每个本地事务都有对应的补偿操作。如果中间某一步失败,不是回滚(已经没有全局事务这个概念了),而是按顺序执行之前每一步的补偿,把系统恢复到业务上的"一致"状态。

Saga 这个名字的灵感来自"长篇故事"(saga 在英文中意为"萨迦/长篇故事"),寓意一个由多个步骤组成的长事务,如同一个由多个章节组成的故事。

文章配图
文章配图

2. 核心概念:什么是 Saga

Saga 是一个由多个本地事务组成的序列,每个本地事务都更新各自服务内的数据库并发布一个事件或消息来触发下一个本地事务。如果某个本地事务失败,Saga 会执行一系列补偿事务来撤销之前已完成事务的影响。

Saga 的四个关键特征:

  1. 本地事务性:每个步骤在自己的服务内是 ACID 的。服务 A 更新自己的数据库,这个操作要么成功要么失败,不存在中间状态。
  2. 全局最终一致性:整个 Saga 不保证同时一致,但保证最终一致。如果所有步骤都成功,系统达到新的全局一致状态。
  3. 补偿机制:每个步骤定义了一个语义相反的补偿操作。例如"扣减库存"的补偿是"回补库存",而不是简单地"撤销事务"。
  4. 异步推进:步骤之间通过消息或事件驱动,不要求同步等待。

Saga 与 ACID 事务的对比:

维度 ACID 事务 Saga
范围 单个数据库 多个服务/数据库
一致性 强一致性(立即) 最终一致性
隔离性 严格隔离 需要额外处理
回滚 数据库自动回滚 补偿事务手动回滚
锁持有时间 事务期间持有 无全局锁
适用场景 短事务、单体应用 长事务、微服务

什么时候用 Saga,什么时候不用?

适合 Saga 的场景:

  • 跨多个微服务的业务流程(订单、支付、库存)
  • 事务步骤之间有较长的时间间隔
  • 对一致性要求不是"立刻"而是"最终"
  • 涉及第三方系统,无法参与分布式事务

不适合 Saga 的场景:

  • 需要强一致性的金融核心记账(考虑 TCC 或本地事务表)
  • 步骤很少且都是同步的(可以简化处理)
  • 补偿操作不可逆或代价极高(如发送了实物商品)

3. 两种协调模式:Choreography vs Orchestration

Saga 的实现有两种截然不同的协调方式,理解它们的区别是做架构决策的关键。

3.1 编排式(Choreography)—— 去中心化的舞蹈

编排式的核心是没有中心协调者。每个服务完成自己的本地事务后,发布一个事件,关心这个事件的其他服务各自响应。

工作流程:

文章配图
文章配图

code
订单服务 → 发布"订单已创建"事件
         ↓
支付服务 监听"订单已创建" → 执行支付 → 发布"支付已完成"事件
库存服务 监听"订单已创建" → 冻结库存 → 发布"库存已冻结"事件
         ↓
物流服务 监听"支付已完成" + "库存已冻结" → 创建运单 → 发布"运单已创建"事件

优点:

  • 松散耦合:服务之间只知道事件,不知道彼此的存在
  • 简单场景下实现简单:不需要额外的协调器服务
  • 天然适合事件驱动架构

缺点:

  • 流程隐式:没有地方能看到完整的业务流程,新人很难理解"一个订单到底经历了什么"
  • 循环依赖风险:A 监听 B 的事件,B 监听 A 的事件,一不小心就死循环
  • 补偿复杂:当某一步失败,谁负责触发补偿?补偿需要按照依赖的逆序执行,但编排式下这个顺序是隐式的
  • 调试困难:出问题时,你需要在多个服务的日志中拼凑完整的事件链路

代码示例:

java
// 订单服务 - 发布事件
@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);
        eventPublisher.publish(new OrderCreatedEvent(order.getId(), order.getAmount()));
    }
}

// 支付服务 - 监听事件并执行
@Service
public class PaymentService {
    @EventListener
    @Transactional
    public void handleOrderCreated(OrderCreatedEvent event) {
        Payment payment = paymentGateway.charge(event.getOrderId(), event.getAmount());
        paymentRepository.save(payment);
        eventPublisher.publish(new PaymentCompletedEvent(event.getOrderId(), payment.getId()));
    }
}

3.2 协同式(Orchestration)—— 中央指挥的乐队

协同式引入一个中心协调器(Orchestrator),它负责告诉每个参与者"该做什么",并根据响应决定下一步。

工作流程:

文章配图
文章配图

code
                    ┌──────────────────┐
                    │  Saga 协调器      │
                    │  (OrderSaga)      │
                    └──────┬───────────┘
           ┌───────────────┼───────────────┐
           ▼               ▼               ▼
      ┌─────────┐    ┌─────────┐    ┌─────────┐
      │ 订单服务 │    │ 支付服务 │    │ 库存服务 │
      └─────────┘    └─────────┘    └─────────┘

协调器: "创建订单" → 订单服务: "成功"
协调器: "执行支付" → 支付服务: "成功"  
协调器: "扣减库存" → 库存服务: "失败!库存不足"
协调器: "补偿支付" → 支付服务: "退款成功"
协调器: "补偿订单" → 订单服务: "取消成功"

优点:

  • 流程显式:协调器中的代码就是完整的业务流程定义,一目了然
  • 补偿逻辑集中:补偿的触发和执行顺序由协调器统一管理,不会遗漏
  • 依赖关系清晰:谁先谁后,谁依赖谁,在协调器中明确定义
  • 易于监控和调试:在一个地方就能看到整个 Saga 的状态

缺点:

  • 协调器成为关键单点:虽然可以通过集群部署解决,但架构上确实多了一个中心化组件
  • 服务间耦合到协调器:每个服务需要暴露给协调器调用的接口
  • 协调器可能变得臃肿:如果所有业务流程都放在协调器里,它可能变成一个"上帝对象"

代码示例:

java
@Service
public class OrderSagaOrchestrator {
    
    public void execute(CreateOrderRequest request) {
        SagaState state = new SagaState();
        
        try {
            // Step 1: 创建订单
            Order order = orderService.createOrder(request);
            state.setOrderId(order.getId());
            state.markStepCompleted("CREATE_ORDER");
            
            // Step 2: 执行支付
            Payment payment = paymentService.charge(order.getId(), request.getAmount());
            state.setPaymentId(payment.getId());
            state.markStepCompleted("PROCESS_PAYMENT");
            
            // Step 3: 扣减库存
            inventoryService.deduct(request.getItems());
            state.markStepCompleted("DEDUCT_INVENTORY");
            
            // 全部成功
            sagaStateRepository.save(state.toCompleted());
            
        } catch (Exception e) {
            // 按逆序执行补偿
            compensate(state);
            throw new SagaFailedException("Saga failed, compensated", e);
        }
    }
    
    private void compensate(SagaState state) {
        if (state.isStepCompleted("PROCESS_PAYMENT")) {
            paymentService.refund(state.getPaymentId());
        }
        if (state.isStepCompleted("CREATE_ORDER")) {
            orderService.cancel(state.getOrderId());
        }
    }
}

3.3 如何选择?

考虑因素 选编排式 选协同式
流程复杂度 简单(≤3 步) 复杂(>3 步)
团队规模 小团队,每个人了解全局 大团队,需要清晰的流程边界
可观测性要求 不高 高,需要集中监控
补偿逻辑复杂度 简单 复杂,需要精确控制顺序
是否已有事件驱动基础设施 有 没有或不成熟

实际项目中,混合使用是常态。 例如,使用协同式作为主流程控制,但步骤之间的通信通过消息队列(编排式的事件机制)来实现,这样既保留了流程的可见性,又获得了异步解耦的好处。

4. 补偿事务:正向操作与逆向补偿的设计原则

补偿事务是 Saga 最核心也最容易出错的部分。它不是简单的"撤销"——在分布式系统中,你无法像数据库那样通过 undo log 回到事务开始前的状态。

4.1 补偿的本质

补偿是一个业务级别的逆向操作,它让系统回到一个业务上可接受的状态,而不是物理上回到原始状态。

举例说明"撤销"和"补偿"的区别:

文章配图
文章配图

code
正向操作:用户下单 → 扣减库存 100 → 扣款 100 元
数据库回滚:库存 +100,退款 100 元(通过 undo log 自动完成)
Saga 补偿:库存 +100,退款 100 元(需要手动写代码实现)

看起来一样?再看这个例子:
正向操作:发送了"下单成功"的推送通知
数据库回滚:撤销推送(做不到)
Saga 补偿:发送"订单已取消"的推送通知(业务上的逆向操作)

4.2 补偿设计原则

原则一:补偿幂等

补偿操作可能被重复执行(重试、消息重复投递),必须保证多次执行补偿的结果和执行一次相同。

java
// 错误:每次补偿都退款
public void compensate(Long paymentId) {
    Payment payment = paymentRepo.findById(paymentId);
    paymentGateway.refund(payment.getTransactionId(), payment.getAmount());
    payment.setStatus("REFUNDED");
    paymentRepo.save(payment);
}

// 正确:先检查状态再补偿
public void compensate(Long paymentId) {
    Payment payment = paymentRepo.findById(paymentId);
    if (payment.getStatus().equals("REFUNDED")) {
        return; // 已补偿,幂等返回
    }
    paymentGateway.refund(payment.getTransactionId(), payment.getAmount());
    payment.setStatus("REFUNDED");
    paymentRepo.save(payment);
}

原则二:补偿允许最终失败

补偿本身也可能失败(退款接口挂了)。此时需要进入"补偿的补偿"——通常是人工介入或定时重试。

java
public void compensate(SagaState state) {
    for (Step step : state.getCompletedStepsInReverse()) {
        try {
            step.compensate();
        } catch (Exception e) {
            // 补偿失败,记录到失败表,触发告警
            compensationFailureRepository.save(
                new CompensationFailure(step, e.getMessage())
            );
            alertService.send("Saga 补偿失败,需人工介入", state.getSagaId());
            // 不要静默吞掉异常
        }
    }
}

原则三:正向操作和补偿操作必须成对设计

在设计每个 Saga 步骤时,同时设计它的补偿。不要写完正向操作后再说"补偿回头再想"。

code
正向:创建订单(状态=PENDING)
补偿:取消订单(状态=CANCELLED,记录取消原因)

正向:冻结库存(可用库存 -N,冻结库存 +N)
补偿:解冻库存(可用库存 +N,冻结库存 -N)

正向:发送通知邮件
补偿:发送取消通知邮件(而不是"撤回邮件",因为撤回做不到)

原则四:补偿的顺序必须是正向的逆序

如果正向顺序是 A → B → C,补偿顺序必须是 C⁻¹ → B⁻¹ → A⁻¹。这是死规定,不能改。

原因:如果 A 是"创建订单",B 是"扣款",C 是"发货"。如果 C 失败,你只能先补偿 C(可以不补偿,因为 C 失败了),再补偿 B(退款),最后补偿 A(取消订单)。如果先补偿 A(取消订单)再补偿 B(退款),用户会看到"订单已取消但钱还没退回来"。

5. 隔离性问题与应对策略

Saga 最棘手的问题不是补偿,而是隔离性缺失。在 ACID 事务中,隔离性保证了一个事务看不到另一个事务的中间状态。但在 Saga 中,由于步骤是顺序执行的,步骤之间时间间隔可能很长,其他 Saga 可以看到中间状态。

5.1 经典问题:丢失更新

假设两个 Saga 同时执行:

code
Saga A: 创建订单(状态=待支付)→ 扣款 → 扣库存
Saga B: 取消订单(检查状态=待支付 → 允许取消)→ 退款 → 回库存

时间线:
T1: Saga A 创建订单(状态=待支付)
T2: Saga B 检查状态=待支付,决定取消
T3: Saga A 扣款成功  ← 钱扣了!
T4: Saga B 退款(但 Saga A 刚扣的款还没到账!)
T5: Saga A 扣库存失败,触发补偿
T6: Saga A 补偿:退款(但 Saga B 已经退了一次!)
结果:用户被退款两次!

文章配图
文章配图

5.2 应对策略

策略一:语义锁(Semantic Lock)

在 Saga 开始时,在业务数据上设置一个"处理中"标记,阻止其他 Saga 操作同一资源。

java
// 订单表添加 saga_status 字段
public class Order {
    private Long id;
    private String status;       // PENDING, CONFIRMED, CANCELLED
    private String sagaStatus;   // null, IN_PROGRESS, COMPLETED, COMPENSATING
    private String sagaId;       // 当前 Saga 的 ID
}

// 取消订单时检查
public void cancelOrder(Long orderId) {
    Order order = orderRepo.findById(orderId);
    if ("IN_PROGRESS".equals(order.getSagaStatus())) {
        throw new SagaConflictException("订单正在被其他 Saga 处理");
    }
    // 执行取消...
}

策略二:交换式更新(Commutative Updates)

把操作设计成可交换的——即执行顺序不影响最终结果。

例如,账户余额的加减操作是可交换的(+100 再 -50 和 -50 再 +100,结果都是 +50),但"设置状态为已支付"不是可交换的。

策略三:悲观视图(Pessimistic View)

对关键资源加锁,让 Saga 步骤串行化。类似数据库的悲观锁,但锁的粒度是业务级别的。

java
// 使用 Redis 分布式锁
public void deductInventory(Long productId, int quantity) {
    String lockKey = "inventory:lock:" + productId;
    RLock lock = redisson.getLock(lockKey);
    try {
        if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
            // 执行扣减
        }
    } finally {
        lock.unlock();
    }
}

策略四:重读检查(Reread Check)

在执行关键操作前,重新读取数据,确认状态没有变化。

java
public void deductInventory(Long productId, int quantity) {
    Inventory inventory = inventoryRepo.findById(productId);
    if (inventory.getAvailableQuantity() < quantity) {
        // 触发补偿
        throw new InsufficientInventoryException();
    }
    // 乐观锁更新
    int updated = inventoryRepo.deductWithVersion(productId, quantity, inventory.getVersion());
    if (updated == 0) {
        throw new OptimisticLockException("库存被并发修改");
    }
}

6. 实战:Spring Boot + 消息队列实现 Saga

下面是一个基于 Spring Boot + RabbitMQ 的协同式 Saga 实现。

6.1 架构设计

文章配图
文章配图

code
┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│ order-service│   │payment-service│  │inventory-svc │
│  (MySQL)     │   │  (MySQL)      │  │  (MySQL)     │
└──────┬───────┘   └──────┬───────┘   └──────┬───────┘
       │                  │                  │
       └──────────────────┼──────────────────┘
                          │
                   ┌──────┴───────┐
                   │   RabbitMQ   │
                   └──────┬───────┘
                          │
                   ┌──────┴───────┐
                   │saga-orchestrator│
                   │   (MySQL)    │
                   └──────────────┘

6.2 Saga 状态管理

java
@Entity
@Table(name = "saga_instance")
public class SagaInstance {
    @Id
    private String sagaId;
    private String sagaType;        // "CREATE_ORDER"
    private String status;           // STARTED, COMPLETED, COMPENSATING, COMPENSATED, FAILED
    
    @OneToMany(mappedBy = "saga", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @OrderBy("stepOrder ASC")
    private List<SagaStep> steps;
    
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

@Entity
@Table(name = "saga_step")
public class SagaStep {
    @Id
    private String stepId;
    private String stepName;         // "CREATE_ORDER", "PROCESS_PAYMENT"
    private String serviceName;      // "order-service", "payment-service"
    private String command;          // "OrderCommand.create"
    private String compensateCommand; // "OrderCommand.cancel"
    private String status;           // PENDING, SUCCEEDED, FAILED, COMPENSATED
    private int stepOrder;
    private String requestPayload;   // JSON
    private String responsePayload;  // JSON
    
    @ManyToOne
    @JoinColumn(name = "saga_id")
    private SagaInstance saga;
}

文章配图
文章配图

6.3 协调器核心逻辑

java
@Component
public class SagaCoordinator {
    
    @Autowired
    private SagaInstanceRepository sagaRepo;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private ObjectMapper objectMapper;
    
    @Transactional
    public SagaInstance startSaga(SagaDefinition definition, Object requestData) {
        SagaInstance saga = SagaInstance.create(definition, requestData);
        sagaRepo.save(saga);
        executeNextStep(saga);
        return saga;
    }
    
    public void handleStepResponse(String sagaId, String stepId, boolean success, String responseJson) {
        SagaInstance saga = sagaRepo.findById(sagaId)
            .orElseThrow(() -> new SagaNotFoundException(sagaId));
        
        SagaStep step = saga.findStep(stepId);
        
        if (success) {
            step.markSucceeded();
            // 检查是否所有步骤完成
            if (saga.isLastStep(step)) {
                saga.markCompleted();
                sagaRepo.save(saga);
                return;
            }
            executeNextStep(saga);
        } else {
            // 步骤失败,启动补偿
            saga.markCompensating();
            step.markFailed(responseJson);
            compensate(saga, step);
        }
        sagaRepo.save(saga);
    }
    
    private void executeNextStep(SagaInstance saga) {
        SagaStep nextStep = saga.getNextPendingStep();
        nextStep.markPending();
        sagaRepo.save(saga);
        
        // 发送命令消息到目标服务
        SagaCommand command = new SagaCommand(
            saga.getSagaId(), nextStep.getStepId(),
            nextStep.getCommand(), nextStep.getRequestPayload()
        );
        String routingKey = "saga.command." + nextStep.getServiceName();
        rabbitTemplate.convertAndSend("saga.exchange", routingKey, 
            objectMapper.writeValueAsString(command));
    }
    
    private void compensate(SagaInstance saga, SagaStep failedStep) {
        List<SagaStep> completedSteps = saga.getCompletedStepsBefore(failedStep);
        // 逆序执行补偿
        Collections.reverse(completedSteps);
        for (SagaStep step : completedSteps) {
            SagaCommand compensateCommand = new SagaCommand(
                saga.getSagaId(), step.getStepId(),
                step.getCompensateCommand(), step.getResponsePayload()
            );
            String routingKey = "saga.compensate." + step.getServiceName();
            rabbitTemplate.convertAndSend("saga.exchange", routingKey,
                objectMapper.writeValueAsString(compensateCommand));
        }
        saga.markCompensated();
    }
}

6.4 服务端实现

java
// 各微服务中的 Saga 参与者
@Component
public class OrderSagaParticipant {
    
    @Autowired
    private OrderService orderService;
    
    @RabbitListener(queues = "saga.order-service.command")
    public void handleCommand(SagaCommand command) {
        try {
            switch (command.getCommand()) {
                case "OrderCommand.create":
                    Order order = orderService.createOrder(
                        parsePayload(command.getPayload(), CreateOrderRequest.class)
                    );
                    sendResponse(command, true, toJson(order));
                    break;
                case "OrderCommand.cancel":
                    orderService.cancelOrder(command.getSagaId());
                    sendResponse(command, true, "{}");
                    break;
            }
        } catch (Exception e) {
            sendResponse(command, false, "{\"error\":\"" + e.getMessage() + "\"}");
        }
    }
    
    private void sendResponse(SagaCommand command, boolean success, String response) {
        SagaStepResponse resp = new SagaStepResponse(
            command.getSagaId(), command.getStepId(), success, response
        );
        rabbitTemplate.convertAndSend("saga.exchange", "saga.response",
            objectMapper.writeValueAsString(resp));
    }
}

7. 生产环境关注点

7.1 幂等性

消息队列保证的是"至少一次投递",不是"恰好一次"。所以每个 Saga 步骤和补偿操作都必须是幂等的。

实现幂等的常见手段:

java
// 方法一:唯一键约束
@Transactional
public void createOrder(CreateOrderRequest request) {
    if (orderRepo.existsBySagaId(request.getSagaId())) {
        return; // 已处理过,幂等返回
    }
    Order order = new Order();
    order.setSagaId(request.getSagaId()); // 数据库唯一索引
    orderRepo.save(order);
}

// 方法二:状态机
public void processPayment(PaymentRequest request) {
    Payment payment = paymentRepo.findBySagaId(request.getSagaId());
    if (payment == null) {
        payment = new Payment(request.getSagaId(), "PENDING");
        paymentRepo.save(payment);
    }
    if (!"PENDING".equals(payment.getStatus())) {
        return; // 已处理或已补偿
    }
    // 执行支付...
}

7.2 超时处理

Saga 的某个步骤可能永远不返回(服务挂了、网络断了)。必须有超时机制。

java
@Component
public class SagaTimeoutChecker {
    
    @Scheduled(fixedDelay = 30000) // 每 30 秒检查一次
    public void checkTimeout() {
        List<SagaInstance> stuckSagas = sagaRepo.findByStatusAndUpdatedAtBefore(
            "STARTED", LocalDateTime.now().minusMinutes(5) // 5 分钟超时
        );
        for (SagaInstance saga : stuckSagas) {
            log.warn("Saga {} 超时,触发补偿", saga.getSagaId());
            saga.markCompensating();
            coordinator.compensate(saga, saga.getCurrentStep());
        }
    }
}

文章配图
文章配图

7.3 监控与告警

你需要监控的核心指标:

  • Saga 执行时长:P99 延迟,超过阈值告警
  • Saga 补偿率:补偿次数 / 总执行次数,突增说明有系统性问题
  • 补偿失败数:补偿也失败,需要人工介入
  • Saga 堆积数:处于 STARTED 状态超过 N 分钟的 Saga 数量
java
@Component
public class SagaMetrics {
    private final MeterRegistry registry;
    
    public void recordSagaCompleted(String sagaType, Duration duration) {
        Timer.builder("saga.duration")
            .tag("type", sagaType)
            .tag("result", "success")
            .register(registry)
            .record(duration);
    }
    
    public void recordSagaCompensated(String sagaType) {
        Counter.builder("saga.compensations")
            .tag("type", sagaType)
            .register(registry)
            .increment();
    }
}

7.4 最终一致性保证

Saga 保证的是最终一致性,不是强一致性。这意味着一笔订单的状态可能在短时间内(几百毫秒到几秒)处于"中间态"。

对前端和用户体验的影响:

java
// 前端轮询订单状态
@GetMapping("/orders/{id}/status")
public OrderStatus getStatus(@PathVariable Long id) {
    SagaInstance saga = sagaRepo.findByBusinessKey("ORDER_" + id);
    if (saga == null) {
        return new OrderStatus("UNKNOWN");
    }
    return switch (saga.getStatus()) {
        case "STARTED" -> new OrderStatus("PROCESSING", "订单处理中,请稍候...");
        case "COMPLETED" -> new OrderStatus("CONFIRMED", "订单已确认");
        case "COMPENSATING", "COMPENSATED" -> new OrderStatus("FAILED", "订单失败,已退款");
        default -> new OrderStatus("UNKNOWN");
    };
}

8. Saga 与其他分布式事务方案的对比

方案 一致性 吞吐量 复杂度 适用场景
2PC 强一致 低 中 同构数据库、短事务
TCC 最终一致 高 高 资金交易、需要预留资源
Saga 最终一致 高 中 长事务、跨服务编排
本地消息表 最终一致 高 低 简单场景,发消息+本地事务
AT 模式(Seata) 最终一致 中 低 不想改业务代码的场景

文章配图
文章配图

Saga 和 TCC 的区别经常被问到:

  • TCC 分 Try/Confirm/Cancel 三个阶段,Try 阶段需要预留资源
  • Saga 只有两步:正向操作 + 补偿操作,不需要预留资源
  • TCC 的 Cancel 是"释放预留资源",Saga 的补偿是"逆转已执行操作"
  • TCC 适合需要"锁定资源"的场景(如扣减余额),Saga 适合不需要锁定的场景(如创建订单)

面试官视角:高频追问

以下问题从面试官视角设计,考察你对 Saga 模式的理解深度,不限于本文内容。

Q1:Saga 的补偿操作失败了怎么办?

补偿操作失败意味着系统无法自动恢复到一致状态。此时需要进入"人工介入"流程。在设计上,补偿操作应该记录每次失败,进入"死信队列"或"补偿失败表",并由定时任务重试。重试次数用尽后触发告警,由运维或开发人员手动处理。关键设计原则:补偿操作必须幂等,这样重试才是安全的。

Q2:编排式和协同式在实际项目中怎么选?你的项目用了哪种?为什么?

考察点:不是背答案,而是看你能不能结合具体业务场景做决策。如果项目中流程简单(比如只有 3 个步骤),编排式的事件驱动足够,不需要引入额外的协调器。如果流程复杂(比如订单流程有 7-8 个步骤,涉及风控、优惠券、积分等多个系统),协同式的集中管理更合适。混合方案也很常见:用协同式管理主流程,用编排式的事件机制做异步通信。

Q3:Saga 如何保证隔离性?如果两个 Saga 同时操作同一笔订单怎么办?

Saga 的隔离性比 ACID 弱很多。可以通过以下方式加强:(1)语义锁——在订单上标记"处理中",阻止并发操作;(2)乐观锁——基于版本号更新,失败则重试或补偿;(3)在业务设计上避免冲突——比如订单状态机的设计让状态转换是单向的,不存在两个 Saga 可能同时修改同一个状态。

Q4:你们项目中 Saga 的状态存在哪里?怎么保证协调器本身的高可用?

Saga 状态通常存在协调器的数据库中。高可用方面:(1)协调器本身是无状态的,可以多实例部署,通过数据库锁或 Redis 锁保证同一 Saga 只被一个实例处理;(2)消息队列保证命令不会丢失;(3)如果协调器实例挂了,另一个实例可以通过定时任务扫描超时的 Saga 实例继续处理。

Q5:什么场景下 Saga 不适合?你遇到过 Saga 用错的案例吗?

Saga 不适合需要强一致性的场景,比如银行核心转账——如果一笔转账的扣款和入账之间出现不一致,即使是最终一致,用户也无法接受。Saga 也不适合补偿代价极高的场景——比如已经发送了实物商品,物理上的逆向操作成本太高。另外,如果 Saga 的步骤太多(比如超过 10 步),补偿成功的概率会指数级下降,这时候应该考虑拆分 Saga 或使用其他方案。

Q6:Saga 和事件溯源(Event Sourcing)有什么关系?能结合使用吗?

Saga 和事件溯源是天然的好搭档。事件溯源记录所有状态变更事件,Saga 本身就是事件驱动的。结合起来:Saga 的每一步产生一个事件,事件溯源存储这些事件,Saga 的状态可以从事件流中重建。这样即使协调器崩溃,也能从事件存储中恢复 Saga 的当前状态。Axon Framework 就是这种组合的典型实现。

Q7:如何测试 Saga 的补偿逻辑?

补偿逻辑是 Saga 中最容易出 Bug 的部分,必须专门测试。方法包括:(1)单元测试——Mock 每个步骤的失败场景,验证补偿是否被正确触发且顺序正确;(2)集成测试——使用 Testcontainers 启动真实的消息队列和数据库,模拟网络超时触发补偿;(3)混沌工程——在生产环境注入故障(如随机杀死某个服务),验证 Saga 的补偿能否正确执行。


本文为「Java 技术专家 / 架构师面试:250 个高频考点」系列第 168 篇,下一篇将讲解 169|分布式事务方案对比:2PC、TCC、Saga 与 AT 模式。