分布式事务完全指南

分布式事务完全指南


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

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 一个注解搞定,框架自动回滚 快速开发、中小规模

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