侧边栏壁纸
博主头像
程序员进阶之路

技术之道,不可浅尝辄止;架构之路,须当支持以恒!

  • 累计撰写 18 篇文章
  • 累计创建 3 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

分布式事务深度解析:从 XA、TCC、Saga 到 Seata 与事务消息实战

云端行笔
2026-05-20 / 0 评论 / 0 点赞 / 8 阅读 / 0 字

摘要:微服务拆分后,一次业务操作往往需要同时修改订单、库存、支付、积分等多个服务的数据。单机数据库事务无法跨越服务边界,分布式事务因此成为系统设计中绕不开的问题。本文从下单扣库存场景出发,系统讲解分布式事务的核心概念、CAP 与 BASE、XA 和 2PC、TCC、Saga、Transactional Outbox、本地消息表、RocketMQ 事务消息、最大努力通知,以及 Apache Seata 的 AT、XA、TCC、Saga 四种模式。文章同时给出方案选型、幂等设计、补偿机制、事务防悬挂和生产排查方法。

写在前面:数据库事务失效的那一刻

单体应用里,创建订单和扣减库存可能只是一个普通方法:

  @Transactional
  public Long createOrder(CreateOrderCommand command) {
      stockRepository.deduct(command.skuId(), command.quantity());
      Order order = orderRepository.save(Order.create(command));
      return order.getId();
  }

订单表和库存表都在同一个数据库中,Spring 开启一个本地事务,两条 SQL 要么一起提交,要么一起回滚。只要数据库支持 ACID,这件事没有太多悬念。

业务增长以后,订单和库存通常会拆成独立服务:Order Service和Stock Service。

麻烦从这里开始:

  • 如果订单创建成功,库存扣减失败,系统里会出现一个无法履约的订单。

  • 如果库存扣减成功,但 Order Service 在响应前超时,调用方不知道这笔订单究竟有没有成功。

  • 如果库存服务已经扣减,订单服务随后崩溃,库存应该回滚,还是等待订单服务恢复后继续完成?

这些问题无法再靠一个 @Transactional 解决。因为数据库本地事务只能管理自己的连接,不能自动控制另一个服务、另一个数据库或一个远程支付系统。

分布式事务要解决的,就是跨服务、跨数据库、跨资源的一致性问题。

但先说一个重要结论:分布式事务不等于“想办法让所有系统像单库事务一样同时提交”。在微服务里,很多业务更适合接受短暂不一致,通过状态机、消息、补偿和对账最终收敛。

真正的难点不是背方案名称,而是判断业务究竟需要哪种一致性。

一、什么是分布式事务

事务是一组操作构成的执行单元。对外看,这些操作应该像一个整体:

  • 要么全部成功。

  • 要么全部失败。

  • 或者在无法立即完成时,进入可恢复的中间状态,最终收敛到成功或失败。

本地事务通常发生在单个数据库内部:

  BEGIN;
  
  UPDATE stock
  SET available = available - 1
  WHERE sku_id = 1001
    AND available >= 1;
  
  INSERT INTO orders(order_id, sku_id, status)
  VALUES (90001, 1001, 'CREATED');
  
  COMMIT;

分布式事务则跨越多个独立资源:

它们可能使用不同数据库,运行在不同机器上,甚至由不同公司维护。网络随时可能超时、重试、断开。某个请求没有收到响应,不代表对方没有执行;对方返回成功,也不代表后续动作一定完成。

因此,分布式事务比本地事务多了几个现实约束:

  • 网络不是可靠内存调用。

  • 超时不等于失败。

  • 重试可能造成重复执行。

  • 服务可能在任何阶段重启。

  • 消息可能重复、延迟或乱序。

  • 补偿操作本身也可能失败。

  • 外部系统未必提供回滚接口。

设计分布式事务,本质上是在设计这些不确定性如何被系统吸收。

二、先厘清几个常被混在一起的概念

