分布式事务理论:XA 规范与 JTA 实现

4 阅读55分钟

概述

系列定位说明

本文是 “分布式事务工程实践” 系列的开篇。在《分布式理论基石》系列中,我们深入探讨了 CAP 定理、Raft 共识算法、ZAB 协议、etcd 的 MVCC 与 Revision 机制、分布式唯一 ID、分布式锁的正确性边界、选举算法以及分布式系统反模式。现在,我们将焦点从共识算法与协调服务转移到 分布式事务——如何在多个数据库、多个服务间保证数据一致性。

XA 规范及其在 Java 生态中的实现 JTA,代表了分布式事务的 刚性基准。理解 XA 如何通过两阶段提交(2PC)实现跨资源的强一致性,以及它为何在现代微服务架构中从主流变为边缘,是学习所有后续柔性事务方案(AT、TCC、Saga、可靠消息)的必要前提。只有看清刚性事务的天花板,才能体会为何柔性方案要牺牲强一致性换取高可用与高性能。

核心要点

  • XA 规范xa_startxa_endxa_preparexa_commitxa_rollback 五个接口,严格定义 2PC Phase 1 Prepare + Phase 2 Commit / Rollback 的完整时序,涵盖 Xid 结构、flags 语义和超时处理。
  • 启发式决策:协调者宕机后,参与者在超时后单方面决策,若两个参与者做出相反决策(一个提交、一个回滚),将造成永久性数据不一致,且无法自动恢复。详细推演从 Prepare 成功到协调者重启的全过程。
  • 与 InnoDB 内部 2PC 对比:MySQL InnoDB 的 PFS 2PC 与 XA 2PC 在时序上同构,但协调者位置、网络开销与参与者数量不同。分析 Redo Log Prepare 段与 Binlog 的协同,对比 XA 中 RM 事务日志的角色。
  • JTA 模型TransactionManagerUserTransactionXAResource 三元组,通过 XidTransactionSynchronizationRegistry 传播全局事务上下文,与 Spring 本地事务的 ThreadLocal Connection 绑定有本质区别。
  • Atomikos vs NarayanaCompositeTransaction + CoordinatorImp 日志持久化 vs ArjunaCore 状态机引擎 + ObjectStore。详细拆解 tmlog 格式、恢复机制与性能对比。
  • Spring Boot 整合spring-boot-starter-jta-atomikos 自动配置多数据源,@Transactional 通过 JtaTransactionManager 接入全局事务。提供完整可运行的代码示例与日志分析。
  • XA 四大致命缺陷:协调者单点故障导致事务阻塞;启发式决策引发永久不一致;锁持有时间过长导致并发崩溃(TPS 降至 1/3~1/5);数据库厂商耦合与云原生弹性矛盾。
  • 与 Seata AT 的对比:AT 通过 undo_log 将锁持有时间缩短至本地事务提交前,TPS 可达 XA 的 3–5 倍。从锁模型、日志结构到性能的量化分析。

文章组织架构图

flowchart TD
    A["1. XA 规范与 2PC 协议<br/>五个核心接口与完整时序<br/>含启发式决策故障推演"]
    B["2. 与 InnoDB 内部 2PC 的同构性对比<br/>关联 MySQL 系列第 2 篇"]
    C["3. JTA 事务管理模型<br/>TransactionManager / UserTransaction / XAResource<br/>含传播机制对比"]
    D["4. Atomikos 内核<br/>CompositeTransaction + CoordinatorImp + 日志持久化"]
    E["5. Narayana 内核<br/>ArjunaCore + ObjectStore + 状态机引擎"]
    F["6. Spring Boot 整合 JTA<br/>多数据源配置与 @Transactional 示例"]
    G["7. XA 的四大致命缺陷<br/>与云原生时代为何抛弃 XA<br/>含启发式决策故障分析"]
    H["8. 面试高频专题<br/>含系统设计题与深度解析"]

    A --> B
    B --> C
    C --> D
    C --> E
    D --> F
    E --> F
    F --> G
    G --> H

    classDef default fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
  

总览说明:全文八个模块从 XA 规范与 2PC 协议出发,先夯实协议细节与故障推演,再与 MySQL InnoDB 内部 2PC 对比,随后深入 JTA 事务管理模型,对比两大主流实现 Atomikos 与 Narayana 的内核,接着给出 Spring Boot 整合 JTA 的完整示例,最后集中剖析 XA 的四大致命缺陷与云原生时代的失落,并以扩充后的面试高频专题巩固。

逐模块说明:模块 1–2 建立 XA 的理论根基,包括完整的 2PC 时序、Xid 结构与启发式决策推演,以及与数据库内部 2PC 的对比;模块 3 是 JTA 标准层,突出全局事务传播与 Spring 本地事务的本质区别;模块 4–5 剖析两个主流 JTA 实现的内部架构;模块 6 将理论落地到 Spring Boot;模块 7 是全篇关键结论层,回答“为什么现代微服务极少直接使用 XA”;模块 8 通过深度面试题与系统设计题帮助读者巩固并内化知识。

关键结论:XA 通过 2PC 协议实现跨资源的强一致性,但其协调者单点故障导致事务阻塞,启发式决策更可能造成永久不一致。锁持有时间过长、数据库厂商耦合、云原生弹性矛盾使其在微服务架构中退居边缘。Seata AT 通过 undo_log 缩短锁持有时间,TPS 提升 3–5 倍,成为 Java 生态中替代 XA 的主流刚性方案。


1. XA 规范与 2PC 协议:五个核心接口与完整时序(含启发式决策故障推演)

1.1 X/Open DTP 模型

XA 规范由 The Open Group 于 1991 年在 X/Open DTP(Distributed Transaction Processing)模型中定义。该模型包含三个核心角色:

  • AP(Application Program):应用程序,定义事务边界并执行业务操作。
  • TM(Transaction Manager):事务管理器,协调全局事务,向资源管理器发送 2PC 指令。
  • RM(Resource Manager):资源管理器,通常为数据库或消息队列,提供对共享资源的访问,并支持 XA 接口。

XA 规范的核心即 TM 与 RM 之间的双向接口,通过标准化的函数调用使 TM 能够跨越多个 RM 实现分布式事务的原子提交。

1.2 五个 XA 接口:签名、参数与语义

在 Java 中,XA 接口定义在 javax.transaction.xa.XAResource 中。首先需要理解 Xid 的结构:

public interface Xid {
    int getFormatId();           // 格式标识符,通常由 TM 定义
    byte[] getGlobalTransactionId(); // 全局事务标识,唯一标识一个全局事务
    byte[] getBranchQualifier();    // 分支限定符,区分同一全局事务内的不同 RM
}

Xid 由全局事务 ID 和分支限定符组成。TM 创建全局唯一 globalTransactionId,每 enlist 一个 RM 时,为它分配不同的 branchQualifier,生成唯一的 Xid 实例。恢复时通过 Xid 能够精确匹配到特定 RM 的特定事务分支。

五大核心方法:

  • void start(Xid xid, int flags) throws XAException
    启动一个事务分支,将 RM 与由 xid 标识的全局事务上下文关联。
    flags 常见取值:

    • TMNOFLAGS:新事务分支的开始。
    • TMJOIN:加入一个已存在的事务分支(较少使用)。
    • TMRESUME:恢复之前挂起的事务分支(由 xa_end(TMSUSPEND) 挂起)。
      调用成功后,该 RM 后续的所有操作(如 SQL)都属于该事务分支,直至调用 xa_end
  • void end(Xid xid, int flags) throws XAException
    结束事务分支,断开 RM 与全局事务上下文的关联。
    flags 常见取值:

    • TMSUCCESS:分支内的操作已成功完成,可以进入 Prepare。
    • TMFAIL:分支失败,后续将回滚。
    • TMSUSPEND:挂起事务分支,后续可通过 xa_start(TMRESUME) 恢复。
      断开后,该连接可被释放或用于其他事务。
  • int prepare(Xid xid) throws XAException
    Phase 1 的核心。RM 必须将事务对应的修改持久化到 undo/redo 日志,保证即使宕机也能恢复。
    返回值:

    • XA_OK:表示 RM 已做好准备,承诺后续可以完成 commitrollback。此时 RM 必须保证即使崩溃,重启后仍能根据日志完成提交或回滚。
    • XA_RDONLY:该事务分支只读,无任何修改,RM 可以不参与后续的 Phase 2,优化流程。
      若返回失败或抛出异常,TM 将判定该分支需要回滚。
  • void commit(Xid xid, boolean onePhase) throws XAException
    Phase 2 提交。RM 根据 prepare 阶段的日志将修改生效(应用 redo log),释放所有锁。
    onePhasetrue 时表示一阶段提交优化:TM 确信全局事务只包含这一个 RM,可直接提交,无需 prepare。此时 RM 直接执行 commit,必须保证原子性。

  • void rollback(Xid xid) throws XAException
    Phase 2 回滚。RM 基于 prepare 阶段的日志撤销修改(应用 undo log),释放所有锁。

此外,XAResource 还定义了恢复相关方法:

  • Xid[] recover(int flag) throws XAException
    返回当前 RM 中处于 Prepared(In-Doubt)状态的 XID 列表。flag 取值 TMSTARTRSCANTMENDRSCANTMNOFLAGS 等控制扫描行为。TM 在恢复时遍历所有 RM 的 recover 结果,做出最终决定。
  • void forget(Xid xid) throws XAException
    由 TM 调用,告知 RM 可以安全地丢弃与该 XID 相关的启发式完成的事务记录。通常在人工介入解决启发式不一致后使用。
  • int getTransactionTimeout() / boolean setTransactionTimeout(int seconds)
    读取或设置事务超时时间(秒)。超时后 RM 可能做出启发式决策。

1.3 2PC 完整时序流程与日志交互

