mysql-xa事务处理
MySQL XA分布式事务
DTP分布式事务处理模型
DTP(Distributed transaction processing,分布式事务处理),X/open 组织定义的一套分布式事务标准,用来规范全局事务处理.
参考:https://en.wikipedia.org/wiki/Distributed_transaction
DTP模型规范:
- 应用程序(Application Program,简称AP):用于定义事务边界,并且在事务边界内对资源进行操作.
- 资源管理器(Resource Manager,简称RM,也称事务的参与者):如数据库,文件系统等,并提供资源访问的方式.
- 事务管理器(Transaction Manager,简称TM,也称事务协调者):负责分配事务唯一标识,监控事务的执行进度,并负责事务提交,回滚.
XA规范
文档: https://pubs.opengroup.org/onlinepubs/009680699/toc.pdf
MySQL XA
XA事务状态流转图:
一阶段提交: XA_IDLE-->commit one phase/rollback
二阶段提交: XA_IDLE-->XA_PARPARE-->commit/rollback
状态变化:
xa start
启动事务,XA事务处于ACTIVE
状态,可以执行事务SQL,注意处理active状态期间不能包含隐式提交;xa end
XA事务进入IDLE
状态,可以发xa prepare
或xa commit one phase
;xa prepare
XA事务处于PREPARED
状态,xa recover
列出处于该状态的所有XA事务;xa commit one phase
提交事务,终止事务;
PREPARED
状态可以发起,xa commit
提交并终止事务 或xa rollback
回滚并终止事务;
XA命令
名称 | 解释 |
---|---|
xa_start | 负责开启或恢复一个事务分支,并且管理xid到调用线程 |
xa_end | 负责取消当前线程与事务分支的关联 |
xa_prepare | 负责询问RM,是否准备好了提交事务分支 |
xa_commit | 负责通知RM提交事务分支 |
xa_rollback | 负责通知RM回滚事务分支 |
xa_recover | 负责列出需要恢复的XA事务分支 |
核心命令:
-- 1.开始
XA {START|BEGIN} xid [JOIN|RESUME]
-- 2. 执行一些业务SQL
-- 3. 结束SQL
XA END xid [SUSPEND [FOR MIGRATE]]
-- 4. 预提交
XA PREPARE xid
-- 5.1. 提交
XA COMMIT xid [ONE PHASE]
-- 5.2 回滚
XA ROLLBACK xid
-- 6. 查看PREPARE列表
XA RECOVER [CONVERT XID]
xid 唯一事务ID, 最大64 个字节.由事务管理器生成,只要确保在事务期间不会冲突就可以.
XA START
开启一个xa事务或者恢复一个事务.
语法:
XA {START|BEGIN} xid [JOIN|RESUME];
- 开启xid为'abc'的事务.
mysql> xa start 'abc';
Query OK, 0 rows affected (0.00 sec)
或
mysql> xa begin 'abc';
Query OK, 0 rows affected (0.00 sec)
- 不支持事务嵌套,包括普通事务和xa事务,不能处于事务上下文中.
mysql> xa start '123';
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> xa start '123';
ERROR 1400 (XAE09): XAER_OUTSIDE: Some work is done outside global transaction
- xid重复
mysql> xa start 'abc';
ERROR 1440 (XAE08): XAER_DUPID: The XID already exists
- 恢复
状态为IDLE(空闲)时可以通过xa start 'abc' resume
恢复为ACTIVE状态.
mysql> xa start 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1.t2(ID) values(200);
Query OK, 1 row affected (0.00 sec)
mysql> select * from test1.t2;
+------+
| ID |
+------+
| 100 |
| 200 |
+------+
2 rows in set (0.00 sec)
mysql> xa end 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1.t2;
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state
mysql> xa start 'abc' resume;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1.t2;
+------+
| ID |
+------+
| 100 |
| 200 |
+------+
2 rows in set (0.00 sec)
不为IDLE状态时,报错.
mysql> xa start 'abc' resume;
ERROR 1398 (XAE05): XAER_INVAL: Invalid arguments (or unsupported command)
XA END
结束一个XA事务.
语法:
XA END xid [SUSPEND [FOR MIGRATE]]
mysql> xa start 'abc' ;
Query OK, 0 rows affected (0.00 sec)
mysql> xa end 'abc';
Query OK, 0 rows affected (0.00 sec)
状态不是ACTIVE状态时报错.
mysql> xa end 'abc';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the NON-EXISTING state
已经end转成IDLE状态
mysql> xa end 'abc';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state
已经预提交
mysql> xa end 'abc';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the PREPARED state
不存在的XID
mysql> xa end '123';
ERROR 1397 (XAE04): XAER_NOTA: Unknown XID
不支持[SUSPEND [FOR MIGRATE] 语句
mysql> xa end 'abc' SUSPEND;
ERROR 1398 (XAE05): XAER_INVAL: Invalid arguments (or unsupported command)
XA Commit ONE PHASE
一阶段直接提交.
语法:
XA COMMIT xid ONE PHASE;
mysql> xa start 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1.t2;
+------+
| ID |
+------+
| 100 |
| 200 |
+------+
2 rows in set (0.00 sec)
mysql> insert into test1.t2(ID) values(300);
Query OK, 1 row affected (0.01 sec)
mysql> xa end 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> xa commit 'abc';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state
mysql> xa commit 'abc' one phase;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test1.t2;
+------+
| ID |
+------+
| 100 |
| 200 |
| 300 |
+------+
3 rows in set (0.00 sec)
XA ROLLBACK
回滚并终止事务.
处于'IDLE','PREPARED'状态可以回滚.
语法:
XA ROLLBACK xid;
mysql> xa start 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> xa end 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> xa rollback 'abc';
Query OK, 0 rows affected (0.00 sec)
未知XID
mysql> xa rollback 'abc';
ERROR 1397 (XAE04): XAER_NOTA: Unknown XID
Active状态
mysql> xa rollback 'abc';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
XA PREPARE
预提交,准备提交.会被XA RECOVER 列出来,断开连接恢复后可以rollback和commit.
语法:
XA PREPARE xid;
mysql> xa recover;
Empty set (0.00 sec)
mysql> xa start 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> xa end 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> xa recover;
Empty set (0.00 sec)
mysql> xa prepare 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> xa recover;
+----------+--------------+--------------+------------+
| formatID | gtrid_length | bqual_length | data |
+----------+--------------+--------------+------------+
| 1 | 3 | 0 | 0x616263 |
+----------+--------------+--------------+------------+
1 row in set (0.00 sec)
此时即使进程断开或服务重启,也可以操作xid.
锁定表,如果不commit或rollback,表一直被锁定状态.
XA COMMIT
二阶段提交和XA Commit ONE PHASE 结果一样.
需要在PREPARE状态下.
语法:
XA COMMIT xid;
mysql> xa start 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test1.t2(ID) values(600);
Query OK, 1 row affected (0.00 sec)
mysql> xa end 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'abc';
Query OK, 0 rows affected (0.00 sec)
mysql> xa commit 'abc';
Query OK, 0 rows affected (0.00 sec)
Active状态下报错
mysql> xa commit 'abc';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
未知XID
mysql> xa commit 'abc';
ERROR 1397 (XAE04): XAER_NOTA: Unknown XID
IDLE状态报错
mysql> xa commit 'abc';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state
可以跨进程commit
XA RECOVER
列出可恢复的xa事务,只有PREPARED状态.
语法:
XA RECOVER;
mysql> xa end 'test1';
Query OK, 0 rows affected (0.00 sec)
mysql> xa recover;
Empty set (0.00 sec)
mysql> xa prepare 'test1';
Query OK, 0 rows affected (0.00 sec)
mysql> xa recover;
+----------+--------------+--------------+-------+
| formatID | gtrid_length | bqual_length | data |
+----------+--------------+--------------+-------+
| 1 | 5 | 0 | test1 |
+----------+--------------+--------------+-------+
1 row in set (0.00 sec)
连接断开可以恢复.
mysql> xa rollback 'test1';
Query OK, 0 rows affected (0.00 sec)
mysql> xa commit 'test2';
Query OK, 0 rows affected (0.01 sec)
如果XID乱码,可以转16进制.
XA RECOVER CONVERT XID;
mysql>
mysql> xa start '\r123';
Query OK, 0 rows affected (0.01 sec)
mysql> xa end '\r123';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare '\r123';
Query OK, 0 rows affected (0.01 sec)
mysql> xa recover;
+----------+--------------+--------------+------+
| formatID | gtrid_length | bqual_length | data |
+----------+--------------+--------------+------+
123 | 1 | 4 | 0 |
+----------+--------------+--------------+------+
1 row in set (0.01 sec)
mysql> xa recover convert xid;
+----------+--------------+--------------+------------+
| formatID | gtrid_length | bqual_length | data |
+----------+--------------+--------------+------------+
| 1 | 4 | 0 | 0x0D313233 |
+----------+--------------+--------------+------------+
1 row in set (0.00 sec)
mysql> xa rollback 0x0D313233;
Query OK, 0 rows affected (0.00 sec)
Mysql5.7之前差异
mysql从5.0版本开始,innoDB存储支持XA协议. 5.7.7版本之后得到完善.
差异:
- 5.5.7之前客户端连接终止或服务器正常退出,则PREPARED事务将回滚.
之后的版本由用户选择commit或rollback;
5.5版本:
mysql> select version();
+-----------+
| version() |
+-----------+
| 5.5.62 |
+-----------+
1 row in set (0.00 sec)
mysql> xa start 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test.t1;
Empty set (0.00 sec)
mysql> insert into test.t1(id) values(100);
Query OK, 1 row affected (0.01 sec)
mysql> xa end 'test';
Query OK, 0 rows affected (0.01 sec)
mysql> xa prepare 'test';
Query OK, 0 rows affected (0.01 sec)
mysql> select * from test.t1;
ERROR 2013 (HY000): Lost connection to MySQL server during query
No connection. Trying to reconnect...
Connection id: 7
Current database: test
Empty set (0.02 sec)
mysql> xa recover;
Empty set (0.01 sec)
mysql> xa commit 'test';
ERROR 1397 (XAE04): XAER_NOTA: Unknown XID
8.0版:
mysql> select version();
+-----------+
| version() |
+-----------+
| 8.2.0 |
+-----------+
1 row in set (0.00 sec)
mysql> xa start 'test';
Query OK, 0 rows affected (0.01 sec)
mysql> select * from test.t1;
Empty set (0.00 sec)
mysql> insert into test.t1(id) values(100);
Query OK, 1 row affected (0.00 sec)
mysql> xa end 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'test';
Query OK, 0 rows affected (0.01 sec)
mysql> kill 14;
ERROR 1317 (70100): Query execution was interrupted
mysql> select * from test.t1;
ERROR 2013 (HY000): Lost connection to MySQL server during query
No connection. Trying to reconnect...
Connection id: 16
Current database: *** NONE ***
Empty set (0.03 sec)
mysql> xa recover;
+----------+--------------+--------------+------+
| formatID | gtrid_length | bqual_length | data |
+----------+--------------+--------------+------+
| 1 | 4 | 0 | test |
+----------+--------------+--------------+------+
1 row in set (0.00 sec)
mysql> xa commit 'test';
Query OK, 0 rows affected (0.01 sec)
mysql> select * from test.t1;
+------+
| id |
+------+
| 100 |
+------+
1 row in set (0.01 sec)
- prepare后异常后可以恢复到prepare状态.
可以继续xa commit和xa rollback
5.5:
mysql> show processlist;
+----+------+------------------+------+---------+------+-------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+------------------+------+---------+------+-------+------------------+
| 8 | root | 172.18.0.1:62930 | test | Query | 0 | NULL | show processlist |
+----+------+------------------+------+---------+------+-------+------------------+
1 row in set (0.01 sec)
mysql> xa start 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> xa end 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> kill 8;
ERROR 1317 (70100): Query execution was interrupted
mysql> xa commit 'test';
ERROR 2013 (HY000): Lost connection to MySQL server during query
No connection. Trying to reconnect...
Connection id: 9
Current database: test
ERROR 1397 (XAE04): XAER_NOTA: Unknown XID
8.0
mysql> show processlist;
+----+-----------------+------------------+------+---------+------+------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+------------------+------+---------+------+------------------------+------------------+
| 5 | event_scheduler | localhost | NULL | Daemon | 9081 | Waiting on empty queue | NULL |
| 17 | root | 172.18.0.1:63594 | NULL | Query | 0 | init | show processlist |
+----+-----------------+------------------+------+---------+------+------------------------+------------------+
2 rows in set, 1 warning (0.00 sec)
mysql> xa start 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> xa end 'test';
Query OK, 0 rows affected (0.01 sec)
mysql> xa prepare 'test';
Query OK, 0 rows affected (0.01 sec)
mysql> kill 17;
ERROR 1317 (70100): Query execution was interrupted
mysql> xa commit 'test';
ERROR 2013 (HY000): Lost connection to MySQL server during query
No connection. Trying to reconnect...
Connection id: 18
Current database: *** NONE ***
Query OK, 0 rows affected (0.04 sec)
- prepare后日志持久化
5.5 没生成binlog日志
mysql> xa start 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test.t1(id) values(100);
Query OK, 1 row affected (0.01 sec)
mysql> xa end 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW BINLOG events;
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
| mysql-bin.000001 | 4 | Format_desc | 1 | 107 | Server ver: 5.5.62-log, Binlog ver: 4 |
| mysql-bin.000001 | 107 | Query | 1 | 194 | create table test.t1(id INT) |
+------------------+-----+-------------+-----------+-------------+---------------------------------------+
2 rows in set (0.00 sec)
8.0 持久化日志,多了XA_prepare类型
mysql> xa start 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test.t1(id) values(100);
Query OK, 1 row affected (0.00 sec)
mysql> xa end 'test';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'test';
Query OK, 0 rows affected (0.01 sec)
mysql> SHOW BINLOG events;
+---------------+-----+----------------+-----------+-------------+--------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+---------------+-----+----------------+-----------+-------------+--------------------------------------+
| binlog.000001 | 4 | Format_desc | 1 | 126 | Server ver: 8.2.0, Binlog ver: 4 |
| binlog.000001 | 126 | Previous_gtids | 1 | 157 | |
| binlog.000001 | 157 | Anonymous_Gtid | 1 | 236 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| binlog.000001 | 236 | Query | 1 | 328 | XA START X'74657374',X'',1 |
| binlog.000001 | 328 | Table_map | 1 | 376 | table_id: 108 (test.t1) |
| binlog.000001 | 376 | Write_rows | 1 | 416 | table_id: 108 flags: STMT_END_F |
| binlog.000001 | 416 | Query | 1 | 506 | XA END X'74657374',X'',1 |
| binlog.000001 | 506 | XA_prepare | 1 | 546 | XA PREPARE X'74657374',X'',1 |
+---------------+-----+----------------+-----------+-------------+--------------------------------------+
8 rows in set (0.00 sec)
MySQL 8.0.29 开始支持分离事务
当
xa_detach_on_prepare
为ON时可以分离事务,事备进入prepared状态后不用等rollback或commit,就可以开启另一个事务.或在另一个进程rollback或commit;
嵌套XA事务:
# xa1 开始
mysql> xa start 'xa1';
Query OK, 0 rows affected (0.01 sec)
mysql> xa end 'xa1';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare 'xa1';
Query OK, 0 rows affected (0.01 sec)
# xa2事务开启
mysql> xa start 'xa2';
Query OK, 0 rows affected (0.00 sec)
mysql> xa end 'xa2';
Query OK, 0 rows affected (0.00 sec)
# xa2结束
mysql> xa rollback 'xa2';
Query OK, 0 rows affected (0.01 sec)
# xa1 结束
mysql> xa commit 'xa1';
Query OK, 0 rows affected (0.00 sec)
XA事务嵌套普通事务
mysql> xa start 'xa1';
Query OK, 0 rows affected (0.01 sec)
mysql> xa end 'xa1';
Query OK, 0 rows affected (0.01 sec)
mysql> xa prepare 'xa1';
Query OK, 0 rows affected (0.01 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> xa rollback 'xa1';
Query OK, 0 rows affected (0.00 sec)
XA PHP示例代码
单实例一阶段提交:
和普通事务一样.
$host1 = "127.0.0.1:13306";
$username = "root";
$password = "123456";
$dsn1="mysql:host=$host1;";
$rm1Conn = new PDO($dsn1, $username, $password);
// TM统计管理XID
$xaId=uniqid("");
try {
// XA 开始
$rm1Conn->exec("XA START '$xaId'");
$sql1 = "INSERT INTO test1.t1 values (100)";
$rm1Conn->exec($sql1);
$sql2 = "INSERT INTO test2.t1 values (200)";
$rm1Conn->exec($sql2);
// XA 结束
$rm1Conn->exec("XA END '$xaId'");
// 提交
$commitRet1=$rm1Conn->exec("XA COMMIT '$xaId' ONE PHASE");
if($commitRet1===false){
echo 'fail'.PHP_EOL;
}else{
echo 'ok'.PHP_EOL;
}
}catch (Exception $e){
echo $e->getMessage();
// 回滚
$rm1Conn->exec("XA ROLLBACK '$xaId'");
}
多实例2阶段提交:
$host1 = "127.0.0.1:13306";
$host2 = "127.0.0.1:23306";
$username = "root";
$password = "123456";
$dsn1="mysql:host=$host1;dbname=test";
$dsn2="mysql:host=$host2;dbname=test";
$rm1Conn = new PDO($dsn1, $username, $password);
$rm2Conn = new PDO($dsn2, $username, $password);
$xaId=uniqid("");
try {
// XA 开始
$rm1Conn->exec("XA START '$xaId'");
$sql1 = "INSERT INTO t1 values (201)";
$rm1Conn->exec($sql1);
$rm1Conn->exec("XA END '$xaId'");
$rm2Conn->exec("XA START '$xaId'");
$sql2 = "INSERT INTO t1 values (202)";
$rm2Conn->exec($sql2);
$rm2Conn->exec("XA END '$xaId'");
// XA 结束
// 1阶段提交
$commitRet1=$rm1Conn->exec("XA COMMIT '$xaId' ONE PHASE");
$commitRet2=$rm2Conn->exec("XA COMMIT '$xaId' ONE PHASE");
echo 'success';
}catch (Exception $e){
$rm1Conn->exec("XA ROLLBACK '$xaId'");
$rm2Conn->exec("XA ROLLBACK '$xaId'");
}
多实例2阶段提交:
<?php
declare(strict_types=1);
$host1 = "127.0.0.1:13306";
$host2 = "127.0.0.1:23306";
$username = "root";
$password = "123456";
$dsn1="mysql:host=$host1;dbname=test";
$dsn2="mysql:host=$host2;dbname=test";
$rm1Conn = new PDO($dsn1, $username, $password);
$rm2Conn = new PDO($dsn2, $username, $password);
// TM统计管理XID
$xaId=uniqid("");
try {
// XA 开始
$rm1Conn->exec("XA START '$xaId'");
$sql1 = "INSERT INTO t1 values (201)";
$rm1Conn->exec($sql1);
$rm1Conn->exec("XA END '$xaId'");
$rm2Conn->exec("XA START '$xaId'");
$sql2 = "INSERT INTO t1 values (202)";
$rm2Conn->exec($sql2);
$rm2Conn->exec("XA END '$xaId'");
// XA 结束
// 1阶段预提交
$preRet1=$rm1Conn->exec("XA PREPARE '$xaId'");
$preRet2=$rm2Conn->exec("XA PREPARE '$xaId'");
if($preRet1===false || $preRet2===false){
echo 'fail'.PHP_EOL;
throw new Exception('prepare error.');
}
// 2阶段提交
$commitRet1=$rm1Conn->exec("XA COMMIT '$xaId'");
$commitRet2=$rm2Conn->exec("XA COMMIT '$xaId'");
if($commitRet1===false || $commitRet2===false){
echo 'fail'.PHP_EOL;
}else{
echo 'ok'.PHP_EOL;
}
}catch (Exception $e){
echo $e->getMessage();
// 回滚
$rm1Conn->exec("XA ROLLBACK '$xaId'");
$rm2Conn->exec("XA ROLLBACK '$xaId'");
}
XA事务异常处理
不同阶段时的异常处理.
Active
,IDLE
异常时不影响数据一致性;处理方案:重试或不处理;Rollback
不一致时;处理方案:补偿rollback;Prepare
状态;处理方案:重试或补偿rollback;- 一阶段
ONE PHASE Commit
状态;失败重试,无法补偿commit.可以补偿SQL; - 二阶段
Commit
状态;重试,补偿commit或补偿SQL;
业务处理
- prepare失败为业务失败,统一补偿失败处理即回滚.
- 一阶段commit不作考虑.
- 二除段commit失败,不允许失败,继续重试commit最后进入人工处理.prepare成功后一般由于网络造成的失败.
补偿业务:
- 收集prepare阶段之后的xID.
- 延迟清空prepare失败的xID.
- 重试commit阶段的xID.
- 重试失败后进入预警,进入人工处理.
原作者:阿金
本文地址:https://hi-arkin.com/archives/mysql-xa.html