Skip to content

微服务中的 Saga 模式实战:订单/库存/支付场景的补偿设计

提出问题

分布式系统中,一个下单操作需要跨订单服务、库存服务、支付服务、积分服务协同完成。如果库存扣减成功但支付失败,或者支付成功但积分发放失败,如何保证数据最终一致?传统数据库的 ACID 事务在跨服务场景下无法使用(2PC 性能差、阻塞时间长),业务上必须接受"中间状态"。Saga 模式就是解决这个问题的标准方案——它将一个大事务拆分为多个本地事务,每个本地事务都有对应的补偿操作,当某个步骤失败时,按相反顺序执行补偿来撤销已执行的操作。

面试官问 Saga 时,通常不是在考你知不知道 Choreography 和 Orchestration 两种模式的区别,而是在考察你对补偿设计、幂等性、隔离性的实战理解。生产上 Saga 最容易踩坑的地方不是"选哪种模式",而是"补偿操作怎么保证最终成功"、"中间状态如何被其他事务感知"、"补偿悬挂怎么处理"。

分析问题

Saga 核心拆解:本地事务 + 补偿操作

Saga 模式的核心思想很简单:把分布式事务拆成一串本地事务,每个本地事务配一个"反操作"(补偿)。以下单流程为例:

订单服务(创建订单) → 库存服务(扣减库存) → 支付服务(扣款) → 积分服务(发积分)

每个步骤单独提交本地事务,成功后执行下一步。如果某一步失败(比如支付扣款失败),就按相反顺序执行补偿:

积分补偿(取消积分) → 支付补偿(退款) → 库存补偿(加回库存) → 订单补偿(取消订单)

关键点:补偿操作也是本地事务,它本身也可能失败。所以补偿操作必须支持重试,直到成功或达到最大重试次数后人工介入。

java
// Saga 协调器的状态机实现(简化版)
public class SagaOrchestrator {
    
    private final SagaStateRepository stateRepo;
    
    public SagaResult executeOrderSaga(OrderRequest request) {
        String sagaId = UUID.randomUUID().toString();
        stateRepo.save(new SagaState(sagaId, SagaStatus.STARTED));
        
        try {
            // 步骤 1: 创建订单
            stepWithRetry(sagaId, 1, () -> orderService.createOrder(request));
            stateRepo.updateStep(sagaId, 1, StepStatus.SUCCESS);
            
            // 步骤 2: 扣减库存
            stepWithRetry(sagaId, 2, () -> inventoryService.deduct(request.getSkuId(), request.getQuantity()));
            stateRepo.updateStep(sagaId, 2, StepStatus.SUCCESS);
            
            // 步骤 3: 扣款
            stepWithRetry(sagaId, 3, () -> paymentService.charge(request.getUserId(), request.getAmount()));
            stateRepo.updateStep(sagaId, 3, StepStatus.SUCCESS);
            
            // 步骤 4: 发积分
            stepWithRetry(sagaId, 4, () -> pointService.grant(request.getUserId(), request.getAmount()));
            stateRepo.updateStep(sagaId, 4, StepStatus.SUCCESS);
            
            stateRepo.updateStatus(sagaId, SagaStatus.COMPLETED);
            return SagaResult.success(sagaId);
            
        } catch (Exception e) {
            // 失败时触发补偿,按相反顺序
            compensate(sagaId, 4); // 先补偿积分
            compensate(sagaId, 3); // 再退款
            compensate(sagaId, 2); // 加回库存
            compensate(sagaId, 1); // 取消订单
            stateRepo.updateStatus(sagaId, SagaStatus.COMPENSATED);
            return SagaResult.failed(sagaId, e.getMessage());
        }
    }
    
    private void stepWithRetry(String sagaId, int step, Runnable action) {
        // 幂等检查:如果该步骤已成功执行,跳过
        StepStatus status = stateRepo.getStepStatus(sagaId, step);
        if (status == StepStatus.SUCCESS) return;
        
        // 带重试的执行(最多 3 次,指数退避)
        RetryUtils.retry(3, 100, action);
    }
    
    private void compensate(String sagaId, int step) {
        // 补偿操作的幂等重试
        RetryUtils.retry(5, 200, () -> {
            // 根据 step 调用对应的补偿接口
            stateRepo.executeCompensation(sagaId, step);
        });
    }
}

完整时序流程

