bigywater.tech
分布式事务 · 5

XA 事务:MySQL 的 XA 语句实验

XA 是数据库对两阶段提交的标准接口。MySQL 的 XA START、XA END、XA PREPARE、XA COMMIT 各自处于什么状态?连接断开、实例崩溃后 prepared 事务去哪了?代价多大?这篇用真实的 MySQL 26.7 逐条试出来。

6.02 次
跨两个 MySQL 实例的一次 XA 事务引起的 redo fsync(普通事务是 1.01 次)
3.4 倍
跨两个实例的 XA 提交延迟中位数相对普通事务
611.1 ms
prepare 之后 kill -9 的 mysqld 重启用时,重启后 XA RECOVER 里还有这个事务

XA 的状态机

实测 · 本机执行输出
  mysql> XA START 'x1'   -> OK
  mysql> INSERT INTO orders VALUES (1, 'CREATED')   -> OK
  mysql> COMMIT   -> 错误:OperationalError 1399 XAER_RMFAIL: The command cannot be executed when global transaction is in the  ACTIVE state
  mysql> XA PREPARE 'x1'   -> 错误:OperationalError 1399 XAER_RMFAIL: The command cannot be executed when global transaction is in the  ACTIVE state
  mysql> XA END 'x1'   -> OK
  mysql> INSERT INTO orders VALUES (2, 'CREATED')   -> 错误:OperationalError 1399 XAER_RMFAIL: The command cannot be executed when global transaction is in the  IDLE state
  mysql> XA PREPARE 'x1'   -> OK
  mysql> SELECT 1   -> ((1,),)
  另一个会话此时看到的订单数: 0 (prepare 了但还没 commit,不可见)
  XA RECOVER: ((1, 2, 0, 'x1'),)
  mysql> XA COMMIT 'x1'   -> OK
  提交后订单数: 1 ,XA RECOVER: ()

XA START 之后是 ACTIVE,不能直接 COMMIT 或 PREPARE;XA END 之后是 IDLE,不能再写;XA PREPARE 之后才可以被任何会话 XA COMMIT 或 XA ROLLBACK。XA RECOVER 只给 xid,没有 prepare 了多久。

prepare 前后连接的作用

实测 · 本机执行输出
  XA START;INSERT;XA END 之后断开连接:XA RECOVER = () ,订单数 = 0 (没 prepare 的 XA 事务随连接一起回滚)
  XA PREPARE 之后断开连接:XA RECOVER = ((1, 2, 0, 'y2'),) ,订单数 = 0
  mysql> XA COMMIT 'y2'   -> OK
  换一个连接 XA COMMIT 之后订单数 = 1

没有 prepare 的 XA 事务随连接一起回滚;prepare 之后事务属于服务端,断开连接后换一个连接就能收尾。

prepared 事务的锁

实测 · 本机执行输出
  别的会话 SELECT qty: 100 (旧值)
  别的会话 UPDATE 同一行:OperationalError 1205 Lock wait timeout exceeded; try restarting transaction,等了 2004.2 ms
  information_schema.innodb_trx(trx_state, trx_mysql_thread_id, trx_rows_locked, trx_started 距现在多少秒): [('RUNNING', 0, 1, 2)] —— 这个 prepared 的事务在里面显示成 RUNNING,线程号 0(已经没有连接了)

行锁一直持有:读不被挡,写同一行等到 innodb_lock_wait_timeout。在 information_schema.innodb_trx 里它显示成 RUNNING、线程号 0,不容易认出来。

崩溃恢复