一次完整的 XA 2PC 事务流程比单机事务复杂得多,关键点在于 TM 的决策日志RM 的 Prepare 日志

  1. TM 创建全局事务:生成 Xid(全局 ID + 默认分支限定符)。
  2. 启动分支:对于每个 RM,TM 调用 xa_start(xid, TMNOFLAGS)。实际实现中,TM 可能会为每个 RM 分配一个修改了 branchQualifierXid 副本。
  3. AP 执行业务操作:应用程序在 RM 上执行 SQL。
  4. 结束分支:TM 调用 xa_end(xid, TMSUCCESS),此时 RM 不再接受新的操作。
  5. Phase 1 Prepare:TM 向所有 RM 并发发送 xa_prepare(xid)
    • RM 收到后,会执行以下动作:
      • 将事务的所有修改写入 undo/redo 日志,并强制刷盘(fsync)。这一过程与 InnoDB 内部 Prepare 类似。
      • 在内存中标记该事务分支为 Prepared,相关锁依然持有。
      • 返回 XA_OKXA_RDONLY
    • TM 收集所有投票:
      • 全部 返回 XA_OK(或只读),TM 决定提交。
      • 任一 返回失败,或超时未响应,TM 决定回滚。
  6. 写决策日志:TM 必须 先将决策(Commit 或 Rollback)持久化到自己的事务日志(如 Atomikos 的 tmlog 文件),并强制刷盘。这是防止 TM 崩溃后决策丢失的关键。如果 TM 在写日志前崩溃,重启后无从知晓之前的决定,将导致参与者长时间 In-Doubt。
  7. Phase 2 执行
    • 提交路径:TM 向所有返回 XA_OK 的 RM 发送 xa_commit(xid, false)。RM 应用 redo log,使修改最终生效,并释放锁。完成后,TM 可清理事务日志。
    • 回滚路径:TM 向所有 RM 发送 xa_rollback(xid)。RM 应用 undo log 撤销修改,释放锁。TM 清理日志。

时间维度上的锁竞争:从 xa_prepare 开始到 xa_commit/xa_rollback 完成,数据库行锁一直保持。这包括了网络往返、TM 日志刷盘、以及与其他参与者的协调等待。假设一次 xa_prepare 网络 RTT 为 5ms,两个参与者并行,加上 TM 本地日志 10ms,总锁持有时间至少 15ms。在 1000 QPS 的场景下,锁等待队列迅速膨胀。

1.4 启发式决策与故障推演

XA 的核心脆弱点在于:Phase 1 完成后,RM 无法单方面释放锁或回滚——它必须等待 TM 的 Phase 2 指令。若 TM 在发出 Prepare 后宕机,RM 将无限期持有锁,处于 “In-Doubt” 状态。
更糟糕的是,若 RM 在长时间得不到指令后做出 单方面决策(启发式决策),而不同 RM 做出相反决策,将导致 永久性数据不一致

详细故障推演(带时间线)

  1. T1 时刻:TM 向参与者 A(DB1)与 B(DB2)发送 xa_prepare,两者均执行成功,返回 XA_OK。RM 持有的锁此时已建立。
  2. T2 时刻:TM 在持久化 Commit Decision 之前 宕机(如进程崩溃、断电),未向任何参与者发送 Phase 2 指令。TM 的事务日志中没有留下此次全局事务的决策记录。
  3. T3 时刻起:参与者 A 和 B 持续等待 Phase 2 指令。由于各自的超时设置(例如 transaction-timeout 为 60 秒),在等待超时后,各自做出启发式决策:
    • RM A 的策略:乐观策略,假设 TM 会提交,于是调用内部 xa_commit,将修改生效,释放锁,并记录启发式提交标记。
    • RM B 的策略:保守策略,假设 TM 出现问题,调用 xa_rollback,撤销修改,释放锁,记录启发式回滚标记。
  4. T4 时刻:TM 进程重新启动。恢复线程扫描事务日志,找不到该全局事务的任何记录(因为决策未持久化)。但是它知道可能有残留的 In-Doubt 事务,于是调用所有已知 RM 的 recover() 方法。
  5. TM 调用 XAResource.recover():RM A 返回一个包含该 XID 的列表,并带有状态标志 XA_HEURCOM(启发式提交);RM B 返回同一 XID,状态为 XA_HEURRB(启发式回滚)。
  6. 不一致确认:TM 发现对于同一个 globalTransactionId,不同的分支做出了相反的单方面决策。A 分支的修改已永久生效,B 分支的修改已被撤销。此时全局事务的原子性被彻底破坏——例如订单已插入但库存未扣减,或相反。
  7. 人工介入:TM 无法自动修复此状态。协议未定义 “补偿” 语义,只能将事件记录到日志,并发出严重告警。DBA 和开发人员必须手动分析数据差异,编写脚本补偿或回退。

启发式决策的深层原因:RM 实现者面临两难——如果不做启发式决策,In-Doubt 事务会无限期占用锁,导致其他正常业务停滞;如果做了,就可能发生上述灾难。大多数生产环境的 DBA 会选择 禁用启发式决策(设置超时为无限大或直接关闭该特性),但这样一来,TM 的可用性直接决定了整个数据库集群的可用性。

1.5 协调者一般故障恢复

若 TM 在持久化 Commit Decision 之后 宕机,恢复过程相对可控:

  1. TM 重启后读取事务日志,发现已记录的 Commit Decision 及对应的 Xid 列表。
  2. 向列表中的每个 RM 重新发送 xa_commit(幂等重试)。即使 RM 已经提交并清理了日志,重复的 xa_commit 也应当是幂等的(或直接返回成功)。
  3. 若某些 RM 未响应(如网络未恢复),TM 持续重试,直到所有 RM 确认完成,然后清理日志。在此期间,已提交的 RM 锁已释放,未响应的 RM 锁仍未释放,会造成部分阻塞,但不会出现不一致。

若 TM 在 Phase 1 阶段就发现某个 RM 投票失败,它会记录 Rollback Decision,随后向所有 RM 发送 xa_rollback。同样,若 TM 在发送部分 xa_rollback 后宕机,重启后依据日志继续回滚。这是 XA 提供的基线一致性保障。

1.6 XA 2PC 完整时序图(含启发式决策路径)

sequenceDiagram
    participant TM as "协调者 (TM)"
    participant A as "参与者 A (DB1)"
    participant B as "参与者 B (DB2)"

    Note over TM,A: "正常 2PC 流程"
    Note over B: "正常 2PC 流程"
    TM->>A: "xa_start(xid)"
    TM->>B: "xa_start(xid)"
    A-->>TM: "业务操作..."
    B-->>TM: "业务操作..."
    TM->>A: "xa_end(xid, TMSUCCESS)"
    TM->>B: "xa_end(xid, TMSUCCESS)"
    
    Note over TM,A: "Phase 1 Prepare"
    Note over B: "Phase 1 Prepare"
    TM->>A: "xa_prepare(xid)"
    TM->>B: "xa_prepare(xid)"
    A-->>TM: "XA_OK"
    B-->>TM: "XA_OK"

    Note over TM: "持久化 Commit Decision 到日志 (fsync)"
    Note over TM,A: "Phase 2 Commit"
    Note over B: "Phase 2 Commit"
    TM->>A: "xa_commit(xid)"
    TM->>B: "xa_commit(xid)"
    A-->>TM: "提交成功,释放锁"
    B-->>TM: "提交成功,释放锁"

    Note over TM,A: "故障路径:TM 在 Prepare 后宕机"
    Note over B: "故障路径:TM 在 Prepare 后宕机"
    Note over TM: "宕机,未持久化 Decision"
    A->>A: "超时,做出启发式决策:提交"
    B->>B: "超时,做出启发式决策:回滚"
    Note over A,B: "数据永久不一致"

    TM->>TM: "重启,调用 recover()"
    TM->>A: "recover() 返回 XID (XA_HEURCOM)"
    TM->>B: "recover() 返回 XID (XA_HEURRB)"
    Note over TM: "发现相反启发式结果<br/>无法自动修复,需人工介入"

图表元素说明

  • 参与者:协调者 TM 和两个资源管理器 A、B,分别连接不同数据库。
  • 时序流:严格区分正常提交路径和 TM 宕机后的启发式决策路径。
  • 关键转折:TM 在 Phase 1 收到全部 OK 但未持久化 Commit Decision 即宕机,导致两个参与者做出相反的单方面决策。
  • 恢复结果:TM 重启后通过 recover() 发现矛盾状态,但协议本身没有自动化修复手段,凸显启发式决策的不可逆损害。

2. 与 InnoDB 内部 2PC 的同构性对比

MySQL InnoDB 存储引擎在单机事务提交过程中,也采用了内部的两阶段提交协议(称为 PFS:Prepare-Force-Sync),以协调 InnoDB 的重做日志(Redo Log)与 MySQL 服务器层的二进制日志(Binlog)。详见 MySQL 系列第 2 篇事务与 MVCC。

2.1 InnoDB 内部 2PC(PFS)流程

当执行 COMMIT 时,InnoDB 内部执行以下步骤:

  1. Prepare 阶段:InnoDB 将事务对应的 Redo Log 写入 log buffer,并标记为 Prepare 状态,然后执行 fsync 将 Redo Log 刷新到磁盘(Force)。此时事务被标记为 TRX_STATE_PREPARED
  2. Commit 阶段
    • MySQL 服务器层将语句对应的 Binlog 事件写入 Binlog 文件,并 fsync 刷盘(Sync)。
    • 写入完成后,InnoDB 在 Redo Log 中写入一个特殊的 Commit 标记,并再次刷盘,将事务状态改为 TRX_STATE_COMMITTED_IN_MEMORY(已完成提交)。

这里的协调者是 mysqld 进程本身,参与者是 InnoDB 引擎和 Binlog 子系统。如果系统在 Prepare 之后、写 Binlog 之前崩溃,重启时恢复程序会发现 Redo Log 中存在 Prepared 但没有对应 Binlog 的事务,将其回滚。如果 Binlog 已写入但 Redo Commit 标记未写,恢复程序会借助 Binlog 重新完成事务提交(内部 XA COMMIT)。