为了理解 Saga 的补偿时序,这里用文字描述一次完整的正常流程和补偿流程:

正常流程时序

OrderService.createOrder (成功)
  → 提交本地事务,订单状态=CREATED
  → 通知 Orchestrator: step1 OK
InventoryService.deductStock (成功)
  → 提交本地事务,库存扣减+1
  → 通知 Orchestrator: step2 OK
PaymentService.charge (成功)
  → 提交本地事务,扣款 100 元
  → 通知 Orchestrator: step3 OK
PointService.grant (成功)
  → 提交本地事务,积分+100
  → 通知 Orchestrator: step4 OK
Orchestrator: 标记 Saga COMPLETED

补偿流程时序(step3 扣款失败)

OrderService.createOrder (成功)
InventoryService.deductStock (成功)
PaymentService.charge (失败,余额不足)
  → 抛异常,Orchestrator catch
  → Orchestrator: 标记 Saga COMPENSATING
PointService.compensateGrant (补偿:扣回积分)
  → 本步骤未执行过,跳过(幂等检查)
PaymentService.compensateCharge (补偿:退款)
  → 本步骤未扣款成功,跳过(幂等检查)
InventoryService.compensateDeduct (补偿:加回库存)
  → 执行:INVENTORY_TABLE.stock_quantity += 1
  → 记录补偿记录:compensation_records(saga_id, 'deduct')
OrderService.compensateCreate (补偿:取消订单)
  → 执行:ORDERS_TABLE.status = 'CANCELLED'
  → 记录补偿记录:compensation_records(saga_id, 'create')
Orchestrator: 标记 Saga COMPENSATED

Choreography vs Orchestration:选型不是二分法

两种实现方式各有适用场景,关键在于业务流程的复杂度和变更频率

Choreography(编排模式):没有中心协调器,每个服务执行完本地事务后通过 MQ 发送事件触发下一个服务。订单服务发"订单已创建"事件 → 库存服务订阅后扣库存,发"库存已扣减"事件 → 支付服务订阅后扣款...

java
// Choreography 模式:订单服务发送事件
@Service
public class OrderService {
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        // 1. 本地事务创建订单
        Order order = orderRepository.save(Order.createPending(request));
        
        // 2. 发送事件触发下一步(库存扣减)
        eventPublisher.publish(new OrderCreatedEvent(
            order.getId(), request.getSkuId(), request.getQuantity()
        ));
        
        return order;
    }
}

优点:服务间完全解耦,新增服务不用改已有代码。缺点:业务逻辑分散在多个服务的消息处理器中,一个流程跨 5 个服务,排错时需要看 5 套日志。最致命的坑:消息顺序不可控——如果积分服务先收到"支付成功"事件,再收到"库存扣减成功"事件,可能导致积分发放了但库存没扣减。

Orchestration(协调器模式):引入一个 Saga 协调器,负责编排整个流程。协调器通过状态机管理 Saga 实例状态,每个步骤完成后记录到数据库,宕机后重启能从持久化状态恢复。

java
// Orchestrator 的状态机持久化
@Entity
@Table(name = "saga_state")
public class SagaState {
    
    @Id
    private String sagaId;
    
    @Enumerated(EnumType.STRING)
    private SagaStatus status;  // STARTED / COMPLETED / COMPENSATING / COMPENSATED
    
    @ElementCollection
    @CollectionTable(name = "saga_steps")
    private List<StepRecord> steps;  // 每个步骤的执行状态
    
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// 重启恢复:扫描所有未完成的 Saga
@Component
public class SagaRecoveryJob {
    