网上讲分布式事务,经常一上来就是 CAP、BASE、2PC、TCC、Seata。概念很多,但层次容易混。可以先按下面的方式拆开:

Seata 不是 TCC 的同义词,RocketMQ 事务消息也不是通用强一致事务。它们是实现工具,各自只解决一部分问题。

三、CAP、BASE 与分布式事务:别套公式

1. CAP 真正讨论的是什么

CAP 包含三个概念:

  • Consistency:一致性。在 CAP 语境中,通常指每次读取都能拿到最新写入或返回错误。

  • Availability:可用性。每个请求都能在有限时间内得到非错误响应,但数据不一定最新。

  • Partition Tolerance:分区容错。即使节点间通信中断或消息长时间延迟,系统仍能继续运行。

常见说法是“CAP 三选二”。这句话方便记忆,但不够准确。更实用的理解是:当网络分区真的发生时,系统必须在一致性和可用性之间做取舍。是拒绝部分请求,守住一致性;还是继续响应,接受短暂不一致。

2. CAP 的 C 不是 ACID 的 C

这两个 Consistency 经常被混为一谈。二者虽然名字相同,但并不是一个概念。

  • ACID 里的 Consistency 更关注事务执行前后是否满足业务约束,例如库存不能为负数,订单状态不能从 CANCELLED 跳到 PAID

  • CAP 里的 Consistency 更接近分布式读写的一致视图,例如任意节点读取时能否看到最新写入。

3. 刚性事务与柔性事务

工程上经常把方案分成两类:

刚性事务追求接近本地 ACID 的行为:

  • 事务边界明确。

  • 提交前资源通常会被锁定或保留。

  • 任一参与者失败,整体回滚。

  • 适合一致性要求高、事务时间短的场景。

典型方案:

  • XA。

  • 2PC。

  • Seata XA。

  • 某些数据库原生分布式事务。

柔性事务接受中间状态:

  • 不要求所有参与者瞬间一致。

  • 允许异步执行。

  • 通过补偿、重试和对账达到最终一致

  • 更适合微服务和长业务流程。

典型方案:

  • TCC。

  • Saga。

  • Transactional Outbox。

  • 本地消息表。

  • RocketMQ 事务消息。

  • 最大努力通知。

需要注意:把刚性事务简单等同于 CP,把柔性事务简单等同于 AP,只能作为粗略直觉,不能当成严格推导。事务协议、复制一致性、服务可用性和网络分区下的行为,是不同维度的问题。

4. BASE 是什么

BASE 常用来描述最终一致系统的设计取向:

  • Basically Available:基本可用。

  • Soft State:软状态,允许存在中间状态。

  • Eventually Consistent:最终一致。

例如订单创建后先显示 PROCESSING,库存扣减和积分发放异步完成。短时间内各服务状态未完全同步,但系统能通过重试或补偿最终收敛。

这不是“数据错了也没关系”,而是“系统明确知道哪些中间状态允许存在,以及怎样恢复”。

四、用一张图建立分布式事务知识体系

可以先用这张结构图定位各类方案:

没有一种方案适合所有业务。下单、支付、退款、优惠券、积分、物流,可能分别使用不同策略。

五、XA 协议:数据库层面的分布式事务基础

XA 是 X/Open 提出的分布式事务处理规范。它定义了事务管理器和资源管理器如何协作。

两个核心角色:

  • Transaction Manager,简称 TM:事务管理器。负责开启全局事务,协调提交或回滚。

  • Resource Manager,简称 RM:资源管理器。通常是数据库、消息系统或其他事务资源。

调用关系可以理解为:

订单数据库和库存数据库都实现 XA 能力以后,TM 可以统一协调两个资源。

XA 是规范,不是 Java 专属技术。主流关系型数据库通常都提供不同程度的 XA 支持。

Java 里的 JTA 和 JTS

在 Java 生态里,经常会看到 JTA 和 JTS。

  • JTA:Java Transaction API,后来演进为 Jakarta Transactions API。它提供应用事务划分、事务管理器接口,以及对 X/Open XA 的 Java 映射。

  • JTS:Java Transaction Service。它是事务管理器实现相关规范。

