分布式事务完全指南

分布式事务完全指南


一、为什么需要分布式事务?

1.1 单体应用的事务

单体应用中,所有操作共享一个数据库连接,事务很简单:

java 复制代码
@Transactional
public void createOrder(OrderRequest request) {
    orderMapper.insert(order);          // 操作1:插入订单
    stockMapper.deduct(productId, qty); // 操作2:扣减库存
    accountMapper.deduct(userId, amount); // 操作3:扣减余额
    // 三个操作要么全部成功,要么全部回滚 → 一个 @Transactional 搞定
}

1.2 微服务架构的问题

当订单、库存、账户拆成三个独立服务后:

复制代码
┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│  订单服务     │   │  库存服务     │   │  账户服务     │
│  order_db    │   │  stock_db    │   │  account_db  │
└──────┬───────┘   └──────┬───────┘   └──────┬───────┘
       │                  │                  │
       ↓                  ↓                  ↓
    订单表              库存表              账户表

每个服务有自己的数据库,@Transactional 只能管本地事务,无法保证跨服务的一致性:

复制代码
1. 订单服务:创建订单 ✅
2. 库存服务:扣减库存 ✅
3. 账户服务:扣减余额 ❌(余额不足)

结果:订单创建了,库存扣了,但钱没扣 → 数据不一致!

分布式事务就是要解决这个跨服务数据一致性问题。


二、理论基础

2.1 ACID vs BASE

ACID(传统事务) BASE(分布式系统)
A Atomicity(原子性) Basically Available(基本可用)
C Consistency(一致性) Soft State(软状态)
I Isolation(隔离性) Eventually Consistent(最终一致性)
D Durability(持久性) ---
适用 单库事务 分布式系统
核心思想 强一致性,全有或全无 允许短暂不一致,最终达到一致

2.2 CAP 定理

分布式系统中,以下三者不可能同时满足:

字母 含义 说明
C Consistency(一致性) 所有节点同一时刻数据相同
A Availability(可用性) 每个请求都能收到响应
P Partition Tolerance(分区容错) 网络分区时系统仍能运行

微服务架构中网络分区不可避免(P 必须保证),所以只能在 C 和 A 之间取舍:

  • CP:保证一致性,牺牲可用性(如 2PC)
  • AP:保证可用性,牺牲强一致性(如最终一致性方案)

大多数互联网业务选择 AP + 最终一致性。

2.3 分布式事务方案谱系

复制代码
强一致性 ←──────────────────────────────────────→ 最终一致性
(性能差,复杂度低)                              (性能好,复杂度高)

  2PC/3PC    │     TCC     │    Saga    │  消息队列+补偿
  (XA协议)   │  (三阶段)   │ (编排/协调) │  (最终一致性)
             │             │            │
  几乎不用    │   金融核心   │ 电商/订单   │  大部分业务

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

三、方案一:本地消息表 + 消息队列(最终一致性)

3.1 核心思想

将"跨服务调用"转化为"本地事务 + 异步消息",通过消息的可靠投递保证最终一致性。

3.2 工作流程

复制代码
┌─── 订单服务 ──────────────────────────────────────┐
│                                                   │
│  @Transactional(本地事务)                        │
│  ┌─────────────────────────────────────────────┐ │
│  │ 1. INSERT INTO t_order (创建订单)             │ │
│  │ 2. INSERT INTO t_local_message (写入本地消息表)│ │
│  └─────────────────────────────────────────────┘ │
│                                                   │
│  定时任务:扫描本地消息表 → 发送到 MQ             │
│  确认发送成功 → 更新消息状态为"已发送"            │
│                                                   │
└───────────────────────────────────────────────────┘
                        │
                        ↓ MQ(Kafka/RocketMQ)
                        │
┌─── 库存服务 ──────────────────────────────────────┐
│                                                   │
│  消费者接收消息                                    │
│  @Transactional(本地事务)                        │
│  ┌─────────────────────────────────────────────┐ │
│  │ 1. 幂等检查(消息是否已处理过)               │ │
│  │ 2. UPDATE stock SET qty = qty - N(扣减库存) │ │
│  │ 3. INSERT INTO t_consume_log(记录消费日志)   │ │
│  └─────────────────────────────────────────────┘ │
│                                                   │
└───────────────────────────────────────────────────┘

3.3 完整代码示例