2.2 与 XA 2PC 的同构性

  • 两阶段时序一致:先 Prepare(持久化 Redo 日志并承诺可提交),再 Commit(写 Binlog 并标记完成)。
  • 都需要日志持久化:InnoDB 依赖 Redo Log 的 Prepare 段和 Binlog,XA 依赖各 RM 的 undo/redo 日志。
  • 决策记录:InnoDB 中,Binlog 的写入相当于 TM 的 “Commit Decision”。XA 中 TM 写自己的事务日志。

2.3 关键差异

维度InnoDB PFS 2PCXA 2PC
协调者位置mysqld 内部,单进程外部独立事务管理器 (TM)
参与者数量2 个(InnoDB 引擎 + Binlog)多个独立的数据库实例
网络开销无(内存通信或本地磁盘 I/O)多次 RPC 网络交互,有网络分区风险
故障恢复单机崩溃重启自动扫描日志恢复TM 恢复依赖自身日志和 RM 的 recover(),可能出现启发式决策
锁持有时间从 Prepare 到 Commit 极短(磁盘 I/O 时间)从 Prepare 到 Commit 包含网络 RTT,较长
一致性边界单机强一致跨节点强一致,但受 CAP 约束(分区时牺牲可用性)

2.4 InnoDB PFS 2PC 与 XA 2PC 架构对比图

flowchart LR
    subgraph InnoDB_PFS ["InnoDB 内部 2PC (单机)"]
        mysqld["mysqld (协调者)"]
        InnoDB_Engine["InnoDB 引擎 (Redo Log)"]
        Binlog["Binlog 子系统"]
        mysqld -->|"Prepare: 写 Redo 并 fsync"| InnoDB_Engine
        mysqld -->|"Commit: 写 Binlog 并 fsync"| Binlog
        InnoDB_Engine -->|"刷盘"| RedoLog["Redo Log Prepare 段"]
        Binlog -->|"刷盘"| BinlogFile["Binlog Commit 事件"]
        mysqld -->|"Commit 标记刷盘"| InnoDB_Engine
    end

    subgraph XA_2PC ["XA 2PC (分布式)"]
        TM["TM (外部协调者)"]
        RM1["RM1 (MySQL)"]
        RM2["RM2 (PostgreSQL)"]
        TM -->|"xa_prepare"| RM1
        TM -->|"xa_prepare"| RM2
        RM1 -->|"持久化 undo/redo"| Log1[("事务日志")]
        RM2 -->|"持久化 undo/redo"| Log2[("事务日志")]
        TM -->|"xa_commit"| RM1
        TM -->|"xa_commit"| RM2
    end

    classDef innoDB fill:#f0f0f0,stroke:#333,stroke-width:2px,color:#333
    classDef xa fill:#fff3e0,stroke:#333,stroke-width:2px,color:#333

    class InnoDB_PFS innoDB
    class XA_2PC xa

图表元素说明

  • 左侧 InnoDB 内部:协调者是 mysqld 进程,参与者为 InnoDB 引擎和 Binlog,流程无网络开销。
  • 右侧 XA 2PC:协调者 TM 独立于数据库,通过 RPC 与多个 RM 交互,每个 RM 维护自己的事务日志。
  • 关键差异:内部 2PC 的日志同步在单机内存/磁盘,XA 的日志分散在不同节点,需处理网络分区。
  • 设计启示:XA 将单机事务的两阶段逻辑推广到分布式场景,却放大了故障域,引入了协调者单点与启发式决策等新问题。

3. JTA 事务管理模型:TransactionManager / UserTransaction / XAResource(含传播机制对比)

JTA(Java Transaction API)1.2 是 Java EE 7 平台的一部分(JSR 907),它为分布式事务提供了标准 Java 接口。其核心接口构成了经典的“三元组”。

3.1 三个核心接口

  • javax.transaction.TransactionManager
    面向应用服务器或事务中间件的接口,管理全局事务的生命周期:

    • begin():启动一个全局事务,并将当前线程与之关联。
    • commit():触发 2PC,协调所有参与者。
    • rollback():回滚全局事务。
    • suspend() / resume(Transaction tobj):挂起当前线程的事务,或将外部事务挂入当前线程,常用于跨线程或异步场景。
    • setTransactionTimeout(int seconds):设置事务超时。

    TransactionManager 通常由容器(如 Spring 的 JtaTransactionManager)内部使用,应用程序很少直接调用。

  • javax.transaction.UserTransaction
    面向应用程序的高层接口,简化事务边界控制:

    • begin()
    • commit()
    • rollback()
    • setTransactionTimeout(int seconds) 在 EJB 中可通过 @Resource 注入,在 Spring 中可结合 @Transactional 使用。Spring 的 JtaTransactionManager 同时实现了这两个接口。
  • javax.transaction.xa.XAResource
    定义资源管理器参与全局事务的协议。已在前文详述。

3.2 事务传播机制对比

JTA 全局事务传播

JTA 通过 Transaction 对象和 TransactionSynchronizationRegistry 来传播事务上下文。Transaction 对象包含 Xid 和参与者列表。TransactionSynchronizationRegistry 内部通常使用 ThreadLocal 存放当前事务的引用,但这个引用指向的是全局事务对象,而非单一的数据库连接。

当应用程序操作多个数据源时,底层 JTA 实现(如 Atomikos)会在获取连接时,自动将对应的 XAResource enlist 到当前事务对象中。所有 enlist 的 XAResource 共享同一个全局 Xid(分支限定符不同)。这样,TM 在提交时就知道要协调哪些 RM。

Spring 本地事务传播

Spring 的 @Transactional 基于 PlatformTransactionManager,默认使用 DataSourceTransactionManager。其底层通过 TransactionSynchronizationManager 使用 ThreadLocal<Map<DataSource, ConnectionHolder>> 来绑定当前线程的数据库连接。一个线程可以同时绑定多个数据源的连接,但它们彼此独立,DataSourceTransactionManager 只是保证每个数据源内的本地事务 ACID。当方法结束时,它按倒序逐个调用每个连接的 commit()。这些提交不是原子的——若第二个提交失败,第一个提交无法回滚,导致数据不一致。

本质差异

  • JTA 传播的是 全局事务上下文(XID),可以跨多个资源管理器进行原子协调。
  • Spring 本地事务传播的是 单一资源的连接绑定,无法自动实现多数据源的原子性。
  • 要使 Spring 管理多数据源事务,必须引入 JtaTransactionManager,它充当 Spring 事务体系与 JTA 之间的桥梁:将 Spring 的 @Transactional 调用委托给真实的 TransactionManager

JTA 跨 JVM 传播(RMI/IIOP)

在传统 Java EE 应用服务器(如 WebLogic, WebSphere)中,全局事务上下文可以通过 RMI/IIOP 协议在服务间传播。当远程 EJB 调用发生,服务器会将当前事务上下文与 IIOP 请求一起发送,接收端服务器利用 TransactionManager 将远程事务合并到本地事务中。这种分布式事务传播依赖于 JTS(Java Transaction Service)协议,但运维复杂,且与现代 REST/gRPC 通信模型格格不入,目前微服务架构中几乎不再使用。

3.3 JTA 恢复机制

XAResource.recover(int flag) 在恢复流程中起到关键作用。TM 重启后,会遍历所有已知 RM 调用 recover,获取当前处于 Prepared 但未完成的 XID 列表。每个 XID 均带有分支信息,TM 将其与自己的事务日志匹配:

  • 若日志中有 Commit Decision,则重试 xa_commit
  • 若日志中有 Rollback Decision,则重试 xa_rollback
  • 若日志中无记录,说明 TM 在决策前崩溃,此时 TM 可以向所有参与者发送 xa_rollback,因为未决策即意味着回滚是安全的(毕竟事务尚未提交)。但需要注意的是,若某个参与者已经启发式提交,回滚将失败,暴露不一致。

恢复流程的复杂性 常常被低估:网络可能分区、RM 可能临时不可用,TM 需要持续重试,可能长达数小时甚至更久。在此期间,参与者的锁一直持有,造成连锁反应。

3.4 JTA 事务传播 vs Spring 本地事务传播对比图

flowchart TD
    subgraph JTA_Propagation ["JTA 全局事务传播"]
        App1["Application"]
        TM1["TransactionManager"]
        XID[("全局 XID")]
        RM_A["XAResource A (DB1)"]
        RM_B["XAResource B (DB2)"]
        App1 -->|"begin"| TM1
        TM1 -->|"创建 XID, enlist"| RM_A
        TM1 -->|"enlist"| RM_B
        App1 -->|"操作"| RM_A
        App1 -->|"操作"| RM_B
        RM_A -.->|"关联同一 XID"| RM_B
    end

    subgraph Spring_Local ["Spring 本地事务传播"]
        App2["@Transactional"]
        TSM["TransactionSynchronizationManager"]
        Conn1[("Connection 1")]
        Conn2[("Connection 2")]
        App2 --> TSM
        TSM -->|"ThreadLocal 绑定"| Conn1
        TSM -->|"独立绑定"| Conn2
        Conn1 -.->|"无协调"| Conn2
    end

图表元素说明

  • JTA 传播:全局 Xid 是事务上下文的核心标识,协调者 TM 将所有参与 RM 的 XAResource 纳入同一事务,确保原子提交。
  • Spring 本地传播:每个数据源的 Connection 被独立绑定在线程上,彼此之间无协调关系。
  • 关键后果:JTA 可以跨越多个数据库,Spring 本地事务仅适用于单一数据源。
  • 整合关系:当需要多数据源事务时,Spring 必须依靠 JtaTransactionManager 将事务管理委托给 JTA 实现。

4. Atomikos 内核:CompositeTransaction + CoordinatorImp + 日志持久化

Atomikos TransactionsEssentials 是 Java 生态中最流行的轻量级 JTA 实现之一。其核心架构围绕三个概念构建:CompositeTransactionCoordinatorImp 和基于文件/数据库的事务日志。