    @Scheduled(fixedDelay = 30_000)
    public void recoverIncompleteSagas() {
        List<SagaState> incomplete = sagaRepo.findByStatusIn(
            List.of(SagaStatus.STARTED, SagaStatus.COMPENSATING)
        );
        for (SagaState saga : incomplete) {
            sagaOrchestrator.resume(saga);
        }
    }
}

选型建议:3-5 个服务、流程简单、变更频率低 → Choreography(轻量)。5-10 个服务、流程复杂、需要可观测 → Orchestration(可控)。生产环境多数团队最终会走向 Orchestration,因为可追踪、可恢复、可调试。

隔离性(Isolation)问题:Saga 最大的坑

Saga 不提供 ACID 中的隔离性——一个 Saga 执行过程中,其他 Saga 可能会看到中间状态(脏读)。这是面试中 P7 级必问的深度点。

场景:用户 A 下单 100 元,扣库存和扣款两个 Saga 同时执行。

Saga-1: 订单创建 → 库存扣减(成功) → 支付扣款(进行中)
Saga-2: 订单创建 → 库存查询(看到 Saga-1 已扣减,库存不足) → 失败

如果 Saga-1 最终支付失败触发补偿,库存加回,但 Saga-2 已经因为"库存不足"而失败返回了——Saga-2 被 Saga-1 的中间状态误导了

更真实的踩坑案例:某电商平台双十一期间,库存服务和支付服务之间存在 200ms 的延迟。Saga 协调器在扣库存成功后立即调用支付,但支付超时触发补偿取消订单。此时另一个用户在同一秒查询库存,发现库存已被释放(补偿生效),下单成功——但原 Saga 的支付重试请求恰好延迟到达,导致一笔扣款成功但订单已取消。这就是"补偿和后续请求时序交错"的经典问题

解决方案

  1. 语义锁(Semantic Lock):在资源上加状态标记。订单状态从 CREATING → PENDING → CONFIRMED,其他 Saga 看到 PENDING 状态时知道该资源正在被处理,不直接操作。
sql
-- 语义锁:订单状态字段作为锁标记
UPDATE orders 
SET status = 'PENDING', saga_id = 'saga-xxx'
WHERE id = ? AND status = 'CREATING';
-- 影响行数 = 0 说明已被其他 Saga 锁定
  1. 冲正抵消(Countermeasure):允许中间状态的脏读,但通过补偿操作抵消副作用。适用于库存这类"最终一致即可"的场景——短时间内的超卖可以容忍,因为补偿操作会把库存加回来。

  2. 版本号乐观锁:每个资源带版本号,更新时检查版本号是否被其他 Saga 修改过。

sql
-- 乐观锁版本号 + 补偿防止超卖
UPDATE inventory 
SET stock_quantity = stock_quantity - 1, version = version + 1
WHERE sku_id = ? AND stock_quantity >= 1 AND version = ?;
-- 影响行数 = 0 说明被其他事务修改了,重试或放弃

补偿悬挂(Compensation Hanging):比补偿失败更隐蔽的坑

补偿悬挂是指:正向操作实际上已经成功,但协调器因为超时判定为失败,于是执行补偿;但正向操作的成功消息事后才到达,导致补偿操作错误地撤销了一个实际上成功的操作。这是 Saga 模式中最容易踩的坑,比"补偿失败"隐蔽得多。

典型时序

协调器 → 支付服务: charge(100元)
  → 支付服务处理成功,返回响应
  → 网络延迟 3 秒,协调器超时
协调器: 超时,标记支付失败
协调器 → 订单服务: compensateCreate(取消订单)
  → 订单取消成功
协调器: 收到支付服务的成功响应(延迟到达)
  → 钱已扣,但订单已取消 → 用户白付了 100 元

落地解法:补偿操作执行前,必须查远端状态的当前快照,确认"我确实需要补偿"。

java
// 补偿前检查远端状态
public void compensateCreate(String sagaId, String orderId) {
    // 1. 查远端状态
    OrderStatus currentStatus = orderService.queryStatus(orderId);
    if (currentStatus != OrderStatus.CREATED) {
        // 订单已经被其他流程取消了,或者支付成功但订单状态不对
        log.warn("补偿跳过,订单当前状态={},不是预期的 CREATED", currentStatus);
        return;
    }
    
    // 2. 用乐观锁更新状态
    int affected = orderRepo.updateStatus(orderId, OrderStatus.CANCELLED, OrderStatus.CREATED);
    if (affected == 0) {
        // 状态被并发修改了,重新检查
        throw new ConcurrentModificationException("订单状态已被修改,需要重试补偿");
    }
    
    // 3. 记录补偿记录
    compensationRepo.save(new CompensationRecord(sagaId, "create", LocalDateTime.now()));
}

生产数据:某 O2O 平台上线 Saga 后,前三个月每月因补偿悬挂导致的数据不一致约 200-300 单(日均 7-10 单),占 Saga 总事务量的 0.05%。排查后发现根因全部是"补偿前未查远端状态"。加上远端状态检查后,补偿悬挂归零。

Saga vs TCC:面试官最爱问的对比

很多面试者会把 Saga 和 TCC(Try-Confirm-Cancel)混为一谈,或者只知道"Seata 支持 TCC 模式"这个结论。以下是两者的关键区别:

维度SagaTCC
事务粒度整个 Saga 一个大事务每个参与方一个 Try,各自预留资源
资源锁定不锁定,直接操作资源Try 阶段预留资源(如冻结库存、冻结金额)
隔离性不提供,业务层处理Try 天然提供写隔离(预留资源被独占)
补偿代价高——补偿需要完全撤销操作低——Cancel 释放预留资源即可
实现复杂度低——只需写正向+补偿逻辑高——每个服务需要写 Try/Confirm/Cancel 三套逻辑
适用场景长事务、跨系统、异构短事务、资源竞争激烈、要求高隔离性
典型框架纯手工实现Seata TCC、Hmily
性能高(无锁)中等(资源预留有锁开销)

选型判断:库存扣减这种资源竞争激烈的场景,TCC 的 Try 阶段冻结库存天然防超卖,比 Saga 的补偿方案更干净。但支付场景,TCC 的 Try 冻结金额需要银行接口支持预授权(authorization),很多第三方支付渠道不提供这个能力,只能走 Saga 的"先扣后退"。所以一个大型系统里往往是 Saga + TCC 混用——库存用 TCC Try 冻结,支付用 Saga 扣款。

补偿操作的幂等性设计

补偿操作可能因为网络超时、协调器重启等原因被重复调用。补偿必须幂等

java
// 库存补偿(加回库存)的幂等实现
@Component
public class InventoryCompensationService {
    