本地消息表(SQL)
sql 复制代码
CREATE TABLE t_local_message (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  message_id VARCHAR(64) NOT NULL UNIQUE COMMENT '消息唯一ID',
  topic VARCHAR(128) NOT NULL COMMENT '目标Topic',
  message_body TEXT NOT NULL COMMENT '消息内容(JSON)',
  status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待发送 1-已发送 2-发送失败',
  retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
  max_retry INT NOT NULL DEFAULT 5 COMMENT '最大重试次数',
  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_status_retry (status, retry_count)
) COMMENT '本地消息表';
消息实体
java 复制代码
@Data
@TableName("t_local_message")
public class LocalMessage {
  @TableId(type = IdType.AUTO)
  private Long id;
  private String messageId;
  private String topic;
  private String messageBody;
  private Integer status;       // 0-待发送 1-已发送 2-发送失败
  private Integer retryCount;
  private Integer maxRetry;
  private LocalDateTime createTime;
  private LocalDateTime updateTime;
}
订单服务 --- 创建订单(本地事务写入消息)
java 复制代码
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

  private final OrderMapper orderMapper;
  private final LocalMessageMapper localMessageMapper;

  @Override
  @Transactional  // 本地事务:订单和消息在同一个事务中
  public OrderResponse createOrder(CreateOrderRequest request) {
    // 1. 创建订单
    Order order = new Order();
    order.setOrderNo(generateOrderNo());
    order.setUserId(request.getUserId());
    order.setProductId(request.getProductId());
    order.setQuantity(request.getQuantity());
    order.setTotalAmount(request.getTotalAmount());
    order.setStatus("CREATED");
    orderMapper.insert(order);

    // 2. 写入本地消息表(与订单在同一个事务中,要么都成功要么都回滚)
    StockDeductMessage stockMsg = new StockDeductMessage();
    stockMsg.setOrderNo(order.getOrderNo());
    stockMsg.setProductId(request.getProductId());
    stockMsg.setQuantity(request.getQuantity());

    LocalMessage message = new LocalMessage();
    message.setMessageId(UUID.randomUUID().toString());
    message.setTopic("stock-deduct-topic");
    message.setMessageBody(JsonUtil.toJson(stockMsg));
    message.setStatus(0);  // 待发送
    message.setRetryCount(0);
    message.setMaxRetry(5);
    localMessageMapper.insert(message);

    log.info("创建订单成功, orderNo={}, 消息已写入本地表", order.getOrderNo());
    return toResponse(order);
  }
}
定时任务 --- 扫描并发送消息
java 复制代码
@Slf4j
@Component
public class MessageSendScheduler {

  private final LocalMessageMapper localMessageMapper;
  private final KafkaTemplate<String, String> kafkaTemplate;

  @Scheduled(fixedDelay = 5000)  // 每 5 秒扫描一次
  public void scanAndSendMessages() {
    // 1. 查询待发送的消息(status=0 且 retry_count < max_retry)
    List<LocalMessage> pendingMessages = 
        localMessageMapper.selectPending(100);

    for (LocalMessage message : pendingMessages) {
      try {
        // 2. 发送到 Kafka
        kafkaTemplate.send(message.getTopic(), message.getMessageId(), 
            message.getMessageBody()).get();  // 同步等待发送结果

        // 3. 发送成功,更新状态
        localMessageMapper.updateStatus(message.getId(), 1);
        log.info("消息发送成功, messageId={}", message.getMessageId());

      } catch (Exception e) {
        // 4. 发送失败,增加重试计数
        localMessageMapper.incrementRetryCount(message.getId());
        log.error("消息发送失败, messageId={}, retryCount={}", 
            message.getMessageId(), message.getRetryCount() + 1, e);

        // 超过最大重试次数,标记为失败(需要人工介入)
        if (message.getRetryCount() + 1 >= message.getMaxRetry()) {
          localMessageMapper.updateStatus(message.getId(), 2);
          log.error("消息重试次数用完, 需要人工处理, messageId={}", 
              message.getMessageId());
        }
      }
    }
  }
}
库存服务 --- 消费消息(幂等处理)
java 复制代码
@Slf4j
@Component
public class StockDeductConsumer {

  private final StockMapper stockMapper;
  private final ConsumeLogMapper consumeLogMapper;

  @KafkaListener(topics = "stock-deduct-topic")
  @Transactional
  public void onMessage(ConsumerRecord<String, String> record) {
    String messageId = record.key();
    log.info("收到库存扣减消息, messageId={}", messageId);

    // 1. 幂等检查(防止重复消费)
    if (consumeLogMapper.existsByMessageId(messageId)) {
      log.info("消息已处理过,跳过, messageId={}", messageId);
      return;
    }

    // 2. 解析消息
    StockDeductMessage msg = JsonUtil.fromJson(record.value(), StockDeductMessage.class);

    // 3. 扣减库存
    int affected = stockMapper.deduct(msg.getProductId(), msg.getQuantity());
    if (affected == 0) {
      log.error("库存扣减失败(库存不足), productId={}, qty={}", 
          msg.getProductId(), msg.getQuantity());
      // 库存不足时发送补偿消息(取消订单)
      sendCompensationMessage(msg.getOrderNo());
      return;
    }

    // 4. 记录消费日志(实现幂等)
    ConsumeLog consumeLog = new ConsumeLog();
    consumeLog.setMessageId(messageId);
    consumeLog.setConsumeTime(LocalDateTime.now());
    consumeLogMapper.insert(consumeLog);

    log.info("库存扣减成功, productId={}, qty={}", msg.getProductId(), msg.getQuantity());
  }