可以粗略理解为:

  XA:跨资源事务协议
  Jakarta Transactions / JTA:Java 应用操作事务的标准接口
  JTS:Java 事务服务实现规范

业务开发里,不一定直接写 XA 接口,但理解它能帮助你判断框架底层在做什么。

六、两阶段提交 2PC:先准备,再决定

2PC,全称 Two-Phase Commit,是经典的两阶段提交协议。

参与角色:

  • Coordinator:协调者。

  • Participant:参与者。

整个过程分两步。

第一阶段:Prepare

协调者询问所有参与者:能不能提交?

  Coordinator
    -> order_db: PREPARE
    -> stock_db: PREPARE

参与者执行本地事务,但暂不真正提交。它们锁定必要资源,并向协调者回复:

  YES:已经准备好,可以提交
  NO:无法提交,需要回滚

第二阶段:Commit 或 Rollback

如果所有参与者都返回 YES:

  Coordinator
    -> order_db: COMMIT
    -> stock_db: COMMIT

只要有一个参与者返回 NO 或超时:

  Coordinator
    -> order_db: ROLLBACK
    -> stock_db: ROLLBACK

2PC 的优点

  • 语义清晰。

  • 能协调多个支持事务协议的资源。

  • 业务代码侵入相对小。

  • 适合事务时间短、参与者可控、一致性要求高的场景。

2PC 的问题

1. 阻塞

参与者 prepare 成功后,需要等待协调者最终指令。等待期间资源可能被锁住。如果协调者故障,参与者无法随意决定提交还是回滚,只能等待恢复或超时处理。

2. 协调者故障

协调者是关键角色。它需要高可用和持久化事务状态,否则故障恢复时会很麻烦。

3. 网络异常带来的不确定状态

协调者发出 commit 后,如果部分参与者收到、部分参与者没收到,系统会进入复杂恢复流程。严格来说,成熟实现会依靠事务日志和恢复机制处理,而不是简单放任不一致。但恢复成本、锁持有时间和系统复杂度都是真实存在的。

4. 吞吐受限

参与者越多,网络往返越多;事务越长,锁持有越久。它不适合跨多个微服务执行长时间业务流程。

一个常见误区

2PC 不是只能用于“两个数据库”。它可以协调多个实现协议的事务资源。真正的限制是:参与资源必须提供对应事务能力,并且业务能够接受同步协调、锁资源和故障恢复成本。

对于第三方支付、物流公司接口、短信服务这类资源,通常没有办法让它们加入 XA 事务。

七、三阶段提交 3PC:理论上减轻阻塞,生产中少见

3PC 在 2PC 的基础上增加一个阶段:

  CanCommit
    -> PreCommit
    -> DoCommit

大致思路是:在真正提交前,让参与者状态更明确,并引入超时机制,减少协调者故障后无限等待的问题。但它依赖较强的网络时延假设。在真实系统中,网络延迟、分区、节点故障很难准确区分。超时以后自行提交,也可能引入新的不一致。因此,3PC 更适合理解分布式协议演进,不是微服务架构里的常规落地方案。

生产系统通常会通过复制状态机、共识协议、事务日志、补偿机制或工作流引擎处理故障恢复,而不是直接实现一套 3PC。

八、TCC:把事务控制权交给业务

TCC 是 Try、Confirm、Cancel 的缩写。它和 2PC 有相似的两阶段思想,但控制层次不同:

  • 2PC 更偏资源层,常见于数据库。

  • TCC 发生在业务层,需要开发者实现预留、确认和取消逻辑。

还是以下单扣库存为例。

1. Try:检查并预留资源

库存服务先冻结库存,而不是直接扣减:

    UPDATE stock
    SET available = available - 1,
        frozen = frozen + 1
    WHERE sku_id = 1001
      AND available >= 1;

