引言:微服务拆分的代价

当单体应用被拆分为微服务后,一个业务请求往往需要跨越多个服务边界。传统数据库的ACID事务在分布式环境下鞭长莫及——跨服务、跨数据库、跨网络的两次提交(2PC)在高并发场景下几乎是不可接受的延迟元凶。

2017年,Hector Garcia-Molina和Kenneth Salem在经典论文中首次提出了Saga的概念。经过十多年的演进,Saga已成为微服务架构下管理长生命周期事务的事实标准模式。

本文将从原理到实战,带你彻底掌握Saga模式的两种实现范式,并通过一个完整的电商下单场景,吃透补偿事务、幂等性保障、空补偿处理等核心工程难题。

一、核心原理:Saga的基本模型

Saga由一系列本地事务(T1, T2, ..., Tn)组成,每个本地事务执行后会发布事件或消息触发下一个事务。如果Ti失败,Saga将按逆序执行补偿事务(Ci-1, ..., C1)来回滚已完成的工作。

// Saga 正向执行流程
T1 → T2 → T3 → T4 → T5
成功 | 成功 | 成功 | 失败 | (未执行)

// 逆序补偿执行流程
        C3 → C2 → C1
        回滚 | 回滚 | 回滚

核心设计原则:

  • 补偿先行:每个正向事务必须提前定义对应的补偿操作
  • 最终一致性:不追求强一致,但保证在故障恢复后达到一致状态
  • 可交换性:补偿操作必须幂等,可重试不产生副作用

二、两种编排范式深度对比

2.1 编排(Orchestration)——中央指挥官

Saga Orchestrator作为中央协调者,按预定义流程依次调用各服务的API。它掌控全局状态,决定下一步执行哪个服务。

// Orchestrator 伪代码示例
public class CreateOrderSaga {
    
    public void execute(OrderRequest request) {
        try {
            // 步骤1: 创建订单(待支付状态)
            Order order = orderService.createPendingOrder(request);
            
            // 步骤2: 扣减库存
            inventoryService.deduct(order.getItems());
            
            // 步骤3: 预扣资金(冻结用户余额)
            paymentService.freeze(order.getUserId(), order.getTotalAmount());
            
            // 步骤4: 扣减优惠券
            couponService.useCoupon(order.getUserId(), request.getCouponId());
            
            // 步骤5: 确认订单
            orderService.confirm(order.getId());
            
        } catch (Exception e) {
            // 任何步骤失败 → Orchestrator 自动触发补偿流程
            compensate(order);
            throw new SagaException("订单创建失败,已执行补偿", e);
        }
    }
    
    private void compensate(Order order) {
        // 逆序补偿:先回滚最后成功的步骤
        if (order.isCouponUsed()) couponService.refundCoupon(order.getCouponRecordId());
        if (order.isAmountFrozen()) paymentService.unfreeze(order.getUserId(), order.getFrozenRecordId());
        if (order.isInventoryDeducted()) inventoryService.restore(order.getItems());
        orderService.cancel(order.getId());
    }
}

优点:流程清晰、易调试、便于添加新步骤;缺点:存在单点风险,协调者本身可能成为瓶颈。

2.2 舞蹈(Choreography)——去中心化协作

没有中央协调者。每个服务监听上游事件,产生下游事件,通过事件驱动自动触发整个流程。

// 服务间通过事件异步协作
┌─────────────┐    OrderCreated     ┌─────────────┐
│ Order       │─────────────────────→│ Inventory   │
│ Service     │                      │ Service     │
└─────────────┘                      └──────┬──────┘
                                            │ InventoryReserved
                                            ▼
                                     ┌──────────────┐
                                     │ Payment      │
                                     │ Service      │
                                     └──────┬───────┘
                                            │ AmountFrozen
                                            ▼
                                     ┌──────────────┐
                                     │ Coupon       │
                                     │ Service      │
                                     └──────────────┘

// 失败时反向传播补偿事件
InventoryService --InsufficientStockEvent→ OrderService(取消订单)

优点:无单点、天然松耦合、弹性好;缺点:流程分散难追踪、循环依赖风险、调试困难。

2.3 选型决策矩阵

维度OrchestrationChoreography
流程复杂度适合多步骤(>5)、条件分支多适合简单线性流程(≤4)
团队规模>3个服务团队,需统一管控小团队,各服务自治
运维要求需监控协调者运行状态依赖事件总线可靠性
典型框架Seata、Axon、Camunda基于Kafka/RabbitMQ自实现

三、电商下单实战:完整代码实现(Orchestration方案)

3.1 架构全景

以下基于Spring Boot 3.x + Seata Saga的完整电商下单实现:

# application-saga.yml
seata:
  enabled: true
  application-id: order-saga-service
  tx-service-group: my_tx_group
  saga:
    state-machine:
      table-prefix: seata_saga_
      async-thread-pool:
        core-size: 8
        max-size: 32

3.2 状态机定义(JSON)