  private void sendCompensationMessage(String orderNo) {
    // 发送补偿消息,通知订单服务取消订单
    // ...
  }
}

3.4 优缺点

优点 缺点
实现简单,易理解 有延迟(不是实时一致)
高可用(MQ 解耦) 需要幂等处理
对业务侵入小 需要本地消息表 + 定时任务
适合大多数业务场景 补偿逻辑需要额外开发

四、方案二:TCC(Try-Confirm-Cancel)

4.1 核心思想

每个参与者提供三个接口:

  • Try:资源预留(冻结库存、冻结余额)
  • Confirm:确认提交(扣减冻结的库存/余额)
  • Cancel:取消回滚(释放冻结的资源)

4.2 工作流程

复制代码
事务协调器
    │
    ├── 阶段1:Try(资源预留)
    │   ├── 订单服务.try()  → 创建订单(状态:TRYING)
    │   ├── 库存服务.try()  → 冻结库存(stock_frozen += qty)
    │   └── 账户服务.try()  → 冻结余额(balance_frozen += amount)
    │
    │   全部 Try 成功?
    │   ├── 是 → 阶段2:Confirm
    │   │   ├── 订单服务.confirm()  → 订单状态改为 CONFIRMED
    │   │   ├── 库存服务.confirm()  → 扣减冻结库存(stock -= qty, stock_frozen -= qty)
    │   │   └── 账户服务.confirm()  → 扣减冻结余额
    │   │
    │   └── 否 → 阶段2:Cancel
    │       ├── 订单服务.cancel()  → 删除订单
    │       ├── 库存服务.cancel()  → 释放冻结库存(stock_frozen -= qty)
    │       └── 账户服务.cancel()  → 释放冻结余额

4.3 完整代码示例

库存表设计(增加冻结字段)
sql 复制代码
CREATE TABLE t_stock (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  product_id BIGINT NOT NULL,
  qty INT NOT NULL DEFAULT 0 COMMENT '可用库存',
  frozen_qty INT NOT NULL DEFAULT 0 COMMENT '冻结库存',
  UNIQUE KEY uk_product (product_id)
);

-- 冻结记录表(用于 Cancel 时回滚)
CREATE TABLE t_stock_freeze_log (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  tx_id VARCHAR(64) NOT NULL COMMENT '事务ID',
  product_id BIGINT NOT NULL,
  freeze_qty INT NOT NULL COMMENT '冻结数量',
  status TINYINT NOT NULL DEFAULT 0 COMMENT '0-已冻结 1-已确认 2-已取消',
  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_tx (tx_id)
);
TCC 接口定义
java 复制代码
/**
 * 库存 TCC 服务接口.
 */
public interface StockTccService {

  /**
   * Try:冻结库存.
   *
   * @param txId 全局事务ID
   * @param productId 商品ID
   * @param qty 数量
   * @return 是否成功
   */
  boolean tryFreeze(String txId, Long productId, Integer qty);

  /**
   * Confirm:确认扣减(将冻结转为实际扣减).
   *
   * @param txId 全局事务ID
   */
  void confirm(String txId);

  /**
   * Cancel:释放冻结的库存.
   *
   * @param txId 全局事务ID
   */
  void cancel(String txId);
}
TCC 实现
java 复制代码
@Slf4j
@Service
public class StockTccServiceImpl implements StockTccService {

  private final StockMapper stockMapper;
  private final StockFreezeLogMapper freezeLogMapper;

  @Override
  @Transactional
  public boolean tryFreeze(String txId, Long productId, Integer qty) {
    log.info("TCC-Try: 冻结库存, txId={}, productId={}, qty={}", txId, productId, qty);

    // 1. 幂等检查
    StockFreezeLog existLog = freezeLogMapper.selectByTxId(txId);
    if (existLog != null) {
      log.info("TCC-Try: 已处理过, txId={}", txId);
      return existLog.getStatus() == 0;
    }

    // 2. 扣减可用库存,增加冻结库存(CAS 防止超卖)
    int affected = stockMapper.freeze(productId, qty);
    // SQL: UPDATE t_stock SET qty = qty - #{qty}, frozen_qty = frozen_qty + #{qty}
    //      WHERE product_id = #{productId} AND qty >= #{qty}
    if (affected == 0) {
      log.warn("TCC-Try: 库存不足, productId={}, qty={}", productId, qty);
      return false;
    }

    // 3. 记录冻结日志
    StockFreezeLog freezeLog = new StockFreezeLog();
    freezeLog.setTxId(txId);
    freezeLog.setProductId(productId);
    freezeLog.setFreezeQty(qty);
    freezeLog.setStatus(0);  // 已冻结
    freezeLogMapper.insert(freezeLog);

    return true;
  }