实测 · 本机执行输出
  kill -9 mysqld(此时 k1 已 PREPARE,k2 还在 ACTIVE)
  重启用时 611.1 ms
  重启后 XA RECOVER: ((1, 2, 0, 'k1'),) ,订单数(k2 的插入): 0
  重启后别的会话 UPDATE 同一行: OperationalError 1205 Lock wait timeout exceeded; try restarting transaction (行锁在恢复时被重新持有)
  mysql> XA COMMIT 'k1'   -> OK
  XA COMMIT 之后 qty = 99
  binlog 里的 XA 相关事件:
    ('binlog.000001', 'Query', "XA START X'7831',X'',1")
    ('binlog.000001', 'Query', "XA END X'7831',X'',1")
    ('binlog.000001', 'XA_prepare', "XA PREPARE X'7831',X'',1")
    ('binlog.000001', 'Query', "XA COMMIT X'7831',X'',1")
    ('binlog.000001', 'Query', "XA START X'7932',X'',1")
    ('binlog.000001', 'Query', "XA END X'7932',X'',1")
    ('binlog.000001', 'XA_prepare', "XA PREPARE X'7932',X'',1")
    ('binlog.000001', 'Query', "XA COMMIT X'7932',X'',1")
    ('binlog.000001', 'Query', "XA START X'7a31',X'',1")
    ('binlog.000001', 'Query', "XA END X'7a31',X'',1")
    ('binlog.000001', 'XA_prepare', "XA PREPARE X'7a31',X'',1")
    ('binlog.000001', 'Query', "XA ROLLBACK X'7a31',X'',1")
    ('binlog.000001', 'Query', "XA START X'6b31',X'',1")
    ('binlog.000001', 'Query', "XA END X'6b31',X'',1")
    ('binlog.000001', 'XA_prepare', "XA PREPARE X'6b31',X'',1")
    ('binlog.000002', 'Query', "XA COMMIT X'6b31',X'',1")
  mysqld 错误日志里和 XA / prepared 有关的行:
    [Server] Starting XA crash recovery...
    [Server] Found 1 prepared XA transactions
    [Server] XA crash recovery finished.

prepare 之后 mysqld 被 kill -9,重启后 XA RECOVER 里还在、锁被恢复,没 prepare 的 XA 事务消失。binlog 里有 XA_prepare 事件。

代价

实测 · 本机执行输出
  写法                            | 中位数    | p99    | 每个事务的 redo fsync(两个实例合计) | 相对普通事务
  ------------------------------+--------+--------+--------------------------+-------
  普通事务(一个实例)                    | 0.2 ms | 0.4 ms | 1.01                     | 1.0x  
  XA(同一个实例,走完 prepare + commit) | 0.3 ms | 0.7 ms | 3.00                     | 1.8x  
  XA(跨两个实例)                     | 0.6 ms | 1.7 ms | 6.02                     | 3.4x  
  redo fsync 来自 Innodb_os_log_fsyncs;binlog 的 fsync(sync_binlog=1)没有计在内,因为没有对应的状态变量
  一个事务里每条语句平均引起几次 redo fsync(Innodb_os_log_fsyncs 的增量,单个实例,各重复 200 个事务取平均;单次测量会被后台刷盘线程干扰,BEGIN 这类不写日志的语句也会量到约 0.1 的本底):
   普通事务:BEGIN 0.11,  INSERT 0.10,  UPDATE 0.09,  COMMIT 1.08  合计 1.38
   XA 事务:  XA START 0.04,  INSERT INTO 0.08,  UPDATE stock 0.10,  XA END 0.12,  XA PREPARE 1.10,  XA COMMIT 2.06  合计 3.49

XA 提交 = XA PREPARE 约 1 次 + XA COMMIT 约 2 次 redo fsync,单个实例就是 3 次,跨两个实例 6 次,中位数 3.4 倍。

分步演示

MySQL XA
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MySQL 26.7.0 与 PostgreSQL 16.15(Homebrew,每个实验自己 initdb、自己起实例,端口 9150 以上,只监听 127.0.0.1)/ Python 3.14.8 + PyMySQL 1.2.3 + psycopg 3.3.6 / Redis 8.10.2 / Kafka 4.3.1 / RabbitMQ 4.3.6 / etcd 3.7.2(按各篇注明用到哪些);不连任何外部服务,不碰本机已有的数据库实例;故障用 kill -9 进程、让连接断开或在代码里的指定位置抛异常来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用两个独立的 MySQL 26.7.0 实例(binlog_format=ROW,sync_binlog=1,innodb_flush_log_at_trx_commit=1,innodb_lock_wait_timeout 在实验里设成 2 秒),客户端是 PyMySQL;redo fsync 数来自 Innodb_os_log_fsyncs 状态变量,不含 binlog 的 fsync;MySQL 在 macOS 上的 fsync 很便宜,倍数比绝对值更有参考价值
    • 没有验证:XA COMMIT 为什么引起约 2 次 redo fsync(没有深究);MySQL 的 XA 在主从复制下的行为;Java 里 Atomikos / Narayana 这类事务管理器怎么驱动 XA;长时间悬挂的 prepared 事务对 binlog 清理和复制的影响;PostgreSQL、MySQL 以外的数据库的 XA 实现
    京ICP备2026067502号-1京公网安备11010502063487号