4.1 核心组件交互

  • UserTransactionManager:同时实现 TransactionManagerUserTransaction 接口,提供事务的启动、提交、回滚以及资源 enlist 功能。它是整个框架的入口。
  • CompositeTransaction:代表一个全局事务。内部有一个 SubTransaction 列表,每个 SubTransaction 对应一个参与者(资源管理器)。CompositeTransaction 本身实现了 Transaction 接口,并持有 Xid。它负责管理全局事务的状态:ACTIVEPREPARINGPREPAREDCOMMITTINGCOMMITTED(或 ABORTED)。
  • CoordinatorImp:协调者实现。当 commit()rollback() 被调用时,CompositeTransaction 委托给 CoordinatorImp 执行两阶段流程。CoordinatorImp 遍历所有 SubTransaction,对每个对应的 XATransactionalResource 执行 preparecommit/rollback。同时,它通过 TransactionLog 将事务状态变更持久化。
  • XATransactionalResource:封装 JDBC 连接的 XAResource。它负责将连接 enlist 到当前的 CompositeTransaction 中,并记录参与信息。在 prepare 阶段,调用底层驱动的 XAResource.prepare()
  • AtomikosDataSourceBean:包装 JDBC DataSource,返回支持 XA 的连接。当应用调用 getConnection() 时,它会从连接池获取一个物理连接,检测当前线程是否有活动事务,如有则自动 create 一个 XATransactionalResource 并 enlist 到该事务中。

4.2 事务日志 (tmlog) 与恢复

Atomikos 将事务日志默认存储在文件系统 tmlog 目录下。每个事务对应一个或一组文件,文件名包含事务 ID。日志内容包括:

  • 全局事务 ID
  • 参与者列表(每个参与者的类型、标识,以及其 XAResource 的恢复信息)
  • 当前状态(如 PREPARING, PREPARED, COMMITTING, COMMITTED
  • 超时信息

写日志时,CoordinatorImp 在关键点执行强制刷盘 (fsync),以保证宕机后日志的一致性。例如,在决定提交前,必须将 COMMITTING 状态持久化。

恢复流程

  1. Atomikos 启动时扫描 tmlog 目录,读取所有未完成的事务日志。
  2. 对于每个日志,根据状态执行:
    • PREPARED:说明 TM 已发出 Prepare 并收到成功响应,但尚未决定。由于日志中没有 Commit Decision,TM 将发起回滚(因为崩溃前尚未决策,回滚是最安全的选择)。
    • COMMITTING:TM 已决定提交,但可能未完成全部 commit。恢复时向所有参与者重新发送 xa_commit
    • ABORTING:同理,重试 xa_rollback
  3. 若调用 xa_commitxa_rollback 时发现参与者已经启发式完成(返回特定错误码),Atomikos 会记录严重的启发式异常,并停止自动恢复,要求管理员介入。
  4. 所有参与者成功响应后,Atomikos 删除对应的日志文件,事务完成。

日志的可配置性:除了默认的文件存储,Atomikos 还支持将日志存入数据库(通过 com.atomikos.icatch.log_base_dircom.atomikos.icatch.enable_logging 等配置),以适应容器化环境。

4.3 Atomikos 的核心组件交互图

flowchart TB
    App["Application<br/>begin / commit / rollback"]
    UTM["UserTransactionManager<br/>实现 TransactionManager & UserTransaction"]
    CT["CompositeTransaction<br/>全局事务,持有 SubTransaction 列表"]
    Coord["CoordinatorImp<br/>2PC 协调,日志持久化"]
    XARes1["XATransactionalResource<br/>封装 DB1 XAResource"]
    XARes2["XATransactionalResource<br/>封装 DB2 XAResource"]
    TMLog[("Transaction Log<br/>tmlog 文件/数据库")]

    App --> UTM
    UTM -->|"创建/获取"| CT
    UTM -->|"commit / rollback"| Coord
    Coord -->|"2PC 指令"| XARes1
    Coord -->|"2PC 指令"| XARes2
    Coord -->|"写日志"| TMLog
    CT -->|"登记 SubTransaction"| XARes1
    CT -->|"登记 SubTransaction"| XARes2
    XARes1 --> JDBC1[("JDBC Connection DB1")]
    XARes2 --> JDBC2[("JDBC Connection DB2")]

    classDef default fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a

图表元素说明

  • 事务启动:应用程序通过 UserTransactionManager 开始事务,创建 CompositeTransaction
  • 资源登记:当应用从 AtomikosDataSourceBean 获取连接时,对应的 XATransactionalResource 被加入到 CompositeTransaction 的 SubTransaction 列表。
  • 2PC 执行:调用 commit() 后,CoordinatorImp 遍历所有 XATransactionalResource,执行 Prepare 和 Commit 流程,并将决策写入事务日志。
  • 恢复路径:重启时协调器读取日志,向资源管理器查询状态并重试未完成的操作。

5. Narayana 内核:ArjunaCore + ObjectStore + 状态机引擎

Narayana(原名 JBoss Transaction Manager)是功能完整的 JTA 实现,也是 WildFly 应用服务器的默认事务管理器。其内核 ArjunaCore 提供了一个通用的事务状态机引擎,不仅支持 JTA,还支持 JTS、Web Services 事务等多种协议。

5.1 ArjunaCore 事务状态机

ArjunaCore 将全局事务建模为严格的状态机,由 BasicAction 类驱动。状态转换路径:

  • CREATEDPREPARINGPREPAREDCOMMITTINGCOMMITTED(成功路径)
  • PREPARING 及之前发生故障,可转到 ABORTINGABORTED(回滚路径)

每个状态转换都是一个原子操作,通过 ObjectStore 持久化状态变更。例如,进入 COMMITTING 状态时,会将 XAResourceRecord(代表每个参与者)的当前状态写入存储。

TransactionReaper(事务回收器):监控事务超时,若事务超过预设时间未完成,回收器会强制将其转为 ABORTING 并回滚。

5.2 ObjectStore 持久化

ObjectStore 是 Narayana 的事务日志抽象层,支持多种后端:

  • 文件系统(默认):在 ObjectStore/ 目录下按事务类型分层存储。
  • 数据库:通过 JDBC 存储,适合多实例共享场景。
  • 内存:仅用于测试。

在分布式事务中,每个参与者(RM)被封装为一个 XAResourceRecord,其中包含 XAResource 引用、Xid 以及当前状态。事务每次状态变更都会导致 XAResourceRecord 被更新并写入 ObjectStore。恢复时,Narayana 从 ObjectStore 加载所有 AtomicAction,检查其 XAResourceRecord 状态,然后驱动状态机尝试完成(提交或回滚)。

5.3 与 Atomikos 的对比

维度AtomikosNarayana
架构轻量级,核心是 CoordinatorImp 与 CompositeTransaction通用事务引擎 ArjunaCore,支持多协议
日志持久化默认文件 tmlog,也支持数据库ObjectStore 抽象,支持文件、DB 等
性能优化日志写入伴随 2PC 流程,有一定开销无锁日志写入(基于内存缓冲+批量刷盘),性能略优
配置与集成Spring Boot 开箱即用,配置简单独立部署配置复杂,但 WildFly 原生集成
适用场景微服务、Spring Boot 多数据源传统 Java EE 应用服务器、需要 JTS 等复杂事务场景

对于 Spring Boot 多数据源事务,Atomikos 通常是更轻便的选择;对于已构建在 WildFly 之上的大型企业应用,Narayana 则提供了无缝集成和更广泛的事务协议支持。


6. Spring Boot 整合 JTA:多数据源配置与 @Transactional 示例

6.1 自动配置原理

引入 spring-boot-starter-jta-atomikos 后,Spring Boot 自动配置类 AtomikosJtaConfiguration 会:

  • 创建 UserTransactionManagerTransactionManager Bean。
  • 创建 JtaTransactionManager Bean,并将默认的 PlatformTransactionManager 切换为它。这使得所有 @Transactional 注解的方法都自动使用 JTA 事务。
  • 当声明多个 DataSource 时,可以通过属性 spring.jta.atomikos.datasource.xxx 将每个数据源包装为 AtomikosDataSourceBean,使其连接自动 enlist 到全局事务。

6.2 完整配置示例

以下示例配置两个 H2 内存数据库(分别模拟订单库和库存库),使用 XA 分布式事务。

Maven 依赖(核心):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

application.yml

spring:
  jta:
    atomikos:
      properties:
        log-base-dir: ./tmlog   # 事务日志目录
        service: myapp          # 事务管理器标识
        max-timeout: 300000     # 最大事务超时 5 分钟

# 主数据源(订单库)
spring.jta.atomikos.datasource.primary:
  xa-data-source-class-name: org.h2.jdbcx.JdbcDataSource
  xa-properties:
    URL: jdbc:h2:mem:orders;DB_CLOSE_DELAY=-1
    user: sa
    password:
  pool-name: PrimaryH2Pool
  max-pool-size: 20

# 次数据源(库存库)
spring.jta.atomikos.datasource.secondary:
  xa-data-source-class-name: org.h2.jdbcx.JdbcDataSource
  xa-properties:
    URL: jdbc:h2:mem:inventory;DB_CLOSE_DELAY=-1
    user: sa
    password:
  pool-name: SecondaryH2Pool
  max-pool-size: 20

数据源配置类(通过 @ConfigurationProperties 绑定):

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.primary")
    public DataSource primaryDataSource() {
        return new AtomikosDataSourceBean();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.secondary")
    public DataSource secondaryDataSource() {
        return new AtomikosDataSourceBean();
    }

    @Bean
    public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource ds) {
        return new JdbcTemplate(ds);
    }

    @Bean
    public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource ds) {
        return new JdbcTemplate(ds);
    }
}

业务服务(跨数据源事务):

@Service
public class OrderService {

    @Autowired @Qualifier("primaryJdbcTemplate")
    private JdbcTemplate orderJdbc;
    @Autowired @Qualifier("secondaryJdbcTemplate")
    private JdbcTemplate inventoryJdbc;