  @Override
  @Transactional
  public void confirm(String txId) {
    log.info("TCC-Confirm: 确认扣减, txId={}", txId);

    // 1. 查询冻结记录
    StockFreezeLog freezeLog = freezeLogMapper.selectByTxId(txId);
    if (freezeLog == null || freezeLog.getStatus() != 0) {
      log.info("TCC-Confirm: 无需处理, txId={}, status={}", 
          txId, freezeLog != null ? freezeLog.getStatus() : "null");
      return;  // 幂等:已确认或已取消
    }

    // 2. 释放冻结数量(库存已在 Try 阶段扣减了)
    stockMapper.releaseFrozen(freezeLog.getProductId(), freezeLog.getFreezeQty());
    // SQL: UPDATE t_stock SET frozen_qty = frozen_qty - #{qty}
    //      WHERE product_id = #{productId}

    // 3. 更新状态
    freezeLogMapper.updateStatus(txId, 1);  // 已确认
  }

  @Override
  @Transactional
  public void cancel(String txId) {
    log.info("TCC-Cancel: 取消回滚, txId={}", txId);

    // 1. 查询冻结记录
    StockFreezeLog freezeLog = freezeLogMapper.selectByTxId(txId);
    if (freezeLog == null) {
      // 空回滚:Try 还没执行就 Cancel 了,直接记录防悬挂
      StockFreezeLog emptyLog = new StockFreezeLog();
      emptyLog.setTxId(txId);
      emptyLog.setProductId(0L);
      emptyLog.setFreezeQty(0);
      emptyLog.setStatus(2);  // 已取消
      freezeLogMapper.insert(emptyLog);
      return;
    }
    if (freezeLog.getStatus() != 0) {
      return;  // 幂等:已确认或已取消
    }

    // 2. 恢复库存
    stockMapper.unfreeze(freezeLog.getProductId(), freezeLog.getFreezeQty());
    // SQL: UPDATE t_stock SET qty = qty + #{qty}, frozen_qty = frozen_qty - #{qty}
    //      WHERE product_id = #{productId}

    // 3. 更新状态
    freezeLogMapper.updateStatus(txId, 2);  // 已取消
  }
}
事务协调器(编排 TCC)
java 复制代码
@Slf4j
@Service
public class OrderTccCoordinator {

  private final OrderTccService orderTccService;
  private final StockTccService stockTccService;
  private final AccountTccService accountTccService;

  public OrderResponse createOrder(CreateOrderRequest request) {
    String txId = UUID.randomUUID().toString();
    log.info("开始分布式事务, txId={}", txId);

    boolean orderTryOk = false;
    boolean stockTryOk = false;
    boolean accountTryOk = false;

    try {
      // ===== Phase 1: Try =====
      orderTryOk = orderTccService.tryCreate(txId, request);
      if (!orderTryOk) {
        throw new BusinessException("创建订单失败");
      }

      stockTryOk = stockTccService.tryFreeze(txId, request.getProductId(), request.getQuantity());
      if (!stockTryOk) {
        throw new BusinessException("库存不足");
      }

      accountTryOk = accountTccService.tryDeduct(txId, request.getUserId(), request.getTotalAmount());
      if (!accountTryOk) {
        throw new BusinessException("余额不足");
      }

      // ===== Phase 2: Confirm(全部 Try 成功) =====
      orderTccService.confirm(txId);
      stockTccService.confirm(txId);
      accountTccService.confirm(txId);

      log.info("分布式事务提交成功, txId={}", txId);
      return orderTccService.getByTxId(txId);

    } catch (Exception e) {
      // ===== Phase 2: Cancel(任一 Try 失败) =====
      log.error("分布式事务回滚, txId={}, error={}", txId, e.getMessage());
      if (orderTryOk) {
        orderTccService.cancel(txId);
      }
      if (stockTryOk) {
        stockTccService.cancel(txId);
      }
      if (accountTryOk) {
        accountTccService.cancel(txId);
      }
      throw e;
    }
  }
}

4.4 TCC 三大难题

问题 场景 解决方案
空回滚 Try 未执行,直接收到 Cancel Cancel 中检查是否有冻结记录,无则直接返回
幂等 Confirm/Cancel 被重复调用 通过状态字段判断是否已处理
悬挂 Cancel 先到,Try 后到 Cancel 时插入标记记录,Try 检查是否已 Cancel

4.5 优缺点

优点 缺点
强一致性(接近实时) 实现复杂(每个参与者要写 3 个接口)
不依赖消息队列 对业务侵入大(需要拆 Try/Confirm/Cancel)
性能较好(无锁等待) 需要处理空回滚、幂等、悬挂
适合短事务 数据库需要增加冻结字段

五、方案三:Saga 模式

5.1 核心思想

将一个长事务拆成多个本地短事务,每个事务都有对应的补偿操作。如果某一步失败,按逆序执行前面所有步骤的补偿操作。

复制代码
正向操作:T1 → T2 → T3 → T4
          ↓    ↓    ↓    ↓
补偿操作:C1   C2   C3   C4

如果 T3 失败:执行 C2 → C1(逆序补偿)

5.2 两种实现模式

编排式(Orchestration)--- 中心化协调

