MySQL对分布式事务(XA Transactions)的支持

摘要:MySQL对分布式事务(XA Transactions)进行了很好的支持,我们看看它是怎么做的,并实战验证其提供的分布式事务控制语句效果。

MySQL从5.0.3开始,InnoDB存储引擎支持XA事务(XA Transactions)。MySQL XA是基于X/Open CAE文档中的Distributed Transaction Processing:The XA Specification(DTP XA规范)实现的。

  • X/Open是一个独立的、全球性的开放系统组织,由世界上最大的信息系统供应商、用户组织和软件公司支持。其使命是通过开放系统的实际实施,为用户带来更大的计算价值。
  • X/Open CAE规范,即X/Open Common Applications Environment,这个环境覆盖了高于硬件级别的,支持开放系统所需的一组标准。它提供了应用程序的可移植性和互操作性。

一个分布式事务会涉及多个行动,这些行动本身是事务性的。所有行动都必须一起成功完成,或者一起被回滚。

在MySQL中,使用分布式事务涉及一个或多个资源管理器和一个事务管理器。

  • 资源管理器(RM)用于提供通向事务资源的途径。数据库服务器是一种资源管理器,该管理器必须可以提交或者回滚由RM管理的事务。
  • 事务管理器(TM)用于协调作为一个分布式事务一部分的事务。TM与管理每个事务的RMs进行通信。在一个分布式事务中,各个单个事务均是分布式事务的“分支事务”。分布式事务和各分支通过一种命名方法进行标识。

执行XA事务时,MySQL服务器相当于一个用于管理分布式事务的XA事务资源管理器。与MySQL服务器连接的客户端相当于事务管理器。

要执行一个分布式事务,必须知道这个分布式事务涉及了哪些资源管理器,并且把每个资源管理器的事务执行到事务可以被提交或者回滚。根据每个资源管理器报告的执行情况,这些分支事务必须作为一个原子性操作全部提交或回滚。要管理一个分布式事务,必须考虑任务组件或者网络可能出现故障。

用于执行分布式事务的过程称为两阶段提交,发生时间在由分布式事务的各个分支需要进行的行动已经被执行之后。

  • 在第一阶段,所有的分支被预备好。即它们被TM告知要准备提交。
  • 在第二阶段,TM告知RMs是否要提交或回滚。如果在预备分支时,所有的分值指示它们将能够提交,则所有的分支被告知要提交。如果在预备时,有任何分支指示它将不能提交,则所有分支被告知回滚。

有些情况下,一个分布式事务可能使用一阶段提交。比如,当一个事务管理器发现,一个分布式事务只由一个事务资源组成(即一个分支),则资源可以被告知同时进行预备和提交。

在MySQL 5.7中,分布式事务的语法如下:

XA {START|BEGIN} xid [JOIN|RESUME]

XA END xid [SUSPEND [FOR MIGRATE]]

XA PREPARE xid

XA COMMIT xid [ONE PHASE]

XA ROLLBACK xid

XA RECOVER [CONVERT XID]

 

每个分布式事务语句都以XA关键字开头,并且大多数语句都需要xid值。xid是XA事务标识符,它表示语句操作哪个事务。xid值由客户端提供,或由MySQL服务器生成。xid值由1到3部分组成:

xid: gtrid [, bqual [, formatID ]]

 

  • gtrid是全局事务标识符。
  • bqual是分支限定符,默认值是’’,对于分布式事务中的每个分支事务,bqual值必须是唯一的。
  • formatID是标识gtrid和bqual值使用的格式,默认值为1。

XA START xid使用给定的xid值启动XA事务。每个XA事务必须具有唯一的xid值,该值当前不能由另一个XA事务使用。其它命令就是分别实现不同的XA事务控制,参考下图:
image_editor_3bf2704c-c17a-4833-93a1-dc42c7331206

XA RECOVER返回当前数据库中处于PREPARE状态的分支事务的详细信息。

分布式的关键在于如何确保分布式事务的完整性,以及在某个分支出现问题时的故障解决。XA的相关命令就是提供给应用在多个独立的数据库之间进行分布式事务的管理。

我们人工模拟个场景,实战下MySQL对分布式事务语句的支持。假设肖杰在某银行有一个储蓄账户和一个理财账户,储蓄账户的相关信息被存储在cash数据库中,理财账户相关信息被存储在financing数据库中,他现在要从储蓄账户往理财账户转3000块钱,本质上就是在两个数据库中更新记录,但两个操作需作为同一个事务提交或回滚。

创建基础环境:

root@database-one 21:19:  [(none)]> create database cash_gftest default charset utf8;
Query OK, 1 row affected (0.02 sec)

root@database-one 21:25:  [(none)]> use cash_gftest;
Database changed
root@database-one 21:25:  [cash_gftest]> create table cash_account(name varchar(10),balance decimal(10,2)) engine=innodb;
Query OK, 0 rows affected (0.05 sec)

root@database-one 21:25:  [cash_gftest]> insert into cash_account values('郭佳',10000),('刘强',8000),('肖杰',22000);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

root@database-one 21:29:  [cash_gftest]> create database financing_gftest default charset utf8;
Query OK, 1 row affected (0.00 sec)

root@database-one 21:29:  [cash_gftest]> use financing_gftest
Database changed
root@database-one 21:29:  [financing_gftest]> create table financing_account(name varchar(10),balance decimal(10,2)) engine=innodb;
Query OK, 0 rows affected (0.08 sec)

root@database-one 21:30:  [financing_gftest]> insert into financing_account values('郭佳',0),('刘强',0),('肖杰',0);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