    @Transactional  // 自动使用 JtaTransactionManager
    public void placeOrder(String orderId, String productId, int quantity) {
        // 扣减库存(库存库)
        int updated = inventoryJdbc.update("UPDATE inventory SET stock = stock - ? WHERE product_id = ? AND stock >= ?", quantity, productId, quantity);
        if (updated == 0) throw new RuntimeException("库存不足");
        // 创建订单(订单库)
        orderJdbc.update("INSERT INTO orders(id, product_id, quantity) VALUES (?, ?, ?)", orderId, productId, quantity);
        // 若此处抛出异常,JTA 将回滚两个数据库
    }
}

解读

  • @Transactional 在存在 JtaTransactionManager 时会关联 JTA 全局事务。
  • AtomikosDataSourceBean 获取的连接均为 XA 连接,获取时自动 enlist。
  • 方法成功返回后,Spring 调用 JtaTransactionManagercommit,触发 Atomikos 2PC。
  • 若方法内抛出 RuntimeException,Spring 捕获后调用 rollback,Atomikos 向所有 RM 发送 xa_rollback,撤销修改。

6.3 事务日志 tmlog 的结构与监控

运行应用后,观察 ./tmlog 目录,会生成以事务 ID 命名的文件。文件内容包含:

  • 事务状态
  • 参与者列表(含 XAResource 描述)
  • 超时时间

当应用意外终止(如 kill -9),重启后 Atomikos 会扫描该目录,对未完成事务自动进行恢复。可通过 JMX 或日志观察恢复过程。例如,发现 Prepared 但未 Committed 的事务,TM 会输出类似日志:

INFO: Heuristic completion detected for XID ...

若出现启发式异常,需要立即关注。


7. XA 的四大致命缺陷与云原生时代为何抛弃 XA(含启发式决策故障分析)

尽管 XA 提供了理论上最刚性的跨资源强一致性,但其实际落地充满荆棘,这也是现代微服务架构中极少直接使用 XA 的根本原因。

7.1 协调者单点故障:In-Doubt 事务阻塞

XA 是一个 CP 系统:在发生网络分区时,若协调者无法与参与者通信,事务将无法提交或回滚,参与者持有锁等待。协调者本身若宕机,所有已 Prepared 的事务都成为 In-Doubt,占据数据库锁资源,直到协调者恢复。这违反了微服务“高可用”的基本原则。即使 Atomikos 或 Narayana 提供了恢复,但恢复期间锁依然持有,服务可用性已严重破坏。
在云原生弹性环境中,协调者实例可能随时被调度杀死、漂移,需要有状态持久化卷和快速恢复机制,运维负担极重。

7.2 启发式决策:永久性数据不一致

如 1.4 节详细推演,一旦参与者单方面做出启发式决策且决策相悖,数据将永久不一致,且无自动化修复手段。生产环境中,DBA 常采取禁用启发式决策的方式避免此风险,但这将单点故障转化为业务阻塞,依然不可接受。

7.3 锁持有时间过长:并发性能崩溃

在 XA 2PC 流程中,数据库的行锁从 xa_prepare 开始持有,直到 xa_commit / xa_rollback 完成后才释放。这期间包含:

  • TM 收集所有 Prepare 响应的等待时间(最慢参与者)
  • TM 写决策日志并刷盘
  • 向所有参与者发送 Phase 2 的网络 RTT

假设单次数据库操作需要 5ms,Prepare 阶段由于涉及刷盘可能需要 10ms,网络 RTT 共 10ms,则锁持有时间至少 20ms。单机本地事务锁持有时间可能仅 5ms。在高并发下,同一行数据的锁等待队列将急剧增长,导致 TPS 暴跌。实际压力测试表明,XA 事务的 TPS 通常只有非分布式事务的 1/3 到 1/5

7.4 数据库厂商耦合与云原生弹性矛盾

  • 厂商耦合:所有参与者必须支持 XA 协议。MySQL(InnoDB)、PostgreSQL、Oracle 支持,但 MongoDB、Redis、Cassandra 等 NoSQL 不支持。微服务架构中常混用多种存储,XA 难以统一。
  • 云原生弹性矛盾:微服务要求服务实例可随时扩缩容、无状态。但 XA 协调者持有事务日志,是有状态组件。若将其部署在容器中,必须挂载持久化卷,或使用外部数据库存储日志,同时要处理崩溃恢复、日志清理等,与无状态设计哲学相悖。

7.5 与 Seata AT 的对比:锁持有时间的根本差异

Seata AT 模式通过 undo_log 将锁持有时间大幅缩短。AT 模式在执行本地事务前会先写入 undo_log(记录修改前的数据镜像),本地事务提交时释放数据库锁。全局锁由 Seata TC(Transaction Coordinator)的 global_lock 表管理,仅用于在全局事务提交前防止其他全局事务的脏写。当全局事务最终提交时,才删除 undo_log;若全局回滚,则使用 undo_log 执行补偿(反向 SQL)。因此,数据库层面的行锁在本地事务提交时就已释放,极大提升了并发能力。官方测试数据显示,AT 模式的 TPS 可达 XA 的 3–5 倍。关于 AT 模式的详细原理,详见本系列第 2 篇 Seata AT

7.6 XA vs Seata AT 锁持有时间对比图

gantt
    title 锁持有时间对比:XA vs Seata AT
    dateFormat  HH:mm
    axisFormat %H:%M
    section XA 事务
    本地操作 + xa_prepare :active, a1, 00:00, 20min
    Phase 2 网络等待+提交 :a2, after a1, 15min
    section Seata AT 事务
    本地操作 + undo_log + 本地提交 :active, b1, 00:00, 10min
    全局锁等待(TC) :b2, after b1, 5min
    异步全局提交/回滚 :b3, after b2, 5min

(注:甘特图仅示意时间比例,实际取决于系统负载和网络。)

图表元素说明

  • XA 锁持有时间:横跨整个 Phase 1 和 Phase 2,网络延迟使得锁长时间无法释放。
  • Seata AT 锁持有时间:本地事务提交即释放数据库锁,全局锁在 TC 短暂协调,业务可快速重获锁。
  • 性能差异根源:数据库行锁的早释是提升并发的核心,Seata 将一致性协调上移到应用层。
  • 架构启示:从“数据库强一致锁”向“应用层协调 + 补偿”的演进,是分布式事务从刚性走向柔性的核心变化。

8. 面试高频专题

Q1:XA 规范的五个核心接口是什么?2PC 的两个阶段分别做什么?

一句话回答xa_startxa_endxa_preparexa_commitxa_rollback;Phase 1 Prepare 让各 RM 持久化日志并投票,Phase 2 根据投票结果统一提交或回滚。

详细解释
xa_start 将 RM 与由 Xid 标识的全局事务关联,flags 控制新启、加入或恢复;xa_end 断开关联并标记分支执行结果。xa_prepare 是 Phase 1 核心,RM 必须将事务修改写入 undo/redo 日志并强制刷盘,返回 XA_OK 表示承诺可以提交(此时锁已持有)。Phase 2 中,协调者若收到所有 RM 的 XA_OK 则记录 Commit Decision,随后调用 xa_commit 使修改生效并释放锁;若任一失败,则记录 Rollback 决策,调用 xa_rollback 撤销修改并释放锁。两阶段通过“先征求意见,再统一执行”实现原子性。

多角度追问

  • :如果 Phase 1 某个 RM 返回失败,协调者怎么做?
    :协调者会记录 Rollback Decision,然后向所有 RM(包括已经返回 OK 的)发送 xa_rollback,确保全部回滚。
  • :为什么 xa_prepare 必须持久化日志?
    :为了保证 RM 在 Prepare 后崩溃重启,仍能根据日志完成 commitrollback,从而维持事务的原子性和持久性。
  • xa_end 的 flags 参数中 TMSUSPEND 的作用是什么?
    :允许将事务分支挂起,之后该连接可被用于其他事务,之后再通过 xa_start(TMRESUME) 恢复,常用于有限连接池下的资源复用。

加分回答
可提及 XA 一阶段优化(onePhase=true)跳过 prepare,用于单资源场景;并指出 xa_prepare 返回 XA_RDONLY 时该分支不参与后续提交,减少网络交互。


Q2:什么是启发式决策(Heuristic Decision)?为什么它会导致不可恢复的不一致?请推演协调者宕机后两个参与者做出相反决策的故障场景。

一句话回答:启发式决策是参与者在未收到协调者 Phase 2 指令时,单方面决定提交或回滚;当不同参与者做出相反决策时,全局事务原子性被破坏,数据永久不一致。

详细解释
在 XA 2PC 中,RM 完成 xa_prepare 后进入“准备就绪”状态,此时它既不能单方面提交也不能回滚,必须等待协调者的最终指令。然而,若协调者长时间无响应(宕机),RM 可能根据自身超时策略做出启发式决策,即调用自身的 commitrollback。若两个 RM 做出相反决定(如一个提交、一个回滚),则全局事务的一部分修改生效,另一部分被撤销,违反了原子性。协调者重启后通过 recover() 查询到矛盾状态,但 XA 协议没有定义自动修复算法,数据将永久不一致,必须人工介入(如对比数据,手动补偿或回滚)。

故障推演

  • 初始:TM 向 RM A(订单库)和 RM B(库存库)发送 xa_prepare,均返回 XA_OK
  • 宕机:TM 在写 Commit Decision 前崩溃。
  • 超时:RM A 超时后乐观提交(假设 TM 会提交),订单记录生效;RM B 超时后保守回滚(避免长锁),库存未扣减。
  • 恢复:TM 重启后调用 recover(),RM A 返回 XA_HEURCOM,RM B 返回 XA_HEURRB。TM 无法抉择,只能告警。
  • 结果:订单已生成,库存未扣减,数据不一致,需人工处理。