有一个协调器(Orchestrator)负责编排流程:

复制代码
                    ┌─────────────────┐
                    │  Saga 协调器     │
                    │ (Orchestrator)  │
                    └───────┬─────────┘
                            │
            ┌───────────────┼───────────────┐
            ↓               ↓               ↓
      ┌──────────┐   ┌──────────┐   ┌──────────┐
      │ 订单服务  │   │ 库存服务  │   │ 支付服务  │
      └──────────┘   └──────────┘   └──────────┘
协同式(Choreography)--- 事件驱动

无中心协调器,各服务通过事件自行协作:

复制代码
订单服务                库存服务                支付服务
    │                     │                     │
    │── 订单已创建事件 ──→│                     │
    │                     │── 库存已扣减事件 ──→│
    │                     │                     │── 支付已完成事件 ──→
    │←── 支付完成通知 ──────────────────────────│

5.3 编排式 Saga 代码示例

Saga 定义
java 复制代码
@Slf4j
@Service
public class CreateOrderSaga {

  private final OrderService orderService;
  private final StockService stockService;
  private final PaymentService paymentService;

  /**
   * 执行创建订单 Saga.
   */
  public OrderResponse execute(CreateOrderRequest request) {
    String sagaId = UUID.randomUUID().toString();
    log.info("Saga 开始, sagaId={}", sagaId);

    // 定义 Saga 步骤
    List<SagaStep> executedSteps = new ArrayList<>();

    try {
      // 步骤1:创建订单
      SagaStep step1 = new SagaStep(
          () -> orderService.create(sagaId, request),
          () -> orderService.cancel(sagaId)
      );
      step1.execute();
      executedSteps.add(step1);

      // 步骤2:扣减库存
      SagaStep step2 = new SagaStep(
          () -> stockService.deduct(sagaId, request.getProductId(), request.getQuantity()),
          () -> stockService.restore(sagaId, request.getProductId(), request.getQuantity())
      );
      step2.execute();
      executedSteps.add(step2);

      // 步骤3:扣减余额
      SagaStep step3 = new SagaStep(
          () -> paymentService.deduct(sagaId, request.getUserId(), request.getTotalAmount()),
          () -> paymentService.refund(sagaId, request.getUserId(), request.getTotalAmount())
      );
      step3.execute();
      executedSteps.add(step3);

      log.info("Saga 执行成功, sagaId={}", sagaId);
      return orderService.getBySagaId(sagaId);

    } catch (Exception e) {
      // 逆序补偿
      log.error("Saga 执行失败, 开始补偿, sagaId={}, error={}", sagaId, e.getMessage());
      compensate(executedSteps);
      throw new BusinessException("下单失败:" + e.getMessage());
    }
  }

  /**
   * 逆序执行补偿操作.
   */
  private void compensate(List<SagaStep> executedSteps) {
    for (int i = executedSteps.size() - 1; i >= 0; i--) {
      try {
        executedSteps.get(i).compensate();
      } catch (Exception ex) {
        // 补偿失败需要记录,后续人工处理
        log.error("补偿操作失败, stepIndex={}, error={}", i, ex.getMessage(), ex);
      }
    }
  }
}
SagaStep 抽象
java 复制代码
/**
 * Saga 步骤抽象.
 */
public class SagaStep {

  private final Runnable action;       // 正向操作
  private final Runnable compensation; // 补偿操作

  public SagaStep(Runnable action, Runnable compensation) {
    this.action = action;
    this.compensation = compensation;
  }

  public void execute() {
    action.run();
  }

  public void compensate() {
    compensation.run();
  }
}
各服务的正向 + 补偿接口
java 复制代码
/**
 * 库存服务(Saga 参与者).
 */
@Slf4j
@Service
public class StockServiceImpl implements StockService {

  private final StockMapper stockMapper;
  private final StockOperationLogMapper logMapper;

  /**
   * 正向操作:扣减库存.
   */
  @Override
  @Transactional
  public void deduct(String sagaId, Long productId, Integer qty) {
    log.info("Saga正向-扣减库存, sagaId={}, productId={}, qty={}", sagaId, productId, qty);

    // 幂等检查
    if (logMapper.existsBySagaId(sagaId)) {
      log.info("已处理过, sagaId={}", sagaId);
      return;
    }

    // 扣减库存
    int affected = stockMapper.deduct(productId, qty);
    if (affected == 0) {
      throw new BusinessException("库存不足");
    }

    // 记录操作日志(用于补偿)
    StockOperationLog opLog = new StockOperationLog();
    opLog.setSagaId(sagaId);
    opLog.setProductId(productId);
    opLog.setQty(qty);
    opLog.setOperation("DEDUCT");
    opLog.setStatus(0);  // 0-已执行
    logMapper.insert(opLog);
  }

