深入理解微服务分布式事务:Saga 模式详解与实战

深入理解微服务分布式事务:Saga 模式详解与实战

在微服务架构中,系统被拆分成多个独立的服务,这种拆分带来了灵活性,但也打破了单体应用中数据库 ACID 事务的边界。传统的两阶段提交(2PC/XA)因性能差和锁竞争问题,已不再适用于高并发的微服务场景。

Saga 模式作为一种最终一致性的解决方案,成为了处理长流程分布式事务的首选策略。

什么是 Saga 模式?

Saga 模式将一个长事务拆分成一系列本地事务(Local Transaction)。每个本地事务更新单个服务的数据,并发布消息或事件来触发下一个本地事务。如果某个步骤失败,Saga 会执行一系列补偿事务(Compensating Transaction),以此回滚之前的操作,确保系统最终处于一致状态。

Saga 并不保证隔离性(Isolation),用户可能会在事务中间状态看到未完成的数据(例如:订单已创建但未扣减库存),这符合 BASE 理论(基本可用、软状态、最终一致性)。

Saga 的两种核心实现模式

Saga 主要有两种协调方式:协同式(Choreography)编排式(Orchestration)

1. 协同式(Choreography)—— 基于事件

没有中央协调者,每个参与的服务在完成本地事务后发布领域事件,其他服务订阅该事件并作出反应。

  • 优点:简单,服务间解耦,适合参与者较少的简单业务。
  • 缺点:业务流程分散在各个服务中,难以全局追踪,容易产生循环依赖。

2. 编排式(Orchestration)—— 基于命令

引入一个Saga 协调器(Orchestrator)(通常由发起事务的服务承担)。协调器负责告诉每个参与者该做什么(发送命令),并根据参与者的反馈(成功或失败)决定下一步是继续还是回滚。

  • 优点:流程逻辑集中,易于维护和监控,避免循环依赖。
  • 缺点:协调器可能成为单点瓶颈(需高可用设计)。

实战:基于 Spring Cloud Stream 的协同式 Saga(函数式编程版)

注:以下代码采用 Spring Cloud Stream 目前推荐的函数式编程风格(Functional Style)

场景定义

  1. 订单服务:创建订单(步骤 A)
  2. 库存服务:扣减库存(步骤 B)
  3. 支付服务:扣减余额(步骤 C)
  4. 物流服务:创建运单(步骤 D,假设此处失败)

核心实现逻辑

1. 消息通道配置 (application.yml)

使用函数式绑定名称:

spring:
  cloud:
    function:
      definition: checkInventory;handleInventoryCompensate # 定义消费者函数
    stream:
      bindings:
        checkInventory-in-0: # 输入绑定
          destination: order.created.topic
          group: inventory-service-group
        handleInventoryCompensate-in-0:
          destination: payment.failed.topic # 监听支付失败,触发回滚

2. 订单服务(发起者 & 最终回滚)

@Service
public class OrderService {
    
    @Autowired
    private StreamBridge streamBridge; // Spring Cloud Stream 3.x+ 发送消息推荐方式

    @Transactional
    public void createOrder(Order order) {
        // 1. 本地落库:状态为 PENDING
        orderRepository.save(order);
        
        // 2. 发送事件 (注意:这里存在双写风险,生产环境建议配合本地消息表)
        streamBridge.send("orderCreated-out-0", new OrderCreatedEvent(order.getId()));
    }

    // 监听:库存或后续步骤彻底失败后的回滚通知
    @Bean
    public Consumer<OrderRollbackEvent> handleOrderRollback() {
        return event -> {
            Order order = orderRepository.findById(event.getOrderId());
            order.setStatus(OrderStatus.CANCELLED);
            orderRepository.save(order);
            log.info("订单 {} 已取消", event.getOrderId());
        };
    }
}

3. 库存服务(中间步骤 & 补偿逻辑)

@Configuration
public class InventoryConfig {

    @Autowired
    private InventoryService inventoryService;
    @Autowired
    private StreamBridge streamBridge;

    // 正向流程:监听订单创建 -> 扣减库存
    @Bean
    public Consumer<OrderCreatedEvent> checkInventory() {
        return event -> {
            boolean success = inventoryService.deductStock(event.getOrderId());
            if (success) {
                // 成功:发布库存扣减成功事件,触发支付
                streamBridge.send("inventoryDeducted-out-0", new InventoryDeductedEvent(event.getOrderId()));
            } else {
                // 失败:发布库存扣减失败事件,触发订单服务回滚
                streamBridge.send("inventoryFailed-out-0", new OrderRollbackEvent(event.getOrderId()));
            }
        };
    }