多角度追问

  • :如何配置 RM 禁用启发式决策?
    :在数据库或 JTA 实现中,将 transaction-timeout 设置为 0 或极大值,并关闭自动启发式决策参数(如 Atomikos 的 com.atomikos.icatch.allow_heuristic_commit)。
  • :什么业务场景下会有意启用启发式决策?
    :极低价值数据且对可用性要求高于一致性的场景,如用户行为日志的跨库写入;通常不推荐。
  • :启发式决策是否违反了 ACID 的原子性?
    :是,它直接破坏了原子性,使事务部分提交部分回滚,因而是不可恢复的错误。

加分回答
可指出 JTA 中 XAResourcerecover() 返回的状态标志包括 XA_HEURCOMXA_HEURRBXA_HEURMIX,并解释 forget() 方法用于在人工解决后清理这些标记。


Q3:MySQL InnoDB 的内部 2PC(PFS)与 XA 2PC 有何异同?

一句话回答:同构点在两阶段 Prepare-Commit 时序;本质区别在于协调者位置(内部单进程 vs 外部分布式)、网络开销和参与者数量。

详细解释
InnoDB 的 PFS 将事务提交分为 Prepare(写 Redo Log Prepare 并刷盘)和 Commit(写 Binlog 并刷盘,再标记 Redo 提交)。这保证了 Redo 和 Binlog 的一致性,且协调者就是 mysqld,无网络交互。XA 2PC 将同一模型扩展到多个独立数据库,协调者是外部事务管理器,通过 RPC 与 RM 通信。同构性在于均采用两阶段表决和日志持久化;差异在于 XA 引入了网络分区、单点故障和启发式决策等分布式特有的复杂故障模式。

多角度追问

  • :InnoDB 崩溃恢复如何利用 Redo 和 Binlog 决定提交或回滚?
    :如果 Redo 中有 Prepared 事务,但 Binlog 中没有,则回滚;如果两者都有,则自动重做 Commit,保证一致。
  • :XA 事务能否直接使用 InnoDB 的 PFS 机制替代?
    :不能,因为 PFS 仅协调单个实例内部的 Redo 和 Binlog,无法跨多个独立数据库。
  • :为什么单机 2PC 不会出现启发式决策问题?
    :因为单机内没有网络分区,协调者(mysqld)要么正常执行完,要么崩溃后由恢复程序统一处理,不存在 RM 独自决策的窗口。

加分回答
可提及 MySQL 5.7 后通过 XA RECOVER 语句查看外部 XA 事务状态,以及 InnoDB 实际上也遵循 XA 接口,可以作为外部 XA 的参与者。


Q4:JTA 的 TransactionManager、UserTransaction、XAResource 各自的职责是什么?

一句话回答TransactionManager 管理全局事务生命周期与参与者;UserTransaction 供应用显式界定事务边界;XAResource 代表参与全局事务的资源,执行 2PC 各阶段操作。

详细解释
TransactionManager 面向容器/中间件,提供挂起、恢复、提交、回滚等完整控制,并负责 enlist 资源。UserTransaction 是简化接口,仅暴露 begin/commit/rollback,供应用直接编码。XAResource 由数据库驱动实现,封装了 XA 协议的具体方法,TM 通过它来与 RM 交互。三者职责分层清晰:应用 → UserTransaction → TransactionManager → XAResource → 数据库。

多角度追问

  • :Spring 的 JtaTransactionManager 属于哪一层?
    :它同时实现了 PlatformTransactionManagerTransactionManagerUserTransaction 接口,是 Spring 与 JTA 实现之间的桥梁。
  • :为什么 TransactionManager 通常不直接暴露给应用?
    :因为它功能过于强大且危险(如挂起/恢复),直接暴露容易误用,通过 UserTransaction 或声明式事务可降低出错概率。
  • :如何在 JTA 环境中手工注册 XAResource
    :可通过 TransactionManager.getTransaction().enlistResource(XAResource) 动态注册,但通常由连接池自动完成。

加分回答
可提到 TransactionSynchronizationRegistryregisterInterposedSynchronization 方法,允许在事务完成前执行回调,常用于清理资源或发送异步消息。


Q5:JTA 的事务传播机制与 Spring 本地事务传播有何本质区别?为什么 Spring @Transactional 无法管理多数据源?

一句话回答:JTA 通过全局 Xid 关联多个 RM 实现跨资源协调;Spring 本地事务通过 ThreadLocal 绑定单一 Connection,无法自动协调多个数据源。

详细解释
JTA 的 TransactionManager 维护一个全局事务对象(包含 Xid 和参与者列表),所有参与 RM 的 XAResource 通过 Xid 联系在一起,协调者可在两阶段提交中对它们进行原子操作。Spring 的 DataSourceTransactionManager 基于 TransactionSynchronizationManager,使用 ThreadLocal 为每个数据源绑定独立的 Connection,提交时逐个调用 connection.commit()。这些提交并非原子性的:如果第一个数据源提交成功,第二个失败,第一个无法回滚。因此,在没有 JTA 的情况下,Spring 无法保证多数据源事务的原子性,只能保证单一数据源内的 ACID。

多角度追问

  • :如果在 Spring 中同时操作两个 JdbcTemplate(不同数据源)并标 @Transactional,默认会发生什么?
    :默认会使用 DataSourceTransactionManager,它只会管理第一个数据源的事务,第二个数据源的操作可能在自动提交模式下执行,或者抛出未配置事务的错误,跨数据源无原子性。
  • :如何使 Spring 支持多数据源事务?
    :引入 JtaTransactionManager 和 JTA 实现(如 Atomikos),将多个数据源配置为 XA 数据源,自动 enlist。
  • :JTA 跨 JVM 传播事务上下文如何实现?
    :通过 JTS 和 IIOP 协议传播事务上下文,但现代微服务已很少使用,更倾向最终一致方案。

加分回答
说明 JtaTransactionManager 桥接的关键在于它内部持有 UserTransactionTransactionManager@Transactional 的切面会调用 JtaTransactionManager.getTransaction() 来加入或创建全局事务,从而把 Spring 事务管理委托出去。


Q6:Atomikos 的协调者(CoordinatorImp)是如何执行 2PC 的?事务日志如何持久化?

一句话回答CoordinatorImp 在 Phase 1 遍历所有 XATransactionalResource 调用 prepare,根据结果写日志并决定 Commit 或 Rollback,Phase 2 执行并清理日志;事务日志默认以文件形式存储在 tmlog 目录。

详细解释
当调用 UserTransactionManager.commit() 时,流程进入 CoordinatorImp。它首先通过 CompositeTransaction 获取所有 SubTransaction 对应的 XATransactionalResource。然后并发(或串行)调用每个 XAResource.prepare()。若全部返回 XA_OK,协调者将 “COMMITTING” 状态写入 tmlog 文件并 fsync,确保已持久化决定。接着,依次调用 XAResource.commit()。任何一个失败都会触发回滚逻辑并记录 “ABORTING” 状态。所有完成后,删除日志文件。恢复时,Atomikos 扫描 tmlog,对于状态为 COMMITTING 的事务重试 commit,对 ABORTING 重试 rollback。

多角度追问

  • :日志写入和 Phase 2 之间宕机,恢复流程是什么?
    :重启后看到 COMMITTING 日志,协调者认为应提交,向参与者重试 xa_commit。若某些参与者之前已提交,重复提交应该幂等。
  • tmlog 可以替换为数据库存储吗?
    :可以,Atomikos 支持 JDBC 日志存储,适合容器化环境。
  • :如果日志文件损坏怎么办?
    :将导致事务无法恢复,In-Doubt 事务可能永久悬挂,需人工处理或手动删除参与者的 Prepared 事务。

加分回答
可提及 Atomikos 的 LogControl 接口和 log_base_dir 配置,以及通过 JMX 监控日志目录的方法。


Q7:XA 的协调者单点故障会导致什么问题?In-Doubt 事务如何恢复?

一句话回答:单点故障导致所有 Prepared 事务成为 In-Doubt,长期持有锁阻塞业务;恢复依赖协调者重启后读取日志与参与者 recover() 交互,若未发生启发式决策可自动完成,否则需人工介入。

详细解释
协调者宕机后,参与者一直持有锁等待指令,无法自行决定。协调者重启后,通过自身日志确定全局事务是应提交还是回滚,然后调用 recover() 向各个 RM 询问哪些 XID 仍处于 Prepared。匹配后,重试相应的 Phase 2 指令。如果日志中没有决策记录(TM 在 Prepare 后、写决策前崩溃),则应该执行回滚以保证安全。但如果某个参与者在此期间已启发式提交,回滚就会失败,导致不一致。因此,In-Doubt 事务的恢复可靠性依赖于是否出现启发式决策,以及日志的完整性。

多角度追问

  • :如何监控 In-Doubt 事务的数量?
    :MySQL 可执行 XA RECOVER 查看;PostgreSQL 的 pg_prepared_xacts 视图;协调者可通过 JMX 监控。
  • :MySQL 如何处理长时间 In-Doubt 的 XA 事务?
    :DBA 可手动执行 XA COMMITXA ROLLBACK 来强制结束,但需自行保证业务一致性。
  • :集群化协调者能解决单点问题吗?
    :理论上可以利用 Raft 等共识算法实现高可用协调者集群,但会增加事务延迟和复杂度,实际工程中少见。

加分回答
介绍一些数据库提供的 XA RECOVER 语法查看挂起事务,并讨论基于 Raft 的协调者集群思路,但指出其会增加复杂度和延迟。


Q8:为什么 XA 在现代微服务架构中极少被使用?四大致命缺陷是什么?

一句话回答:协调者单点故障导致阻塞、启发式决策导致永久不一致、锁持有时间过长导致并发崩溃、数据库厂商耦合与云原生弹性矛盾。