  /**
   * 补偿操作:恢复库存.
   */
  @Override
  @Transactional
  public void restore(String sagaId, Long productId, Integer qty) {
    log.info("Saga补偿-恢复库存, sagaId={}, productId={}, qty={}", sagaId, productId, qty);

    // 幂等检查
    StockOperationLog opLog = logMapper.selectBySagaId(sagaId);
    if (opLog == null || opLog.getStatus() == 1) {
      log.info("无需补偿, sagaId={}", sagaId);
      return;  // 没扣过或已补偿
    }

    // 恢复库存
    stockMapper.restore(productId, qty);
    // SQL: UPDATE t_stock SET qty = qty + #{qty} WHERE product_id = #{productId}

    // 更新日志状态
    logMapper.updateStatus(sagaId, 1);  // 1-已补偿
  }
}

5.4 协同式 Saga 示例(事件驱动)

java 复制代码
// === 订单服务 ===
@Service
public class OrderServiceImpl {

  @Transactional
  public void createOrder(CreateOrderRequest request) {
    Order order = buildOrder(request);
    order.setStatus("PENDING");
    orderMapper.insert(order);

    // 发布事件,触发下一步
    eventPublisher.publish(new OrderCreatedEvent(order.getOrderNo(), 
        request.getProductId(), request.getQuantity()));
  }

  // 监听支付成功事件 → 订单完成
  @EventListener
  public void onPaymentCompleted(PaymentCompletedEvent event) {
    orderMapper.updateStatus(event.getOrderNo(), "COMPLETED");
  }

  // 监听库存扣减失败事件 → 取消订单
  @EventListener
  public void onStockDeductFailed(StockDeductFailedEvent event) {
    orderMapper.updateStatus(event.getOrderNo(), "CANCELLED");
  }
}

// === 库存服务 ===
@Component
public class StockEventListener {

  // 监听订单创建事件 → 扣减库存
  @KafkaListener(topics = "order-created")
  public void onOrderCreated(OrderCreatedEvent event) {
    try {
      stockService.deduct(event.getProductId(), event.getQuantity());
      // 成功 → 发布库存扣减成功事件
      eventPublisher.publish(new StockDeductedEvent(event.getOrderNo()));
    } catch (Exception e) {
      // 失败 → 发布库存扣减失败事件(触发订单取消)
      eventPublisher.publish(new StockDeductFailedEvent(event.getOrderNo(), e.getMessage()));
    }
  }
}

// === 支付服务 ===
@Component
public class PaymentEventListener {

  // 监听库存扣减成功事件 → 发起支付
  @KafkaListener(topics = "stock-deducted")
  public void onStockDeducted(StockDeductedEvent event) {
    try {
      paymentService.pay(event.getOrderNo());
      // 成功 → 发布支付完成事件
      eventPublisher.publish(new PaymentCompletedEvent(event.getOrderNo()));
    } catch (Exception e) {
      // 失败 → 发布支付失败事件(触发库存恢复 + 订单取消)
      eventPublisher.publish(new PaymentFailedEvent(event.getOrderNo()));
    }
  }
}

5.5 编排式 vs 协同式对比

维度 编排式(Orchestration) 协同式(Choreography)
控制方式 中心化协调器 无中心,事件驱动
流程可见性 高(流程集中在一个类) 低(分散在各服务的监听器中)
耦合度 协调器依赖所有参与者 参与者之间通过事件松耦合
复杂度 参与者少时简单 参与者多时容易形成事件风暴
适用场景 步骤少(3-5步)、流程清晰 参与者多、需要高度解耦
调试难度 容易(看协调器代码) 困难(需要追踪事件链)

5.6 优缺点

优点 缺点
无全局锁,性能好 一致性有延迟
每步都是本地事务 补偿逻辑需要额外开发
适合长流程 不支持隔离性(中间状态可见)
可以跨异构系统 调试和问题排查较难

六、方案四:Seata 框架

6.1 什么是 Seata?

Seata 是阿里巴巴开源的分布式事务框架,支持 AT、TCC、Saga、XA 四种事务模式,其中 AT 模式对业务侵入最小。

6.2 AT 模式工作原理

复制代码
┌──────────────────────────────────────────────┐
│                Seata Server (TC)              │
│            事务协调器(全局事务管理)           │
└──────────────────────┬───────────────────────┘
                       │
        ┌──────────────┼──────────────┐
        ↓              ↓              ↓
   ┌─────────┐   ┌─────────┐   ┌─────────┐
   │  TM     │   │  RM     │   │  RM     │
   │事务发起方│   │资源管理器│   │资源管理器│
   │(订单服务)│   │(库存服务)│   │(账户服务)│
   └─────────┘   └─────────┘   └─────────┘
角色 全称 职责
TC Transaction Coordinator 全局事务的协调器(Seata Server)
TM Transaction Manager 定义全局事务的范围(发起方)
RM Resource Manager 管理分支事务的资源(参与方)

AT 模式的神奇之处:只需加一个 @GlobalTransactional 注解,框架自动处理跨服务事务!