支付服务可以冻结余额:

  UPDATE account
  SET available_balance = available_balance - 100,
      frozen_balance = frozen_balance + 100
  WHERE user_id = 2001
    AND available_balance >= 100;

2. Confirm:确认执行

所有 Try 成功后,确认扣减冻结资源:

  UPDATE stock
  SET frozen = frozen - 1
  WHERE sku_id = 1001
    AND frozen >= 1;

3. Cancel:取消预留

任一参与者 Try 失败,释放冻结库存:

  UPDATE stock
  SET available = available + 1,
      frozen = frozen - 1
  WHERE sku_id = 1001
    AND frozen >= 1;

TCC 的优点

  • 锁粒度可以由业务控制。

  • 不依赖数据库 XA。

  • 跨数据库、跨服务都能落地。

  • 资源预留后,本地数据库锁可以很快释放。

  • 适合核心交易链路。

TCC 的缺点

  • 侵入业务。

  • 每个动作都要设计 Try、Confirm、Cancel。

  • 需要处理幂等、空回滚、悬挂。

  • 研发和测试成本高。

TCC 很强,但不是“给接口加三个方法”这么简单。它要求业务本身具备可冻结、可确认、可释放的资源模型。

TCC 最容易踩的三个坑

1. 空回滚

场景:

  协调器调用 Try
    -> 网络异常,请求实际没有到达库存服务
  协调器判断失败
    -> 调用 Cancel

库存服务从未执行 Try,却收到了 Cancel。这就是空回滚。Cancel 必须允许这种情况,并安全返回。

常见做法是维护分支事务记录:

  CREATE TABLE tcc_branch_record (
      xid VARCHAR(128) NOT NULL,
      branch_id VARCHAR(128) NOT NULL,
      business_key VARCHAR(128) NOT NULL,
      status VARCHAR(32) NOT NULL,
      created_at DATETIME NOT NULL,
      updated_at DATETIME NOT NULL,
      PRIMARY KEY (xid, branch_id)
  );

Cancel 时先查 Try 是否存在:

  没有 Try 记录
    -> 记录空回滚状态
    -> 返回成功

2. 幂等

Confirm 和 Cancel 都可能重试:

Confirm 已执行
  -> 响应超时
  -> 协调器再次调用 Confirm

如果 Confirm 重复扣库存,系统就错了。

解决方法:

  • 每个分支使用唯一 xid + branch_id

  • 状态流转做条件更新。

  • 重复请求读取当前状态后直接返回成功。

例如:

  UPDATE tcc_branch_record
  SET status = 'CONFIRMED'
  WHERE xid = ?
    AND branch_id = ?
    AND status = 'TRIED';

只有 TRIED 才能进入 CONFIRMED

3. 悬挂

场景:

  Try 请求发送后网络阻塞
  协调器超时,先调用 Cancel
  Cancel 执行完成
  延迟的 Try 请求随后到达
  Try 又冻结了资源

此时已经没有后续 Confirm 或 Cancel,资源被永久挂住。Try 执行前必须检查:

  • 当前分支是否已经 Cancel。

  • 当前分支是否已经 Confirm。

  • 是否存在空回滚标记。

如果二阶段已经发生,Try 不能再执行。这就是事务防悬挂。

九、Saga:更适合长流程的补偿事务

Saga 最早来自 Hector Garcia-Molina 和 Kenneth Salem 在 1987 年发表的论文。

它把一个长事务拆成多个本地事务:

每个本地事务都有对应补偿动作:

如果执行到 T3 失败:

Saga 的特点是:每个步骤完成后直接提交本地事务,不长时间锁资源。失败后通过业务补偿回到可接受状态。

一个订单 Saga

假设下单流程包含:

  订单服务
  支付服务
  库存服务
  物流服务

正向流程:

  创建订单
    -> 扣款
    -> 扣减库存
    -> 创建物流单
    -> 订单完成

补偿流程:

  物流创建失败
    -> 恢复库存
    -> 发起退款
    -> 标记订单失败