    @Autowired
    private CompensationRecordRepo compensationRepo;
    @Autowired
    private InventoryRepo inventoryRepo;
    
    @Transactional
    public void compensateDeduct(String sagaId, String skuId, int quantity) {
        // 幂等检查:去重表
        if (compensationRepo.existsBySagaIdAndAction(sagaId, "deduct")) {
            log.info("补偿已执行,跳过: sagaId={}, action=deduct", sagaId);
            return;
        }
        
        // 执行补偿:加回库存
        inventoryRepo.addStock(skuId, quantity);
        
        // 记录补偿执行记录
        compensationRepo.save(new CompensationRecord(sagaId, "deduct", LocalDateTime.now()));
    }
}

去重表设计(saga_id, action) 作为联合唯一键,同一个 Saga 的同一个补偿操作只执行一次。注意:补偿记录和补偿操作必须在同一个本地事务中,否则补偿记录写入失败但补偿已执行,下次重试会重复补偿。

补偿操作的幂等还需要考虑正向操作的幂等

正向操作本身也可能被重复调用(协调器重试)。如果正向操作不是幂等的,重试会导致重复扣库存、重复扣款。

java
// 正向操作幂等:用 saga_id 作为幂等键
@Transactional
public void deduct(String sagaId, String skuId, int quantity) {
    // 幂等检查
    if (deductRecordRepo.existsBySagaId(sagaId)) {
        log.info("库存扣减已执行,跳过: sagaId={}", sagaId);
        return;
    }
    
    // 扣减库存
    int affected = inventoryRepo.deductStock(skuId, quantity);
    if (affected == 0) {
        throw new InsufficientStockException(skuId, quantity);
    }
    
    // 记录扣减记录
    deductRecordRepo.save(new DeductRecord(sagaId, skuId, quantity));
}

注意:正向操作和补偿操作的幂等键不能冲突。正向用 (saga_id, "deduct"),补偿用 (saga_id, "compensate_deduct"),两个不同的键。

用 MQ 实现 Saga 的一件事特性

一个容易忽略的细节:Saga 的"正向操作 + 发送下一步事件"必须是原子操作。如果事务提交了但 MQ 消息没发出去,流程就断了。

java
// 本地消息表 + 定时任务,保证消息必达
@Service
public class EventualSender {
    
    @Autowired
    private EventMessageRepo eventRepo;
    @Autowired
    private KafkaTemplate<String, Object> kafka;
    
    @Transactional
    public void createOrderAndSendEvent(OrderRequest request) {
        // 1. 本地事务创建订单
        Order order = orderRepository.save(Order.createPending(request));
        
        // 2. 本地事务写入消息表(同一事务)
        EventMessage msg = new EventMessage(
            UUID.randomUUID().toString(),
            "ORDER_CREATED",
            order.getId(),
            LocalDateTime.now()
        );
        eventRepo.save(msg);
    }
    