详细解释
微服务追求高可用、弹性伸缩和松耦合。XA 的强一致性 (CP) 与这些目标冲突。单点故障使数据库被锁死,可用性下降;启发式决策虽然罕见,但一旦发生就是灾难;长锁严重限制并发,在促销等高并发场景下无法支撑;很多微服务使用的 NoSQL 或消息队列不支持 XA;最后,有状态的协调者难以在容器化环境中优雅地伸缩和恢复。因此,社区转向最终一致性柔性事务(AT、TCC、Saga)来平衡一致性和可用性。

多角度追问

  • :哪些场景仍适合用 XA?
    :金融核心账务系统中少量关键服务,对一致性要求极端严格且并发不高,并且运维能力强大的环境。
  • :银行业务为何有时仍坚持 XA?
    :监管和资金安全要求绝对精确,不容许最终一致,此时宁愿牺牲性能和可用性。
  • :XA 有没有可能通过 coordinator 集群解决部分缺陷?
    :理论上可以,但集群共识会导致锁持有时间进一步延长,复杂度剧增,收益有限。

加分回答
可举金融行业核心系统仍用 XA 的例子,但强调互联网高并发场景下 Seata、TCC 等方案的优势。


Q9:Spring Boot 如何整合 JTA 实现跨多个数据源的事务?@Transactional 如何工作?

一句话回答:引入 spring-boot-starter-jta-atomikos,配置多个 XA 数据源,Spring 自动装配 JtaTransactionManager@Transactional 方法操作多个数据源时自动纳入 JTA 全局事务。

详细解释
自动配置将默认的 PlatformTransactionManager 替换为 JtaTransactionManager,后者内部持有 Atomikos 的 UserTransactionTransactionManager。当 @Transactional 注解的方法执行时,Spring 事务拦截器调用 JtaTransactionManager.getTransaction(),该方法检查当前线程是否有活跃的 JTA 事务,如果没有则通过 UserTransaction.begin() 创建。方法内的数据库操作从 AtomikosDataSourceBean 获取连接,连接自动 enlist 到当前事务。方法返回时,拦截器调用 JtaTransactionManager.commit()UserTransaction.commit(),触发 Atomikos 协调者的 2PC。

多角度追问

  • :如何配置 Atomikos 的日志持久化到数据库?
    :设置 spring.jta.atomikos.properties.log-base-direnable-logging 即可。
  • :如果不需要全局事务,如何排除某个数据源?
    :普通数据源不要包装为 AtomikosDataSourceBean,它就不会 enlist。
  • :如何验证 2PC 回滚是否生效?
    :在业务方法中抛异常,检查两个数据库均无变化;也可查看 Atomikos 日志输出了 rollback。

加分回答
可提供实际测试用例,使用 @Transactional@Rollback,或通过 TestTransaction 在测试中触发回滚,验证数据一致性。


Q10:XA 与 Seata AT 在锁持有时间上有何本质区别?为什么 AT 的 TPS 是 XA 的 3–5 倍?

一句话回答:XA 从 Prepare 直到全局 Commit 全程持有数据库锁;AT 在本地事务提交时就释放数据库锁,仅持有 Seata 的全局软锁,锁竞争大幅降低,吞吐显著提升。

详细解释
Seata AT 将全局事务拆分为两个阶段,但第一阶段就完成本地事务提交并释放数据库锁,仅留下 undo_log 和全局锁(在 TC 的表中)。全局锁用于防止其他全局事务对同一数据进行写操作,但它是乐观协调,不阻塞本地事务的提交,因此数据库层面的并发能力几乎不受影响。第二阶段全局提交或回滚是异步进行的。XA 则需要全程持有数据库锁,直到协调者的第二阶段指令到达。因此,XA 的数据库锁持有时间至少是本地事务的 3~5 倍,直接限制了吞吐。

多角度追问

  • :Seata AT 的全局锁如何防止脏写?
    :在第二阶段提交前,AT 会检查是否存在其他全局事务的全局锁冲突,如果冲突则回滚,这是一种乐观并发控制。
  • :AT 模式在回滚时如何使用 undo_log
    :TC 通知 RM 回滚时,RM 读取 undo_log 中的前镜像数据,生成反向 SQL 执行补偿,将数据恢复为修改前的样子。
  • :什么情况下 AT 模式的吞吐会下降?
    :当热点数据冲突严重,全局锁冲突导致大量回滚时,性能会恶化,此时可考虑 TCC 模式。

加分回答
可提及 Seata 的 global_lock 与隔离级别的关系,以及 AT 写隔离的实现依赖 SELECT FOR UPDATE 和全局锁机制。


Q11: 系统设计题

题目:设计一个订单系统,需要同时操作订单库(MySQL)和库存库(PostgreSQL),要求强一致性。请回答:
(1)基于 XA 的完整技术方案,包括核心配置与事务边界,以及详细的架构图。
(2)在并发 1000 QPS 下的性能瓶颈量化分析。
(3)启发式决策风险、故障推演、防范与应急响应。
(4)当 QPS 增长到 5000 时,为什么必须从 XA 迁移到 Seata AT 或 TCC?比较三个方案。
(5)从 XA 到 Seata AT 的详细迁移步骤与灰度方案。

(1)XA 方案的核心配置、事务边界与架构图

整体架构

flowchart TD
    Client[客户端]
    LB[负载均衡]
    OrderService[订单服务<br/>Spring Boot]
    Atomikos[Atomikos TM<br/>UserTransactionManager<br/>+ CoordinatorImp]
    TMLog[(事务日志 tmlog<br/>持久化卷)]
    MySQL[(订单库 MySQL<br/>XA 驱动)]
    PostgreSQL[(库存库 PostgreSQL<br/>XA 驱动)]
    
    Client --> LB --> OrderService
    OrderService --> Atomikos
    OrderService -->|placeOrder| BusinessLogic[业务逻辑<br/>扣减库存 + 创建订单]
    BusinessLogic -->|获取 XA 连接| MySQL
    BusinessLogic -->|获取 XA 连接| PostgreSQL
    Atomikos -->|2PC 协调| MySQL
    Atomikos -->|2PC 协调| PostgreSQL
    Atomikos -->|写决策日志| TMLog
    
    style Atomikos fill:#e0f0ff,stroke:#3080c0
    style MySQL fill:#f9f2d0,stroke:#b0a000
    style PostgreSQL fill:#f9f2d0,stroke:#b0a000

架构说明

  • 服务层:单一的 Spring Boot 服务 order-service,内含业务逻辑 placeOrder
  • 事务管理器层:嵌入在服务进程中,使用 Atomikos 实现 JTA。UserTransactionManager 创建全局事务,CoordinatorImp 执行 2PC。
  • 资源层:两个数据库实例,分别是 MySQL(订单库)和 PostgreSQL(库存库),均通过 XA 驱动连接。连接池使用 AtomikosDataSourceBean 包装。
  • 日志层tmlog 目录挂载在持久化卷(Kubernetes PVC 或宿主机目录)上,用于协调者崩溃恢复。
  • 事务边界@Transactional 注解的 placeOrder 方法入口即为事务开始,方法正常返回触发全局提交,异常导致全局回滚。

核心配置示例
(沿用前文 application.yml 配置,增加超时与恢复参数)

spring:
  jta:
    atomikos:
      properties:
        log-base-dir: /data/tmlog          # 持久化卷挂载
        service: order-service
        max-timeout: 30000                 # 全局事务最大 30 秒
        default-jta-timeout: 15000         # 默认超时 15 秒
        enable-logging: true
        tm-unique-name: order-tm
        allow-sub-tx: false
        serial-jta-transactions: false

事务边界

@Service
public class OrderService {
    @Autowired private JdbcTemplate orderJdbc;
    @Autowired private JdbcTemplate inventoryJdbc;

    @Transactional
    public void placeOrder(String orderId, String productId, int quantity) {
        // 1. 扣减库存 (PostgreSQL)
        int updated = inventoryJdbc.update(
            "UPDATE inventory SET stock = stock - ? WHERE product_id = ? AND stock >= ?",
            quantity, productId, quantity);
        if (updated == 0) throw new RuntimeException("库存不足");
        // 2. 创建订单 (MySQL)
        orderJdbc.update(
            "INSERT INTO orders(id, product_id, quantity) VALUES (?, ?, ?)",
            orderId, productId, quantity);
    }
}

事务边界包含两个数据库操作,任何一个失败都将导致整个全局事务回滚。

(2)并发 1000 QPS 下的性能瓶颈量化分析

假设条件

  • 单次 placeOrder 纯数据库操作耗时 5ms(包括 SQL 执行和本地事务开销)。
  • 两个数据库均在同一数据中心,与服务的网络 RTT 为 2ms。
  • Atomikos 日志写盘(fsync)耗时 5ms。
  • 库存热点行(product_id 固定的某商品)存在严重竞争。

XA 事务的时间分解

  1. 应用获取两个 XA 连接,xa_start(轻量,忽略)。
  2. 执行 SQL 业务操作:5ms。
  3. xa_end:忽略。
  4. Phase 1 Prepare:向两个 RM 并发发送 prepare,等待最慢响应。每个 RM 刷盘 redo/undo 日志,耗时约 5ms,加上网络 RTT 2ms,总耗时约 7ms。
  5. 协调者写 Commit Decision 日志并 fsync:5ms。
  6. Phase 2 Commit:向两个 RM 并发发送 commit,RM 释放锁,网络 RTT 2ms + 轻微处理 1ms = 3ms。
    总计事务完成时间 ≈ 5 + 7 + 5 + 3 = 20ms。
    数据库锁持有时间:从 Prepare 开始到 Commit 完成,约 7 + 5 + 3 = 15ms。

锁竞争模型
对于库存热点行,数据库行锁在 Prepared 期间全程保持,同一时刻只能有一个事务修改该行。因此,对同一商品的请求完全串行化。
即使有多个商品,但热点商品(如秒杀品)的并发上限取决于锁持有时间。
最大 TPS ≈ 1 / 锁持有时间 ≈ 1 / 0.015s ≈ 66 TPS。
若平均有 10 个不同商品均匀分布,理论总 TPS 可达 660。但库存热点通常集中在少数商品,实际有效 TPS 可能低于 200。