Saga 不追求数据库层面的回滚,而是执行业务意义上的反向动作。

退款就是一个很典型的补偿。钱扣掉以后,不可能让第三方支付系统“回滚事务日志”,只能再发起一笔退款交易。

Saga 的两种实现方式

1. Choreography:事件驱动

Choreography 可以理解为舞蹈编排。没有中央协调器,各服务监听事件并做出响应。

  Order Service
    -> 发布 ORDER_CREATED
  
  Payment Service
    -> 监听 ORDER_CREATED
    -> 完成支付
    -> 发布 PAYMENT_SUCCEEDED
  
  Stock Service
    -> 监听 PAYMENT_SUCCEEDED
    -> 扣减库存
    -> 发布 STOCK_DEDUCTED
  
  Delivery Service
    -> 监听 STOCK_DEDUCTED
    -> 创建物流单
    -> 发布 DELIVERY_CREATED

优点:

  • 服务解耦。

  • 吞吐较高。

  • 适合步骤少、链路清晰的流程。

缺点:

  • 参与者多以后,很难看清完整流程。

  • 容易出现事件环路。

  • 异常补偿分散在多个服务里。

  • 排查问题需要追踪很多消息。

如果只有两三个步骤,事件驱动很轻巧。如果十几个服务互相订阅,系统会慢慢变成一张很难解释的图。

2. Orchestration:协调器编排

Orchestration 可以理解为乐队指挥。一个 Saga Orchestrator 明确告诉各服务下一步做什么。

  Order Saga Orchestrator
    -> 创建订单
    -> 命令 Payment Service 扣款
    -> 命令 Stock Service 扣库存
    -> 命令 Delivery Service 创建物流单
    -> 命令 Order Service 标记完成

出现异常:

  Delivery Service 创建失败
    -> Orchestrator 命令 Stock Service 恢复库存
    -> Orchestrator 命令 Payment Service 退款
    -> Orchestrator 命令 Order Service 标记失败

优点:

  • 流程集中,容易理解。

  • 状态机清晰。

  • 补偿顺序明确。

  • 测试和运营干预方便。

缺点:

  • 需要维护协调器。

  • 协调器容易承载过多业务逻辑。

  • 要认真设计状态持久化和故障恢复。

长流程、强运营、需要人工介入的业务,更适合编排式 Saga。Saga 的关键不是“回滚”,而是业务恢复。

十、Transactional Outbox:消息和业务数据一起落库

微服务里一个很常见的问题是:

  更新数据库
  发送 MQ 消息

这两步怎么保证一致?

先更新数据库,再发消息:

  数据库提交成功
    -> 服务崩溃
    -> 消息没发出去

先发消息,再更新数据库:

  消息发出
    -> 数据库回滚
    -> 下游消费了一个不存在的业务变化

Transactional Outbox 的解决思路是:业务数据和待发送事件在同一个本地事务里写入数据库。

  BEGIN;
  
  INSERT INTO orders(order_id, user_id, status)
  VALUES (90001, 2001, 'CREATED');
  
  INSERT INTO outbox_event(
      event_id,
      aggregate_type,
      aggregate_id,
      event_type,
      payload,
      status,
      created_at
  )
  VALUES (
      'evt-001',
      'ORDER',
      '90001',
      'ORDER_CREATED',
      '{"orderId":90001,"userId":2001}',
      'NEW',
      NOW()
  );
  
  COMMIT;

然后由独立发布器把 Outbox 事件发送到 MQ:

  Outbox Relay
    -> 扫描 NEW 事件
    -> 发送 MQ
    -> 标记 SENT

或者直接订阅数据库 binlog,把 Outbox 事件推到消息系统。

Outbox 的优点

  • 不需要 XA。

  • 业务数据和事件在一个本地事务里提交。

  • 实现直观。

  • 能用于事件驱动和 Saga。

