跨两个数据库的事务怎么做到「要么都成功,要么都失败」?两阶段提交里 prepare 之后协调者挂了会怎样?这篇用 PostgreSQL 与 MySQL 的 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)
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/次)