连接池与等待

  • 连接池大小设为 50 时,大量线程等待锁,数据库连接数迅速打满,应用侧线程阻塞,响应时间急剧增大(超过 1 秒),产生超时风暴。
  • 事务超时(15s)可能触发,但此时全局事务回滚,库存扣减失败,用户体验差。

结论:在 1000 QPS 下,XA 方案因数据库长锁导致热点行严重串行化,系统在 200 QPS 左右即可能出现不可用征兆,距离 1000 QPS 差距巨大。

(3)启发式决策风险、故障推演、防范与应急响应

故障推演(详细步骤)

  1. 正常运行:TM 向 RM1(MySQL 订单)和 RM2(PostgreSQL 库存)发送 xa_prepare,均返回 XA_OK
  2. TM 崩溃:协调者进程被 kill -9 或机器断电,此时尚未写 Commit Decision 日志,也无 Phase 2 指令发出。
  3. RM 超时:MySQL 和 PostgreSQL 的事务超时设置为 30 秒,默认均不禁用启发式决策(危险配置)。30 秒后,MySQL 乐观地执行启发式提交(XA HEURISTIC COMMIT),订单记录生效;PostgreSQL 悲观地执行启发式回滚(XA HEURISTIC ROLLBACK),库存修改被撤销。
  4. TM 重启:协调者重新启动,扫描 tmlog 发现该事务无记录,随后调用 recover() 询问 RM。MySQL 返回 XA_HEURCOM,PostgreSQL 返回 XA_HEURRB
  5. 永久不一致:订单库存在一笔有效订单,但库存未扣减,产生超卖。

防范措施

  • 禁用启发式决策:在 RM 端配置无限超时(transaction-timeout=0 或极大值),宁可事务阻塞也不自动单方面决策。
    • MySQL: SET GLOBAL innodb_xa_heur_timeout=0;(或内核参数)
    • PostgreSQL: 不支持直接禁用,需通过协调者重试策略和监控覆盖。
  • 日志高可用:使用支持多写或基于 Raft 的共享日志存储(如 Atomikos 的数据库日志模式),确保协调者重启后能读取决策。
  • 协调者冗余:部署多个协调者实例,利用数据库行锁实现选主,接管 In-Doubt 事务的恢复。
  • 监控与告警:实时查询 XA RECOVER(MySQL)和 pg_prepared_xacts(PostgreSQL),若存在 Prepared 事务超过阈值时间,立即告警。

应急响应流程

  • 收到启发式异常告警后,DBA 立即冻结相关业务。
  • 通过 xa recover 获取两份数据,分析订单表和库存表的差异。
  • 若库存未扣减但订单存在,则手动补扣库存或取消订单(取决于业务决策)。
  • 调用 xa forget 清理 RM 中的启发式事务记录,协调者清理日志。
  • 复盘并修复超时配置,避免再次发生。

(4)QPS 增长到 5000 时的迁移必要性:XA vs Seata AT vs TCC

锁模型对比

维度XASeata ATTCC
数据库锁持有Prepare → Commit 全程持有行锁本地事务提交即释放锁无数据库长锁(业务预留资源)
全局锁无(依赖数据库锁)TC 管理的全局乐观锁无,由业务 try 接口控制
一致性强一致最终一致(本地提交→全局提交/回滚)最终一致(业务补偿)
TPS 能力百级千级~万级万级以上
侵入性无(驱动层)低(代理数据源,需 undo_log 表)高(需实现 try/confirm/cancel)

为什么 5000 QPS 必须迁移

  • XA 的长锁导致热点行串行化,200 QPS 即可能打满,5000 QPS 完全不可行。
  • Seata AT 将数据库锁在本地事务提交后释放,全局锁采用乐观争用,配合 TC 集群可支撑数千 QPS。
  • TCC 完全由业务控制,无数据库锁,可支撑极高并发,但编码成本高。
  • 对于订单-库存场景,Seata AT 是平衡一致性与性能的最佳选择:侵入性低,能快速迁移。

(5)从 XA 到 Seata AT 的详细迁移步骤与灰度方案

迁移架构图

flowchart LR
    subgraph XA_Phase[XA 阶段]
        Svc1[order-service<br/>Atomikos TM]
        DB1[(MySQL)]
        DB2[(PostgreSQL)]
        Svc1 -- XA 2PC --> DB1
        Svc1 -- XA 2PC --> DB2
    end
    
    subgraph Transition[灰度迁移]
        Proxy[流量网关 / 配置中心]
        Svc2_AT[order-service<br/>Seata AT 版本]
        SeataTC[Seata TC 集群]
        DB1_AT[(MySQL)]
        DB2_AT[(PostgreSQL)]
        UndoLog1[(undo_log 表)]
        UndoLog2[(undo_log 表)]
        
        Proxy -->|切流| Svc1
        Proxy -->|切流| Svc2_AT
        Svc2_AT -->|全局事务| SeataTC
        Svc2_AT -->|代理数据源| DB1_AT
        Svc2_AT -->|代理数据源| DB2_AT
        DB1_AT -.-> UndoLog1
        DB2_AT -.-> UndoLog2
    end
    
    style SeataTC fill:#c0e0c0,stroke:#308030

详细迁移步骤

  1. 环境准备

    • 部署 Seata TC 集群(至少 2 节点),使用数据库存储模式(store.mode=db),配置高可用。
    • 在订单库和库存库中创建 undo_log 表(Seata 提供标准建表脚本)。
    • 引入 spring-cloud-starter-alibaba-seata 依赖,配置 TC 地址。
  2. 代码改造

    • 移除 Atomikos 依赖及 spring-boot-starter-jta-atomikos,删除 tmlog 相关配置。
    • 将数据源配置改为标准 DataSource,并注册 Seata 的 DataSourceProxy,使其自动管理本地事务和 undo_log 写入。
    • 业务方法标注 @GlobalTransactional(timeoutMills=300000) 替代 @Transactional,开启 Seata 全局事务。
  3. AT 模式配置

    seata:
      tx-service-group: order-tx-group
      service:
        vgroup-mapping:
          order-tx-group: default
    
    @Configuration
    public class DataSourceProxyConfig {
        @Bean
        public DataSourceProxy dataSource(DataSource dataSource) {
            return new DataSourceProxy(dataSource);
        }
    }
    
  4. 灰度方案

    • 部署两套 order-service:一套保持 XA 版本(v1),一套为 Seata AT 版本(v2)。
    • 通过网关根据流量比例(如 1% → 5% → 20% → 100%)将请求路由到 v2。
    • 监控 Seata TC 控制台的全局事务提交/回滚率、锁冲突次数、平均耗时。
    • 如果出现数据不一致(如订单创建但库存未扣减),Seata 会通过 undo_log 自动补偿,监控补偿成功率。
    • 灰度过程中对比 XA 和 AT 版本的 TPS 和延迟,逐步提升 AT 流量。
  5. 切换与下线 XA

    • AT 版本稳定运行 7×24 小时后,将流量 100% 切至 AT。
    • 下线 XA 版本实例,回收相关持久化卷和配置。
    • 清理 Atomikos 日志目录和 In-Doubt 事务(若存在,手动处理)。
    • 持续观察 Seata TC 日志,确保无残留异常。

风险与回滚

  • 若 AT 模式出现严重性能问题或数据错误,立即通过网关切回 XA 版本,保障业务。
  • 准备回滚脚本:若 AT 产生脏数据,可利用 undo_log 历史记录反向修复,或使用数据库备份恢复。

文末速查表:XA 与 JTA 核心机制

接口/组件职责关键配置/参数故障恢复与 Seata AT 对比
XAResource.start启动事务分支Xid, TMNOFLAGS-AT 无显式 start,通过 GlobalTransactional 拦截器自动管理
XAResource.end结束分支Xid, TMSUCCESS--
XAResource.prepare预提交并持久化日志返回 XA_OK / XA_RDONLYRM 崩溃后可依据日志恢复AT 本地事务提交即完成“准备”,undo_log 承担补偿回滚
XAResource.commit提交事务Xid, onePhaseTM 重试AT 异步删除 undo_log,无持久锁
XAResource.rollback回滚事务XidTM 重试AT 回放 undo_log 进行补偿
TransactionManager管理全局事务生命周期JNDI 或 Spring Bean驱动恢复流程Seata 的 TM (Transaction Manager) 发起全局事务,TC 协调
UserTransaction应用显式事务边界begin/commit/rollback--
CoordinatorImp (Atomikos)2PC 协调,日志持久化tmlog 目录读日志重试Seata TC 集群协调,无文件日志单点
ObjectStore (Narayana)事务日志存储文件/数据库加载状态机重试Seata TC 使用数据库存储全局会话
JtaTransactionManager桥接 Spring 与 JTA自动配置-SeataAutoConfiguration 类似桥接
XA 锁数据库行锁,Prepared 到 Commit 持有事务超时配置In-Doubt 恢复AT 全局锁仅防脏写,数据库锁立即释放,TPS 3-5 倍

延伸阅读

  • 《X/Open CAE Specification - Distributed Transaction Processing: The XA Specification》
  • 《Java Transaction Design Strategies》 (InfoQ)
  • Atomikos Documentation: Architecture & Configuration
  • Narayana Documentation: ArjunaCore Overview
  • 《Designing Data-Intensive Applications》 Chapter 7 (Transactions) and Chapter 9 (Consistency and Consensus)

本文通过完整拆解 XA 规范、JTA 实现以及 Atomikos/Narayana 的内核架构,系统阐释了刚性分布式事务的原理、流程与致命缺陷。从 2PC 的完整时序到启发式决策的永久不一致,再到与 Seata AT 的性能对比,这些内容将为后续学习 AT、TCC、Saga 等柔性事务方案奠定坚实的认知基础。进入第 2 篇之前,请务必理解:XA 是分布式事务的理论基线,它的“刚”与“痛”,正驱动了整个社区向更高可用、更高性能的柔性方案演进。