bigywater.tech
Java · 320

JTA 两阶段提交:跨资源事务如何协调

跨两个数据库的事务怎么做到「要么都成功,要么都失败」?两阶段提交里 prepare 之后协调者挂了会怎样?这篇用 PostgreSQL 与 MySQL 的 XA 接口亲手跑一遍,包括崩溃与恢复。

55P03 / 40001
in-doubt 事务握着行锁时,另一个会话在 PostgreSQL / MySQL 里更新同一行得到的错误
PG 60 | MySQL 140
崩溃后用 recover() + commit 恢复的最终余额
0.19 vs 0.61 ms
单库本地事务与两库 XA 两阶段每次提交的耗时

准备

实测 · 本机执行输出
初始:PG a=100 | MySQL b=100

正常的两阶段提交

实测 · 本机执行输出
初始:PG a=100 | MySQL b=100
1 两个分支都执行完 UPDATE 并 end(TMSUCCESS) 后,第三方连接看到的余额(分支未提交):PG a=100 | MySQL b=100
  PostgreSQL prepare → XA_OK
  MySQL prepare → XA_OK
  prepare 之后、commit 之前:PG a=100 | MySQL b=100;PG 的 pg_prepared_xacts:1 个: 4242_dHgtaGFwcHk=_cGc=;MySQL XA RECOVER 行数:1
  两个分支 commit(二阶段)之后:PG a=70 | MySQL b=130

只读分支与一阶段

实测 · 本机执行输出
2 只读分支 PostgreSQL prepare → XA_OK(规范允许只读分支返回 XA_RDONLY = 3 跳过第二阶段;这两个驱动的只读分支返回的是 XA_OK)
2 只读分支 MySQL prepare → XA_OK(规范允许只读分支返回 XA_RDONLY = 3 跳过第二阶段;这两个驱动的只读分支返回的是 XA_OK)
  prepare 之后 PG 的 pg_prepared_xacts 里留了多少个:1;MySQL XA RECOVER 行数:1
  commit 之后 PG 的 pg_prepared_xacts:0;MySQL XA RECOVER 行数:0
3 单资源一阶段提交 commit(xid, onePhase=true):成功(没有 prepare)

prepare 失败

实测 · 本机执行输出
4 初始:PG a=100 | MySQL b=100
  第一阶段:MySQL 先 prepare → XA_OK
  PG prepare 失败(延迟的唯一约束在 PREPARE 时检查):PGXAException(errorCode=103): Error preparing transaction. prepare xid=tx-pf/pg
  协调者决定回滚:MySQL 分支 rollback
  PG 分支 rollback:PGXAException(errorCode=-3): Error rolling back prepared transaction. rollback xid=tx-pf/pg, preparedXid=tx-pf/pg, currentXid=null
  结果:PG a=100 | MySQL b=100;PG 残留 prepared 事务:0;MySQL XA RECOVER 行数:0

协调者崩溃与恢复

实测 · 本机执行输出
5 两个分支都 prepare 成功;协调者在发 commit 之前崩溃(Runtime.halt):PG a=100 | MySQL b=100
  (崩溃进程退出码 137)
6 协调者崩溃后(新进程):PG a=100 | MySQL b=100;PG pg_prepared_xacts:4242_dHgtY3Jhc2g=_cGc=;MySQL XA RECOVER:[formatID=4242,gtrid_length=8,bqual_length=2,data=0x74782D63726173686D79]
  另一个会话更新 PG 里这一行(被 in-doubt 事务锁住):PSQLException(55P03): ERROR: canceling statement due to lock timeout
  另一个会话更新 MySQL 里这一行(被 in-doubt 事务锁住):MySQLTransactionRollbackException(40001): Lock wait timeout exceeded; try restarting transaction
  PostgreSQL recover() 找到 1 个 in-doubt 分支:[tx-crash/pg(format 4242)]
  MySQL recover() 找到 1 个 in-doubt 分支:[tx-crash/my(format 4242)]
  恢复:对两个资源的 in-doubt 分支都 commit 之后:PG a=60 | MySQL b=140;PG 残留:0;MySQL XA RECOVER 行数:0

代价

实测 · 本机执行输出
7 300 次提交:PG 单库本地事务(复用同一连接)58 ms(0.19 ms/次);两个库各自本地事务、依次提交(不是原子的)106 ms(0.35 ms/次);两个库 XA 两阶段 182 ms(0.61 ms/次)

分步演示

JTA 两阶段提交
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;自己起的 PostgreSQL 16.15(max_prepared_transactions=20,端口 9501)与 MySQL 26.7.0(端口 9601),用户自己的数据库没有被使用;pgjdbc 42.7.4、mysql-connector-j 9.1.0;协调者是我自己写的最小实现(没有事务日志),不是 Narayana / Atomikos 这样的完整事务管理器
    • 没有验证:完整事务管理器的行为(事务日志、自动恢复线程、超时、启发式结果 heuristic commit / rollback)、Spring / Jakarta EE 容器里的 JTA 集成、网络分区、超过两个资源、XA 的并发性能(只测了单线程)、MySQL XA 与二进制日志的交互
    京ICP备2026067502号-1京公网安备11010502063487号