2. Saga 执行流程与补偿机制:正向事务与补偿事务的执行顺序、补偿操作的幂等性要求

2.1 Saga 事务模型的核心执行顺序

Saga 是一种长事务解决方案,将一个全局事务拆分为多个本地子事务(正向事务),并为每个子事务注册一个对应的补偿事务。其核心执行顺序遵循 “正向执行,逆向补偿” 的原则。

关键原则: 正向事务按编排顺序依次执行;一旦任意一个正向事务失败,Saga 引擎立即按 反向顺序 调用已成功执行的正向事务对应的补偿事务,以回滚整个业务流程。

以下是一个典型的 Saga 执行流程(以订单创建为例):

  1. 步骤 1(正向): 订单服务创建订单(状态:待支付)。
  2. 步骤 2(正向): 支付服务扣减用户余额。
  3. 步骤 3(正向): 库存服务扣减商品库存。
  4. 步骤 4(正向): 物流服务创建发货单。
  5. 步骤 5(成功): 所有正向事务完成,全局事务结束。

若步骤 3(库存扣减)失败,则执行补偿流程:

  1. 补偿步骤 1: 调用支付服务的补偿事务(退还用户余额)。
  2. 补偿步骤 2: 调用订单服务的补偿事务(将订单状态置为“已取消”)。
  3. 补偿结束: 全局事务回滚完成。
Saga 正向与补偿执行流程 T1: 创建订单 T2: 扣减余额 T3: 扣减库存 T4: 创建发货单 ✗ 失败 C3: 恢复库存 C2: 退还余额 C1: 取消订单 正向事务 补偿事务 失败节点

2.2 补偿操作的幂等性要求

在分布式系统中,网络超时、节点故障、重试机制等因素可能导致补偿事务被多次调用。因此,补偿操作必须实现幂等性,即多次执行与一次执行产生相同的业务结果,不会造成数据不一致或重复扣减。

幂等性等级 实现方式 适用场景 示例
天然幂等 操作本身多次执行结果相同 状态更新、删除操作 将订单状态置为“已取消”
业务键去重 使用全局唯一业务 ID 进行防重 资金操作、库存变更 退款时检查 refund_id 是否已处理
版本号控制 乐观锁 + 版本号校验 并发较高的资源更新 UPDATE ... WHERE version = old_version
状态机校验 仅允许特定状态流转 订单、工单等有状态实体 只有“待支付”状态才允许取消
⚠️ 常见陷阱: 补偿事务中直接执行“余额增加”操作,若未做幂等处理,多次补偿会导致用户余额被重复增加,造成资金损失。必须使用事务 ID + 去重表或状态机来保证幂等。

2.3 正向事务与补偿事务的配对关系

每个正向事务必须对应一个明确的补偿事务,且补偿逻辑必须能够完全撤销正向事务的副作用。以下是常见的配对模式:

  • 创建 ↔ 删除: 创建订单 → 取消订单(软删除或状态置为无效)。
  • 扣减 ↔ 增加: 扣减库存 → 恢复库存;扣减余额 → 退还余额。
  • 预留 ↔ 释放: 预留资源 → 释放预留资源(常用于 TCC 模式,但 Saga 中也可使用)。
  • 发送消息 ↔ 发送撤销消息: 发送通知 → 发送取消通知(或标记为已撤销)。
正向事务 ↔ 补偿事务配对关系 创建订单 取消订单(状态置为取消) 扣减库存 恢复库存(增加库存量) 扣减用户余额 退还用户余额(增加余额)

2.4 幂等性实现示例(代码片段)

以下是一个基于 业务键去重 的补偿幂等实现示例:

// 补偿事务:退还用户余额
public boolean compensateRefund(CompensationRequest request) {
    String refundId = request.getRefundId();  // 全局唯一业务键
    // 1. 检查去重表,防止重复处理
    if (idempotentChecker.isProcessed(refundId)) {
        log.info("补偿事务已处理,跳过重复执行,refundId={}", refundId);
        return true;  // 返回成功,避免 Saga 引擎继续重试
    }
    // 2. 执行实际退款逻辑
    boolean success = accountService.increaseBalance(request.getUserId(), request.getAmount());
    if (success) {
        // 3. 标记该业务键为已处理
        idempotentChecker.markProcessed(refundId);
    }
    return success;
}
💡 最佳实践: 补偿事务的幂等性检查应优先于业务逻辑执行。使用数据库唯一索引或 Redis 原子操作来保证去重记录的可靠性。同时,补偿事务的返回值应明确告知 Saga 引擎“已处理”或“无需重试”,避免无谓的超时重试。

2.5 执行顺序与幂等性对故障排查的影响

在故障排查场景中,理解 Saga 的执行顺序和幂等性要求有助于快速定位问题根因:

  • 超时场景: 若补偿事务因数据库连接池耗尽而超时,Saga 引擎会触发重试。如果补偿操作未实现幂等,重试可能导致数据重复修改。
  • 连接池水位过高: 当连接池被长时间占用(如慢 SQL 或锁等待),补偿事务可能排队超时。此时需要检查补偿事务的 SQL 执行计划,并确保幂等性检查的查询效率足够高。
  • 死锁与重试风暴: 多个 Saga 实例同时执行补偿,若幂等性检查未加锁或未使用原子操作,可能引发死锁或数据竞争,进一步推高连接池水位。

因此,在设计 Saga 补偿机制时,必须将 幂等性 作为第一优先级,并结合连接池监控和超时配置进行压测验证。