    // 补偿流程:监听支付失败 -> 恢复库存
    @Bean
    public Consumer<PaymentFailedEvent> handleInventoryCompensate() {
        return event -> {
            log.info("收到支付失败事件,开始回滚库存:{}", event.getOrderId());
            inventoryService.restoreStock(event.getOrderId());
            
            // 继续向上传递回滚信号(触发订单取消)
            streamBridge.send("inventoryCompensated-out-0", new OrderRollbackEvent(event.getOrderId()));
        };
    }
}

4. 支付服务(失败触发点)

@Configuration
public class PaymentConfig {
    
    @Autowired
    private StreamBridge streamBridge;

    @Bean
    public Consumer<InventoryDeductedEvent> processPayment() {
        return event -> {
            try {
                // 模拟业务逻辑
                doPayment(event.getOrderId());
                streamBridge.send("paymentSuccess-out-0", new PaymentSuccessEvent(event.getOrderId()));
            } catch (Exception e) {
                // 异常:触发回滚流程
                log.error("支付失败,触发Saga回滚链");
                streamBridge.send("paymentFailed-out-0", new PaymentFailedEvent(event.getOrderId()));
            }
        };
    }
}

编排式 Saga 的进阶:解决“内存不可靠”问题

生产级编排式 Saga 的实现方案:

  1. 状态机引擎:使用数据库记录当前 Saga 的状态(Started, StepA_Done, StepB_Done, Failed, Compensating...)。
  2. 成熟框架
    • Seata (Saga Mode):阿里巴巴开源,通过 JSON 定义状态机,Seata Server 负责记录日志和驱动回滚,非常适合 Spring Cloud 生态。
    • Axon Framework:Java 领域成熟的 CQRS/Event Sourcing 框架,内置强大的 Saga 管理器。

简单的编排器伪代码(持久化版):

public void executeSaga(String orderId) {
    try {
        // 1. 创建订单
        sagaLogRepository.save(new SagaStep(orderId, "CREATE_ORDER", "STARTED"));
        orderService.create();
        
        // 2. 调用库存 (RPC/Feign)
        sagaLogRepository.save(new SagaStep(orderId, "DEDUCT_STOCK", "STARTED"));
        inventoryClient.deduct();
        
        // 3. 调用支付 (模拟抛出异常)
        sagaLogRepository.save(new SagaStep(orderId, "PAYMENT", "STARTED"));
        paymentClient.pay(); 
        
    } catch (Exception e) {
        // 读取日志,反向回滚
        List<SagaStep> steps = sagaLogRepository.findByOrderId(orderId);
        for (int i = steps.size() - 1; i >= 0; i--) {
            compensate(steps.get(i));
        }
    }
}

关键技术难点:可靠性与本地消息表

在 Saga 模式中,最大的挑战不是逻辑,而是通信的可靠性
问题: 数据库事务提交了,但 MQ 消息发送失败怎么办?或者 MQ 发送成功了,数据库回滚了怎么办?

解决方案:本地消息表(Transactional Outbox)

你需要优化你的数据库设计,将业务数据和事件日志放在同一个数据库事务中提交。

表设计 (saga_event_outbox)

字段名 类型 描述
id BIGINT 主键
transaction_id VARCHAR 关联业务ID(如订单号)
event_type VARCHAR 事件类型 (OrderCreated, InventoryCompensated)
payload JSON 事件内容
status VARCHAR NEW(待发送), SENT(已发送)
create_time DATETIME 创建时间

工作流程:

  1. 业务操作:在同一个 @Transactional 方法中,既插入业务表(Order表),也插入消息表(Outbox表)。这保证了原子性。
  2. 异步发送:有一个独立的定时任务(或监听 Binlog),轮询 status='NEW' 的记录,发送到 MQ,发送成功后更新为 SENT
  3. 幂等性:消费端必须实现幂等性(Idempotency),防止重复消息导致的数据错误(例如重复扣款)。

总结

Saga 模式通过“分而治之”和“知错能改(补偿)”的策略,完美解决了微服务长事务问题。

  • 选择建议

    • 如果业务流程简单(3个步骤以内),且流程相对固定,使用 协同式(Event-driven),实现简单。
    • 如果业务流程复杂、涉及服务多,或者流程经常变化,请务必使用 编排式(Orchestration),并推荐引入 Seata 等成熟框架来降低开发复杂度。
  • 核心准则

    • 幂等性:所有的正向操作和补偿操作都必须支持幂等。
    • 向前恢复 vs 向后恢复:Saga 不仅支持回滚(向后恢复),有些场景下(如网络抖动)也可以选择重试(向前恢复)。
    • 可观测性:一定要为整个 Saga 链路生成一个全局唯一的 Trace ID,否则在分布式日志中查错将是大海捞针。

posted on 2024-03-07 21:54  滚动的蛋  阅读(1839)  评论(0)    收藏  举报

导航