原理:

  1. 执行 SQL 前,记录"修改前的数据"(before image)
  2. 执行 SQL
  3. 执行 SQL 后,记录"修改后的数据"(after image)
  4. 全局事务提交 → 删除 undo log
  5. 全局事务回滚 → 用 before image 还原数据

6.3 代码示例

依赖
xml 复制代码
<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <version>2.0.0</version>
</dependency>
配置
yaml 复制代码
seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
  registry:
    type: nacos
    nacos:
      server-addr: nacos:8848
      namespace: seata
  config:
    type: nacos
    nacos:
      server-addr: nacos:8848
      namespace: seata
事务发起方(订单服务)
java 复制代码
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

  private final OrderMapper orderMapper;
  private final StockFeign stockFeign;      // Feign 调用库存服务
  private final AccountFeign accountFeign;  // Feign 调用账户服务

  @Override
  @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
  public OrderResponse createOrder(CreateOrderRequest request) {
    log.info("全局事务开始, xid={}", RootContext.getXID());

    // 1. 创建订单(本地事务)
    Order order = new Order();
    order.setUserId(request.getUserId());
    order.setProductId(request.getProductId());
    order.setQuantity(request.getQuantity());
    order.setTotalAmount(request.getTotalAmount());
    order.setStatus("CREATED");
    orderMapper.insert(order);

    // 2. 扣减库存(远程调用,Seata 自动传播 XID)
    Result<?> stockResult = stockFeign.deduct(request.getProductId(), request.getQuantity());
    if (!stockResult.isSuccess()) {
      throw new BusinessException("库存扣减失败");
    }

    // 3. 扣减余额(远程调用)
    Result<?> accountResult = accountFeign.deduct(request.getUserId(), request.getTotalAmount());
    if (!accountResult.isSuccess()) {
      throw new BusinessException("余额扣减失败");
      // Seata 会自动回滚步骤 1 和步骤 2
    }

    // 全部成功,Seata 自动提交全局事务
    return toResponse(order);
  }
}
事务参与方(库存服务)--- 无需特殊注解
java 复制代码
@Slf4j
@Service
public class StockServiceImpl implements StockService {

  private final StockMapper stockMapper;

  @Override
  @Transactional  // 只需要本地事务注解,Seata 自动管理分支事务
  public void deduct(Long productId, Integer qty) {
    log.info("扣减库存, xid={}, productId={}, qty={}", 
        RootContext.getXID(), productId, qty);

    int affected = stockMapper.deduct(productId, qty);
    if (affected == 0) {
      throw new BusinessException("库存不足");
    }
  }
}
每个参与服务的数据库需要添加 undo_log 表
sql 复制代码
CREATE TABLE undo_log (
  branch_id BIGINT NOT NULL COMMENT '分支事务ID',
  xid VARCHAR(128) NOT NULL COMMENT '全局事务ID',
  context VARCHAR(128) NOT NULL COMMENT '上下文',
  rollback_info LONGBLOB NOT NULL COMMENT '回滚信息',
  log_status INT NOT NULL COMMENT '状态',
  log_created DATETIME NOT NULL COMMENT '创建时间',
  log_modified DATETIME NOT NULL COMMENT '修改时间',
  UNIQUE KEY ux_undo_log (xid, branch_id)
) COMMENT 'Seata AT 模式 undo log';

6.4 Seata 四种模式对比

模式 侵入性 一致性 性能 适用场景
AT 极低(加注解) 最终一致 中 大多数 CRUD 业务
TCC 高(写3个接口) 强一致 高 金融核心
Saga 中(写补偿) 最终一致 高 长流程
XA 极低 强一致 低 短事务、对性能不敏感

6.5 优缺点

优点 缺点
AT 模式几乎零侵入 需要部署 Seata Server
支持多种模式切换 AT 模式有全局锁,高并发下性能下降
社区活跃,文档丰富 每个库要加 undo_log 表
与 Spring Cloud 集成好 不支持非关系型数据库
阿里大规模验证 AT 模式不支持批量 SQL 和复杂 SQL

七、方案对比总结

维度 消息队列+补偿 TCC Saga Seata AT
一致性 最终一致 强一致(接近) 最终一致 最终一致
实时性 有延迟(秒级) 实时 有延迟 接近实时
性能 高 高 高 中
复杂度 低 高 中 低
业务侵入 低 高 中 极低
隔离性 无 有(冻结) 无 有(全局锁)
适用场景 对延迟不敏感的通用业务 金融、资金 长流程、跨异构 通用 CRUD
基础设施 消息队列 无(自研) 无/消息队列 Seata Server

八、选型建议

复制代码
                你的业务需要强一致性吗?
                       │
            ┌──── 是 ──┴── 否 ────┐
            ↓                     ↓
      对性能敏感吗?          有延迟容忍度吗?
       │        │              │        │
    是 ↓     否 ↓          是 ↓     否 ↓
   TCC     Seata AT     消息队列   Seata AT
                          +补偿

具体建议