Outbox 的限制

  • 发布器可能重复发送。

  • Outbox 表需要清理和归档。

  • 轮询方案有延迟和数据库压力。

  • binlog 方案要维护 CDC 链路。

最重要的一点:Outbox 通常只能提供至少一次投递,因此消费者必须幂等。

本地消息表:Outbox 的常见落地方式

本地消息表和 Transactional Outbox 基本属于同一类思想:业务表和消息表在同一个数据库事务中写入。

一个简化表结构:

CREATE TABLE local_message (
    message_id VARCHAR(64) PRIMARY KEY,
    topic VARCHAR(128) NOT NULL,
    business_key VARCHAR(128) NOT NULL,
    payload JSON NOT NULL,
    status VARCHAR(32) NOT NULL,
    retry_count INT NOT NULL DEFAULT 0,
    next_retry_at DATETIME NOT NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY uk_topic_business_key(topic, business_key)
);

Java 写法:

@Transactional
public Long createOrder(CreateOrderCommand command) {
    Order order = orderRepository.save(Order.create(command));

    localMessageRepository.save(
        LocalMessage.newEvent(
            UUID.randomUUID().toString(),
            "ORDER_CREATED",
            String.valueOf(order.getId()),
            JsonUtils.toJson(OrderCreatedEvent.from(order))
        )
    );

    return order.getId();
}

后台任务发送:

public void publishPendingMessages() {
    List<LocalMessage> messages = localMessageRepository.findReadyToSend(100);

    for (LocalMessage message : messages) {
        try {
            messagePublisher.publish(message);
            localMessageRepository.markSent(message.getMessageId());
        } catch (Exception e) {
            localMessageRepository.scheduleRetry(message.getMessageId());
        }
    }
}

注意:即使 MQ 发送成功,markSent 也可能失败。下一次扫描时消息会再次发送。所以消费者必须幂等。

消费者幂等表

  CREATE TABLE consumed_message (
      consumer_group VARCHAR(128) NOT NULL,
      message_id VARCHAR(64) NOT NULL,
      consumed_at DATETIME NOT NULL,
      PRIMARY KEY (consumer_group, message_id)
  );

消费时把“记录已消费”和“修改业务数据”放在一个本地事务里:

  @Transactional
  public void consume(OrderCreatedEvent event) {
      if (consumedMessageRepository.exists("stock-consumer", event.messageId())) {
          return;
      }
  
      stockRepository.deduct(event.skuId(), event.quantity());
      consumedMessageRepository.save("stock-consumer", event.messageId());
  }

十一、RocketMQ 事务消息:把半消息和回查交给 MQ

本地消息表需要业务自己维护消息表、扫描任务、重试状态。RocketMQ 事务消息把其中一部分能力收进了 MQ。

核心流程:

流程说明:

如果 Producer 执行完本地事务,但二次确认因为网络异常丢失:

RocketMQ 官方文档明确指出,事务消息用于保证消息生产和本地事务之间的最终一致性。它不等于“消息消费方一定执行成功”,下游仍然需要消费重试和幂等。

RocketMQ 事务消息适合什么场景

  • 本地核心事务成功后,可靠触发下游。

  • 下游可以异步处理。

  • 业务能实现本地事务状态回查。

  • 可以接受最终一致。

典型场景:

  • 支付成功后发放积分。

  • 订单支付后清理购物车。

  • 订单创建后触发异步库存处理。

  • 状态变更后通知多个下游系统。

事务消息不解决什么

  • 不保证消费者只消费一次。

  • 不替代消费者幂等。

  • 不提供跨服务同步回滚。

  • 不适合必须立即拿到下游执行结果的业务。

十二、最大努力通知:该重试重试,该查询查询

最大努力通知适合通知类业务。

常见例子是支付回调:

  支付平台
    -> 第一次通知商户
    -> 失败后重试
    -> 按策略多次重试
    -> 商户仍可主动查询支付结果

它不是严格意义上的原子事务,而是通过:

  • 多次通知。

  • 指数退避。

  • 状态查询接口。

  • 对账任务。