    // 定时任务:扫描未发送的消息并发送
    @Scheduled(fixedDelay = 1_000)
    @Transactional
    public void sendPendingMessages() {
        List<EventMessage> pending = eventRepo.findTop100ByStatus(MessageStatus.UNSENT);
        for (EventMessage msg : pending) {
            try {
                kafka.send(msg.getTopic(), msg.getPayload()).get(3, TimeUnit.SECONDS);
                msg.setStatus(MessageStatus.SENT);
                eventRepo.save(msg);
            } catch (Exception e) {
                log.warn("消息发送失败,下次重试: msgId={}", msg.getId());
            }
        }
    }
}

核心逻辑:订单创建和消息写入在同一个本地事务里,定时任务保证消息最终发出。这比用 @TransactionalEventListener(phase = AFTER_COMMIT) 靠谱,因为 MQ 宕机时 AFTER_COMMIT 回调不会重试。

Seata AT 模式 vs 纯 Saga:他们不是一个东西

很多面试者会把 Seata AT 和 Saga 混为一谈,这是扣分项

维度纯 SagaSeata AT
原理业务方手动写补偿操作代理自动生成逆向 SQL(UNDO_LOG)
隔离性不提供,需业务层自己处理读已提交 + 写隔离(全局锁)
性能高(无锁)中等(有全局锁,TC 节点压力)
侵入性低(只需实现补偿接口)中(需接入 Seata 客户端)
适用场景长事务、跨异构系统短事务、同技术栈微服务
补偿可靠性业务方保证框架自动重试 + 手动回滚
超时场景协调器超时触发补偿全局锁超时回滚,不阻塞

真实案例:某物流系统用 Seata AT 管理运单分发流程,但每单涉及 3 个外部物流商 API 调用(非关系型数据库),Seata 的自动 UNDO 回滚无法回滚 HTTP 请求。后来改纯 Saga,手工写 3 个补偿接口(调用物流商取消运单 API),流程反而更可控。

生产实战:Saga 协调器的异常处理策略

java
// 更完善的 Saga 协调器(含超时和重试)
@Component
public class RobustSagaOrchestrator {
    
    private static final Duration STEP_TIMEOUT = Duration.ofSeconds(10);
    private static final int MAX_RETRIES = 3;
    private static final Duration COMPENSATE_TIMEOUT = Duration.ofSeconds(30);
    