root@database-one 21:30:  [financing_gftest]> select * from financing_gftest.financing_account;
+--------+---------+
| name   | balance |
+--------+---------+
| 郭佳   |    0.00 |
| 刘强   |    0.00 |
| 肖杰   |    0.00 |
+--------+---------+
3 rows in set (0.00 sec)

root@database-one 21:31:  [financing_gftest]> select * from cash_gftest.cash_account;
+--------+----------+
| name   | balance  |
+--------+----------+
| 郭佳   | 10000.00 |
| 刘强   |  8000.00 |
| 肖杰   | 22000.00 |
+--------+----------+
3 rows in set (0.00 sec)

 

创建会话1,连入cash数据库,模拟储蓄账户应用程序。

root@database-one 21:36:  [(none)]> prompt \u@database-one \R:\m:\s [\d] session1>
PROMPT set to '\u@database-one \R:\m:\s [\d] session1>'
root@database-one 21:36:33 [(none)] session1>use cash_gftest;
Database changed
root@database-one 21:36:49 [cash_gftest] session1>select * from cash_account;
+--------+----------+
| name   | balance  |
+--------+----------+
| 郭佳   | 10000.00 |
| 刘强   |  8000.00 |
| 肖杰   | 22000.00 |
+--------+----------+
3 rows in set (0.01 sec)

 

创建会话2,连入financing数据库,模拟理财账户应用程序。

root@database-one 21:38:  [(none)]> prompt \u@database-one \R:\m:\s [\d] session2>
PROMPT set to '\u@database-one \R:\m:\s [\d] session2>'
root@database-one 21:38:38 [(none)] session2>use financing_gftest;
Database changed
root@database-one 21:38:50 [financing_gftest] session2>select * from financing_account;
+--------+---------+
| name   | balance |
+--------+---------+
| 郭佳   |    0.00 |
| 刘强   |    0.00 |
| 肖杰   |    0.00 |
+--------+---------+
3 rows in set (0.00 sec)

 

肖杰开始转账,在cash库中启动一个分布式事务的一个分支,xid的gtrid为“transfer_of_account”,bqual为“cash”

root@database-one 21:45:09 [cash_gftest] session1>xa start 'transfer_of_account','cash';
Query OK, 0 rows affected (0.01 sec)

root@database-one 21:45:43 [cash_gftest] session1>update cash_account set balance=balance-3000 where name='肖杰';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

root@database-one 21:46:50 [cash_gftest] session1>xa end 'transfer_of_account','cash';
Query OK, 0 rows affected (0.01 sec)

root@database-one 21:48:17 [cash_gftest] session1>xa prepare 'transfer_of_account','cash';
Query OK, 0 rows affected (0.00 sec)

 

可以看到在对肖杰储蓄账户减去3000元后,将分支事务“cash”进行第一阶段提交,进入prepare状态。

在financing库中启动分布式事务“transfer_of_account”的另一个分支,bqual为“financing”

root@database-one 21:54:24 [financing_gftest] session2>xa start 'transfer_of_account','financing';
Query OK, 0 rows affected (0.00 sec)

root@database-one 21:54:51 [financing_gftest] session2>update financing_account set balance=balance+3000 where name='肖杰';
Query OK, 1 row affected (0.06 sec)
Rows matched: 1  Changed: 1  Warnings: 0

root@database-one 21:55:28 [financing_gftest] session2>xa end 'transfer_of_account','financing';
Query OK, 0 rows affected (0.02 sec)

root@database-one 21:56:03 [financing_gftest] session2>xa prepare 'transfer_of_account','financing';
Query OK, 0 rows affected (0.02 sec)

 

可以看到在对肖杰理财账户增加3000元后,将分支事务“financing”进行第一阶段提交,进入prepare状态。

分别在两个库中用xa recover查看当前分支事务状态

root@database-one 22:04:50 [cash_gftest] session1>xa recover;
+----------+--------------+--------------+------------------------------+
| formatID | gtrid_length | bqual_length | data                         |
+----------+--------------+--------------+------------------------------+
|        1 |           19 |            4 | transfer_of_accountcash      |
|        1 |           19 |            9 | transfer_of_accountfinancing |
+----------+--------------+--------------+------------------------------+
2 rows in set (0.00 sec)

 

root@database-one 22:06:05 [financing_gftest] session2>xa recover;
+----------+--------------+--------------+------------------------------+
| formatID | gtrid_length | bqual_length | data                         |
+----------+--------------+--------------+------------------------------+
|        1 |           19 |            4 | transfer_of_accountcash      |
|        1 |           19 |            9 | transfer_of_accountfinancing |
+----------+--------------+--------------+------------------------------+
2 rows in set (0.01 sec)

 

两个会话都能看到两个分支事务的信息,这是因为这两个库是在同一个MySQL服务中,如果是位于不同的MySQL服务中,将只能看到各自的分支事务信息。

如果某个分支事务在进入到prepare之前,遇到任何错误,就可以回滚其它分支事务,确保分布式事务的正确。

我们这里操作正常,对所有分支事务均提交,彻底完成整个转账动作。

root@database-one 22:12:43 [cash_gftest] session1>xa commit 'transfer_of_account','cash';
Query OK, 0 rows affected (0.01 sec)

 

root@database-one 22:13:50 [financing_gftest] session2>xa commit 'transfer_of_account','financing';
Query OK, 0 rows affected (0.00 sec)

 

通过验证可以发现,MySQL确实很好的实现了对分布式事务的支持。

© 2020, morinson. 版权所有. 欢迎转载,但请保留作者及出处。