尽最大努力让双方状态一致。

适合场景:

  • 第三方支付回调。

  • 短信、邮件、推送。

  • 跨企业系统通知。

  • 对实时性要求没那么高的同步。

对外部系统,最大努力通知往往比强行追求“同步成功”更现实。

十三、Seata:把多种分布式事务模式放进统一框架

Apache Seata 是常见的分布式事务中间件。根据官方文档,它提供四种事务模式:

  • AT。

  • XA。

  • TCC。

  • Saga。

Seata 里有三个核心角色:

调用关系:

1、Seata AT 模式:低侵入,但不是没有代价

Seata AT 模式适用于:

  • 使用支持本地 ACID 事务的关系型数据库。

  • Java 应用通过 JDBC 访问数据库。

  • 希望降低业务改造成本。

它可以理解为对两阶段事务的一种工程化演进。

第一阶段

业务 SQL 和回滚日志在同一个本地事务里提交:

解析业务 SQL
  -> 保存修改前镜像 before image
  -> 执行业务 SQL
  -> 保存修改后镜像 after image
  -> 写 undo_log
  -> 获取全局锁
  -> 提交本地事务
  -> 释放本地锁和数据库连接

第二阶段

全局提交:

异步清理 undo_log

全局回滚:

根据 undo_log 生成反向补偿
  -> 恢复业务数据

为什么 AT 比传统 XA 更轻

AT 模式第一阶段提交本地事务后,就能较快释放本地数据库锁和连接。第二阶段提交可以异步完成。但它仍然要维护全局锁,避免多个全局事务对同一行产生脏写。

AT 模式的限制

  • 依赖关系型数据库和 JDBC。

  • SQL 需要能被框架识别和代理。

  • 维护 undo_log 有成本。

  • 热点数据上的全局锁竞争仍会影响性能。

  • 默认全局读隔离并不等同于强一致快照读。

  • 跨第三方服务无法透明接入。

AT 模式适合改造成本敏感的内部服务,不适合拿来掩盖所有架构问题。

一个简化示例

  @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
  public Long createOrder(CreateOrderCommand command) {
      stockClient.deduct(command.skuId(), command.quantity());
      return orderRepository.save(Order.create(command)).getId();
  }

代码看起来轻量,但生产落地前仍要验证:

  • 数据源代理是否生效。

  • undo_log 是否创建。

  • SQL 是否支持。

  • RPC 上下文是否传递 XID。

  • 全局锁冲突怎么监控。

  • TC 是否高可用。

2、Seata XA 模式:标准 XA 语义

Seata XA 模式使用数据库对 XA 协议的支持管理分支事务。

执行阶段:

  XA start
    -> 执行业务 SQL
    -> XA end
    -> XA prepare

完成阶段:

  XA commit
  或
  XA rollback

优点:

  • 使用数据库原生 XA 能力。

  • 语义更接近强一致事务。

  • 数据无需由框架生成反向补偿 SQL。

缺点:

  • 事务资源占用时间较长。

  • 吞吐受限。

  • 依赖数据库和驱动 XA 支持。

  • 跨服务长事务不合适。

适合:

  • 参与数据库数量有限。

  • 事务短。

  • 强一致优先。

  • 系统能接受性能损耗。

3、Seata TCC 模式:核心交易链路的细粒度控制

Seata TCC 让业务定义 Try、Confirm、Cancel。

接口示意:

  @LocalTCC
  public interface StockTccAction {
  
      @TwoPhaseBusinessAction(
          name = "stockTccAction",
          commitMethod = "confirm",
          rollbackMethod = "cancel"
      )
      boolean tryReserve(
          BusinessActionContext context,
          @BusinessActionContextParameter(paramName = "skuId") Long skuId,
          @BusinessActionContextParameter(paramName = "quantity") Integer quantity
      );
  
      boolean confirm(BusinessActionContext context);
  
      boolean cancel(BusinessActionContext context);
  }