    public SagaResult execute(String sagaId, List<SagaStep> steps) {
        stateRepo.save(sagaId, SagaStatus.STARTED);
        
        // 正向执行
        for (int i = 0; i < steps.size(); i++) {
            SagaStep step = steps.get(i);
            try {
                // 带超时的远程调用
                CompletableFuture<?> future = CompletableFuture.runAsync(
                    () -> step.execute(sagaId)
                );
                future.get(STEP_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
                stateRepo.recordStep(sagaId, i, StepStatus.SUCCESS);
                
            } catch (TimeoutException e) {
                // 超时场景:不确定远端是否成功,必须查远端状态
                log.warn("步骤 {} 超时,查询远端状态: sagaId={}", i, sagaId);
                boolean actuallySucceeded = step.queryRemoteStatus(sagaId);
                if (actuallySucceeded) {
                    stateRepo.recordStep(sagaId, i, StepStatus.SUCCESS);
                    continue; // 实际成功了,继续下一个
                }
                // 确实没成功,走补偿
                return compensate(sagaId, steps, i);
                
            } catch (Exception e) {
                return compensate(sagaId, steps, i);
            }
        }
        
        stateRepo.updateStatus(sagaId, SagaStatus.COMPLETED);
        return SagaResult.success(sagaId);
    }
    
    private SagaResult compensate(String sagaId, List<SagaStep> steps, int failedAt) {
        stateRepo.updateStatus(sagaId, SagaStatus.COMPENSATING);
        
        // 从失败步骤的前一个开始补偿,逆序执行
        for (int i = failedAt - 1; i >= 0; i--) {
            SagaStep step = steps.get(i);
            // 补偿操作最多重试 5 次,每次间隔 2 秒
            retryWithBackoff(5, Duration.ofSeconds(2), () -> {
                CompletableFuture<?> future = CompletableFuture.runAsync(
                    () -> step.compensate(sagaId)
                );
                future.get(COMPENSATE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
            });
            stateRepo.recordStep(sagaId, i, StepStatus.COMPENSATED);
        }
        
        stateRepo.updateStatus(sagaId, SagaStatus.COMPENSATED);
        return SagaResult.failed(sagaId, "步骤 " + failedAt + " 失败,已完成补偿");
    }
    
    private void retryWithBackoff(int maxRetries, Duration baseDelay, Runnable task) {
        for (int i = 0; i < maxRetries; i++) {
            try {
                task.run();
                return; // 成功
            } catch (Exception e) {
                if (i == maxRetries - 1) {
                    // 补偿都失败了,必须告警人工介入
                    log.error("补偿失败{}次,告警人工介入", maxRetries, e);
                    alertService.sendAlert("SAGA_COMPENSATE_FAILED", 
                        "补偿重试" + maxRetries + "次全部失败,请人工处理");
                    throw e;
                }
                // 指数退避:2s, 4s, 8s, 16s
                long waitMs = baseDelay.toMillis() * (1L << i);
                Thread.sleep(waitMs);
            }
        }
    }
}

这段代码的关键设计

  • 超时后不是直接补偿,而是先查远端状态(因为 TCP 超时不能代表远端失败)
  • 补偿重试用指数退避,最大 5 次,每次间隔 2s 起步
  • 补偿全部失败后告警人工介入,不走死循环
  • 每个步骤的补偿状态持久化,重启可恢复

总结

Saga 模式的核心要点:

维度要点
核心思想大事务拆小 + 每个事务配补偿操作
幂等性补偿和正向操作都必须幂等,用去重表保证
隔离性用语义锁/乐观锁/冲正抵消处理中间状态
补偿悬挂补偿前先查远端状态,防止错误撤销成功操作
重启恢复协调器持久化 Saga 状态,重启扫描未完成实例
选型原则简单场景 Choreography,复杂场景 Orchestration
与 TCC 关系不是替代品,实践中混合使用——库存用 TCC,支付用 Saga
超时处理先查远端状态再决定补偿,不盲目补偿

面试话术示例:"Saga 不是分布式事务的替代品,而是业务上接受中间状态的权衡。我们团队用 Orchestration 模式实现订单 Saga,协调器持久化每个步骤的状态到数据库,宕机后通过定时任务恢复未完成的 Saga。补偿操作有去重表保证幂等,库存用语义锁防止其他 Saga 读到中间状态,补偿前先查远端状态防止补偿悬挂。生产运行 1 年,Saga 失败率约 0.3%,恢复率 100%。"

生产避坑十条

  1. 补偿操作的事务边界必须和正向操作一致——不要在补偿里写业务逻辑,只做"撤销动作"
  2. 如果补偿也需要调用多个下游,那补偿本身也需要 Saga 模式,形成嵌套补偿。遇到这种情况,说明你的服务拆分粒度有问题,应该合并强关联的服务
  3. 超时场景不要直接补偿,先查远端状态。TCP 超时不代表远端没收到请求,盲目补偿会导致重复扣款
  4. 补偿失败必须要走告警通道,别静默吞掉异常。线上 90% 的分布式事务数据不一致问题,都是因为补偿失败后没人知道
  5. Saga 协调器必须是单线程 + 持久化状态机,不要用多线程并发执行 Saga 步骤,否则状态管理会乱
  6. 补偿操作的日志必须打印完整的 sagaId 和操作前后数据快照,方便人工介入时定位
  7. Saga 里不要混用同步 RPC 和异步 MQ——同步调用超时后补偿触发,但异步消息可能已经发出,导致补偿和消息同时执行
  8. 用 MQ 做 Choreography 时,事件消息必须带 sagaIdstepSequence,接收方根据这两个字段做幂等和顺序判断
  9. 补偿操作不要依赖外部系统的实时状态——可以用"补偿+延迟重试"模式,等外部系统状态稳定后再补偿
  10. 不要试图用 Saga 实现 100% 的数据一致性,接受"最终一致 + 对账修复"的兜底策略。对账系统才是分布式事务的最终兜底

参考:Chris Richardson. Microservices Patterns; 阿里中间件团队. Seata AT 模式原理; 宋净超等. 云原生分布式事务原理与实践; Caitie McCaffrey. Distributed Sagas: A Protocol for Coordinating Microservices; 京东中间件团队. Saga 模式在京东履约系统的实践.

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。