场景 推荐方案
电商下单(扣库存+扣款) Seata AT 或 消息队列+补偿
银行转账(A 扣 B 加) TCC
订单→物流→通知(长流程) Saga
积分发放、优惠券发放 消息队列+补偿
简单 CRUD 微服务 Seata AT
跨公司/跨系统对接 消息队列+补偿 或 Saga

九、通用最佳实践

9.1 幂等性设计

无论哪种方案,所有参与者都必须支持幂等:

java 复制代码
// 通过唯一业务ID实现幂等
@Transactional
public void deductStock(String bizId, Long productId, Integer qty) {
    // 检查是否已处理
    if (operationLogMapper.exists(bizId)) {
        return;  // 已处理,直接返回
    }
    
    // 执行业务
    stockMapper.deduct(productId, qty);
    
    // 记录操作(唯一索引防并发)
    operationLogMapper.insert(new OperationLog(bizId, "DEDUCT"));
}

9.2 超时处理

java 复制代码
// 定时任务:处理超时未完成的事务
@Scheduled(fixedDelay = 60000)
public void handleTimeoutTransactions() {
    // 查询超过 5 分钟未完成的事务
    List<TransactionLog> timeoutList = 
        txLogMapper.selectTimeout(Duration.ofMinutes(5));
    
    for (TransactionLog tx : timeoutList) {
        log.warn("事务超时, 执行补偿, txId={}", tx.getTxId());
        compensate(tx);
    }
}

9.3 对账机制

生产环境需要定期对账,发现不一致数据:

java 复制代码
// 每天凌晨对账
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReconciliation() {
    // 比对订单表和库存扣减记录
    List<String> orderNos = orderMapper.selectTodayOrderNos();
    List<String> stockDeductNos = stockLogMapper.selectTodayDeductNos();
    
    // 找出差异
    Set<String> missing = new HashSet<>(orderNos);
    missing.removeAll(stockDeductNos);
    
    if (!missing.isEmpty()) {
        log.error("对账发现不一致, 需要人工处理, orderNos={}", missing);
        alertService.sendAlert("对账异常", missing.toString());
    }
}

9.4 兜底方案

无论用什么框架,都需要人工介入的兜底:

java 复制代码
// 补偿失败的记录,发送告警
if (compensateRetryCount >= MAX_RETRY) {
    // 写入异常事务表
    failedTxMapper.insert(new FailedTransaction(txId, "COMPENSATE_FAILED"));
    
    // 发送告警(钉钉/企微/短信)
    alertService.send("分布式事务补偿失败,需要人工处理", txId);
}

十、常见问题 FAQ

Q1: 最简单的方案是什么?

对大多数业务来说:本地消息表 + 消息队列 + 幂等消费。不需要引入额外框架,只需要一张表 + 一个定时任务。

Q2: Seata 适合高并发场景吗?

AT 模式有全局锁,热点数据高并发下会有性能瓶颈。如果 TPS 超过千级且数据有热点(如同一商品秒杀),建议用 TCC 或消息队列方案。

Q3: 如果补偿也失败了怎么办?

  1. 先重试(有限次数)
  2. 记录到异常事务表
  3. 发送告警通知
  4. 人工介入处理

这是所有分布式事务方案的最终兜底------没有 100% 自动化解决的银弹。

Q4: 如何处理"中间状态可见"问题?

Saga 和消息队列方案中,用户可能看到"订单已创建但库存未扣"的中间状态。解决方式:

  • 订单增加"处理中"状态,前端展示"订单处理中,请稍后"
  • 使用消息队列确保快速处理(延迟控制在毫秒级)
  • 查询时做状态合并(订单+库存+支付状态综合判断)

Q5: 跨数据库的本地事务怎么做?

同一服务连多个数据库时,可以用 Spring 的 JtaTransactionManager + Atomikos:

xml 复制代码
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

但性能较差,尽量避免。更好的做法是按数据库拆分服务边界。

Q6: 消息丢了怎么办?

三重保障:

  1. 生产端:本地消息表保证一定能发出去(定时重试)
  2. MQ 端:Kafka/RocketMQ 的 acks=all 保证消息持久化
  3. 消费端:手动 ACK + 失败重试 + 死信队列

Q7: 哪些场景不需要分布式事务?

  • 操作可以重试的(发短信、发邮件)→ 最多多发一次,可接受
  • 操作可以异步最终一致的(更新搜索索引、同步数据)→ 消息队列即可
  • 单库操作(同一个服务的多个表)→ 本地 @Transactional 足够

十一、总结

方案 一句话总结 何时选择
本地消息表 本地事务写消息 + 定时发送 + 幂等消费 90% 的场景首选
TCC Try 冻结 → Confirm 确认 → Cancel 释放 金融级强一致性
Saga 正向执行 + 逆序补偿 长流程、多步骤
Seata AT 一个注解搞定,框架自动回滚 快速开发、中小规模

核心原则:能用最终一致性就不追求强一致性,能用简单方案就不引入复杂框架。 分布式事务的复杂度是实实在在的成本,只在真正需要的地方使用。