Seata 官方文档强调,TCC 属于侵入式方案。它不依赖底层数据库事务模型来自动完成全部事情,而是把资源控制交给业务。

适合:

  • 核心交易。

  • 对性能要求高。

  • 资源能冻结和释放。

  • 团队能承担更高研发成本。

不适合:

  • 业务没有可补偿模型。

  • 参与方由外部公司维护。

  • 流程长、步骤多。

  • 团队还没有幂等、监控和压测基础。

4、Seata Saga 模式:状态机驱动的长事务

Seata Saga 面向长流程事务。每个参与者提交自己的本地事务,失败后补偿此前成功的参与者。

根据 Seata 官方文档,Saga 模式适合:

  • 业务流程长。

  • 参与者多。

  • 包含第三方或遗留系统。

  • 参与方无法提供 TCC 所需的三个接口。

Seata Saga 当前可以通过状态机引擎定义流程。一个节点代表一次服务调用,也可以配置对应补偿节点。

示意:

  CreateOrder
    -> DeductBalance
    -> DeductStock
    -> CreateDelivery
    -> Succeed
  
  失败时:
  CreateDelivery 失败
    -> CompensateStock
    -> CompensateBalance
    -> CancelOrder

优点:

  • 本地事务一阶段提交,不长时间锁资源。

  • 流程可视化和可恢复。

  • 适合异步、高吞吐、长链路。

缺点:

  • 不天然保证隔离性。

  • 补偿逻辑要由业务实现。

  • 状态机设计和运营复杂度更高。

5、Seata 四种模式怎么选

选型要看业务:不要因为 AT 接入简单,就默认所有服务都上 AT。也不要因为 TCC 性能好,就让普通通知链路实现三套接口。

十四、常见业务该选哪种方案

1. 创建订单并扣减库存

可选:

  • 内部系统、短链路、改造成本敏感:Seata AT。

  • 核心秒杀、需要预留库存:TCC。

  • 接受异步下单:Outbox + MQ + 库存消费者幂等。

2. 支付成功后发积分、发优惠券、清购物车

优先:

  • RocketMQ 事务消息。

  • Outbox + MQ。

这些下游动作不应该阻塞支付主流程。失败后重试即可。

3. 退款流程

优先:

  • Saga。

  • 状态机 + 补偿 + 对账。

退款涉及支付渠道、库存、优惠券、积分、订单状态,流程长,而且第三方支付不可能加入本地事务。

4. 跨两个内部数据库的短事务

可选:

  • XA。

  • Seata XA。

  • 如果能改模型,优先考虑合库或异步解耦。

分布式事务有成本。如果两个表本来就应该处于同一个一致性边界,先问一句:是不是服务拆得过细?

5. 支付平台通知商户

优先:

  • 最大努力通知。

  • 商户查询接口。

  • 定时对账。

外部系统无法完全控制,重试和查询接口比强行同步更可靠。

方案选型速查表

结语:分布式事务的答案通常不是“全部回滚”

单机事务给人的感觉很干净:成功就是成功,失败就是失败。

分布式系统没有这么听话。网络超时、服务重启、消息重试、第三方回调、人工处理,都会让业务处于中间状态。所以,设计分布式事务时,不要只问:

  • 怎么保证所有操作一起提交?

还要问:

  • 如果无法一起提交,系统怎么恢复?

  • 如果状态不明确,系统怎么查询?

  • 如果消息重复,系统怎么幂等?

  • 如果自动补偿失败,系统怎么对账?

  • 如果流程卡住,运营怎么介入?

XA、2PC、TCC、Saga、Outbox、RocketMQ 事务消息、Seata 都只是工具。真正可靠的系统,靠的是清楚的业务边界、有限状态机、幂等写入、可控重试、补偿流程和持续对账。

在分布式事务里,最成熟的设计往往不是“永远不出错”,而是出了错以后,系统知道自己在哪里,也知道下一步该怎么走。

0

评论区