{
  "startState": "CreateOrder",
  "version": "0.1",
  "states": {
    "CreateOrder": {
      "type": "serviceTask",
      "serviceName": "orderServiceImpl",
      "methodName": "createPendingOrder",
      "compensateState": "CancelOrder",
      "next": "DeductInventory",
      "input": ["$.[request]"],
      "output": {"orderId": "$.#orderId"}
    },
    "DeductInventory": {
      "type": "serviceTask",
      "serviceName": "inventoryServiceImpl",
      "methodName": "deductStock",
      "compensateState": "RestoreInventory",
      "next": "FreezePayment",
      "input": ["$.[request]", "[orderId]"]
    },
    "FreezePayment": {
      "type": "serviceTask",
      "serviceName": "paymentServiceImpl",
      "methodName": "freezeAmount",
      "compensateState": "UnfreezePayment",
      "next": "UseCoupon"
    },
    "UseCoupon": {
      "type": "serviceTask",
      "serviceName": "couponServiceImpl",
      "methodName": "consumeCoupon",
      "compensateState": "RefundCoupon",
      "next": "ConfirmOrder"
    },
    "ConfirmOrder": {
      "type": "serviceTask",
      "serviceName": "orderServiceImpl",
      "methodName": "confirmOrder",
      "end": true
    },
    "CancelOrder":     { "type": "serviceTask", "methodName": "cancelOrder", "end": true },
    "RestoreInventory":{ "type": "serviceTask", "methodName": "restoreStock", "end": true },
    "UnfreezePayment": { "type": "serviceTask", "methodName": "unfreezeAmount", "end": true },
    "RefundCoupon":    { "type": "serviceTask", "methodName": "refundCoupon", "end": true }
  }
}

3.3 幂等性保障——解决空补偿和悬挂

// 关键:每个补偿操作必须检查前面步骤是否真正执行过
// 否则会引发"空补偿"问题

@Transactional
public void restoreStock(Long orderId) {
    // 查询正向扣减记录
    StockOpLog opLog = stockOpLogRepo.findByOrderIdAndType(orderId, OperationType.DEDUCT);
    
    if (opLog == null) {
        // 空补偿:正向操作从未执行 → 直接返回成功
        log.info("空补偿检测:orderId={} 未执行过库存扣减,跳过", orderId);
        return;
    }
    
    if (opLog.isReversed()) {
        // 已补偿 → 幂等返回
        log.info("幂等检测:orderId={} 已执行过库存恢复,跳过", orderId);
        return;
    }
    
    // 真正执行恢复
    inventoryRepo.increase(opLog.getSkuId(), opLog.getQuantity());
    opLog.markReversed();
    stockOpLogRepo.save(opLog);
}

关键陷阱与对策:

陷阱场景对策
空补偿先触发补偿再执行正向 → 补偿发现无事可做正向表记录存在性判断
悬挂补偿完全完成,正向操作才姗姗来迟正向操作前检查补偿状态,已补偿则拒绝执行
幂等失效网络超时重试导致同一操作执行多次唯一约束 + 版本号乐观锁

3.4 Saga事务日志持久化

// Seata Saga 自动持久化状态机执行日志
// 关键表结构(简化)
CREATE TABLE seata_saga_state_machine_inst (
    id              VARCHAR(100) PRIMARY KEY,
    business_key    VARCHAR(100) UNIQUE,    -- 幂等键(订单号)
    machine_id      VARCHAR(100),
    start_time      DATETIME,
    end_time        DATETIME,
    status          VARCHAR(10),             -- RUNNING/FAILED/SUCCESSED/COMPENSATED
    compensation_status VARCHAR(10),
    running_result  JSON                     -- 失败时的上下文快照
);

// 通过business_key实现天然幂等:同一订单号的重复请求直接返回历史结果

四、高级模式:Saga与其他分布式事务方案的对比

方案一致性性能适用场景
2PC/XA强一致低(阻塞等待)跨库且要求绝对一致(银行核心)
TCC最终一致中高需要预留资源的场景(积分+余额)
AT模式最终一致高简单CRUD,数据库直连代理
Saga最终一致高长流程、跨服务、可补偿性操作
本地消息表最终一致中异步操作,无需实时协调

Saga的独特优势在于:天然适配长事务。如果业务流程需要等待外部事件(如人工审核、第三方回调),Saga可以在中间态安全地"暂停"并稍后恢复,而TCC的Try阶段长时间持有预留资源是不现实的。

五、生产级最佳实践

  1. 事务粒度最小化:每个正向事务尽可能短小,降低补偿概率和成本
  2. 补偿操作前置设计:在设计确定阶段就定义好每步的补偿逻辑,而非事后补救
  3. 幂等贯穿始终:所有参与Saga的服务接口必须保证幂等性
  4. 超时与死锁检测:为Saga实例设置最大执行时间,防止"僵尸事务"
  5. 可观测性三板斧:日志聚合 + 状态机状态大盘 + 补偿告警(空补偿率异常升高往往是上游bug的信号)
  6. 对账兜底:定时任务扫描不一致状态(如订单已创建但库存未扣),自动修复或告警人工介入

六、总结

Saga模式的本质是用"最终一致性+补偿"替代"即时一致性+锁定"。它不追求ACID的强一致承诺,但让长生命周期事务在分布式环境下变得可控。

选择Orchestration还是Choreography,取决于你的团队规模和流程复杂度。如果你的订单流程涉及超过5个服务且需要频繁变更,Orchestrator + Seata Saga的组合是一个稳妥的工程选择。

记住,没有银弹——最适合你业务场景的方案才是好方案。先从补偿操作的设计开始,把每一步的可逆性想透,Saga就成功了一大半。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部