这才是真正的分布式事务 XA 详解

2,120 阅读7分钟

1.概述

随着微服务架构变得流行,分布式事务成了后台开发者需要了解和掌握的技能。

分布式事务: 简单的说,就是一个的操作由不同的小操作组成,这些小的操作分布在不同的数据库服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

在分布式事务的文章和书籍中一定是要花很多笔墨阐述 XA 和两段式提交(2PC)。但是基本上都是从概念层级做了说明。比如在两段式提交的准备阶段,需要参数事务的各个数据库准备分支事务、记录事务日志、并告知事务管理器准备结果。但是对于准备什么说的并不是很明白。本文将以 MySQL 的XA 实现作为例子来做个专门的说明。

2.XA

XA 是分布式事务控制协议,由Tuxedo首先提出的,并交给 X/Open 组织。Oracle、MySQL 等主流数据库都提供了对XA的支持。X/Open是一个独立的、全球性的开放系统组织,由世界上最大的信息系统供应商、用户组织和软件公司支持。其使命是通过开放系统的实际实施,为用户带来更大的计算价值。

XA 协议定义了三个参与角色,如图 image.png

  • AP(Application Program) :即需要支持分布式事务的应用程序。
  • RM(Resource Manager):即资源管理器,是事务的参与者,一般情况下是指一个数据库实例。AP 通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
  • TM(Transaction Manager) :事务管理器,负责协调和管理全局事务,控制着全局事务生命周期,并协调各个 RM。

3. MySQL XA 实现

XA 相关的操作比较简单

#开启RM 分支事务
XA {START|BEGIN} xid [JOIN|RESUME] 
#当应用完成一个RM 中的业务数据操作时调用 XA End 标记应用完成,这个时候应用的数据库连接就可以释放了,RM 的分支事务还在
XA END xid [SUSPEND [FOR MIGRATE]] XA PREPARE xid
#RM 响应TM 的事务提交准备指令。
XA PREPARE xid
#RM 提交分支事务
XA COMMIT xid [ONE PHASE]
#RM 回滚分支事务
XA ROLLBACK xid
#查询分支事务信息
XA RECOVER [CONVERT XID]

每个分布式事务语句都以XA关键字开头,并且大多数语句都需要 xid。xid 是 XA 事务标识符,它标识语句操作哪个事务。xid 由客户端提供。xid 由三个部分组成:

xid: gtrid [, bqual [, formatID ]]
  • gtrid 是全局事务标识符。
  • bqual是分支限定符,非必需,默认值是''。
  • formatID是标识gtrid和bqual值使用的格式,非必需,默认值为1。

XA 的状态机如图: image.png

我们通过一个虚拟场景实战演练,测试 MySQL 对分布式事务语句的支持。假设 Tom 在某银行有一个储蓄账户和一个理财账户,储蓄账户的相关信息被存储在 cash 数据库中,理财账户相关信息被存储在 investment 数据库中。Tom 要从储蓄账户往理财账户转30000块钱,本质上就是在两个数据库中更新记录,但两个操作需在同一个事务中提交或回滚。

3.1 初始化测试环境

创建两个MySQL 数据库 cash 和 investment。

create database db_cash default charset utf8;
use db_cash;
create table cash_account(name varchar(10),balance decimal(10,2)) engine=innodb;
insert into cash_account values('Tom',210000);

create database db_investment default charset utf8;
use db_investment;
create table investment(name varchar(10),balance decimal(10,2)) engine=innodb;
insert into investment values('Tom',0);

3.2 储蓄微服务的数据库操作

创建会话1,登录 db_cash 数据库,模拟储蓄微服务对数据库的操作。db_cash 在整个全局事务中是一个 RM。在 db_cash 库中启动一个分布式事务的一个分支,xid 的 gtrid为 “transfer_of_account”,bqual为“cash”

use db_cash;
# 最终的 xid 为 transfer_of_accountcash。分支事务进入 Active 状态
xa start 'transfer_of_account','cash';
# 分支事务的业务数据操作
update cash_account set balance=balance-30000 where name='Tom';
# 将分支事务置于IDLE状态,表示事务内的SQL操作完成。
xa end 'transfer_of_account','cash';
# 分支事务提交准备工作,事务状态置于 PREPARED 状态。如果无法完成提交前的准备操作,该语句会执行失败
xa prepare 'transfer_of_account','cash';

当储蓄 RM 收到TM 发送的 prepare 指令的时候,RM 执行 xa prepare 语句触发分支事务的准备工作,事务状态置于 PREPARED 状态。如果无法完成提交前的准备操作,该语句会执行失败。准备工作包括写本地的 Undo/Redo 日志,获取锁等。这个时候表cash_account 处于锁定状态,其他数据库 session 更新操作都会处于等待锁状态(如果没有主键,整个表处于锁定状态,如果有主键,则更新的记录处于锁定状态)。此时分支事务没有提交,对于分布式事务中的一个数据库来说,与单机事务差不多,处于已经执行DML,还没有执行commit/rollback 语句。只是多了一些用于控制全局XA 事务的信息。。(Undo 日志是记录修改前的数据,用于数据库回滚,Redo 日志是记录修改后的数据,用于提交事务后写入数据文件)。 可以通过命令查看XA 事务信息

08:28:28 [db_cash] session1>xa recover;
+----------+--------------+--------------+-------------------------+
| formatID | gtrid_length | bqual_length | data                    |
+----------+--------------+--------------+-------------------------+
|        1 |           19 |            4 | transfer_of_accountcash |
+----------+--------------+--------------+-------------------------+

这个时候 RM 储蓄数据库处于第一阶段

3.3 理财微服务的数据库操作

创建会话2,登录 db_investment 数据库,模拟理财微服务对数据库的操作。db_investment 在整个全局事务中是一个 RM。在 db_investment 库中启动一个分布式事务的一个分支,xid 的 gtrid为 “transfer_of_account”,bqual为“investment”

use db_investment;
# 最终的 xid 为 transfer_of_accountcash。分支事务进入 Active 状态
xa start 'transfer_of_account','investment';
# 分支事务的业务数据操作
update investment set balance=balance+3000 where name='Tom';
# 分支事务进入 IDLE 状态
xa end 'transfer_of_account','investment';
# 分支事务进入 PREPARED 状态
xa prepare 'transfer_of_account','investment';

与储蓄 RM 类似,理财 RM 收到 TM 的准备指令,RM 执行 xa prepare 语句触发分支事务的准备工作,事务状态置于 PREPARED 状态。 可以通过命令查看XA 事务信息。可以看到现在有两个 XA 事务信息,由于我们是在一个物理数据库上创建的两个逻辑数据库,MySQL 的 XA 信息是记录在物理数据库维度的。

08:33:28 [db_investment] session2>xa recover;
+----------+--------------+--------------+-------------------------------+
| formatID | gtrid_length | bqual_length | data                          |
+----------+--------------+--------------+-------------------------------+
|        1 |           19 |           10 | transfer_of_accountinvestment |
|        1 |           19 |            4 | transfer_of_accountcash       |
+----------+--------------+--------------+-------------------------------+

3.3 两阶段提交

当TM 收到两个 RM 成功完成准备反馈后。TM发出提交指令。两个RM 执行xa commit 指令。

08:38:21 [db_cash] session1>xa commit 'transfer_of_account','cash';
Query OK, 0 rows affected (0.03 sec)

08:59:16 [db_cash] session1>xa recover;
+----------+--------------+--------------+-------------------------------+
| formatID | gtrid_length | bqual_length | data                          |
+----------+--------------+--------------+-------------------------------+
|        1 |           19 |           10 | transfer_of_accountinvestment |
+----------+--------------+--------------+-------------------------------+

执行完xa commit 命令后,储蓄 RM 的分支事务就结束了。这个时候,等待更新表cash_account 的其他session 将获得锁,并执行。

08:33:32 [db_investment] session2>xa commit 'transfer_of_account','investment';
Query OK, 0 rows affected (0.02 sec)

08:59:41 [db_investment] session2>xa recover;
Empty set (0.00 sec)

到这里整个全局事务就结束了。这里只是模拟了一个全局事务中两个 RM 的行为,并且是成功提交的行为。回滚的操作基本类似,这里就不做演示了。在这个过程中我们自己相当于是 TM。在应用系统中,我们可以借助一些其他组件来承担TM 职责,比如 Seata.

4.总结

XA 执行流程如下:

  • AP 持有两个以上RM(AP 可以由多个微服务构成,每个微服务可能会有自己的本地数据库),AP 开启全局事务(xa start),调用RM 执行业务数据操作。
  • AP 通过 TM 通知储蓄 RM 准备提交,RM 执行xa prepare 命令,此时并未提交事务,undo/redo log 已记录,获取了表锁或者记录锁。
  • TM 收到准备回复,只要有一方失败则分别向其他 RM 发起回滚事务,回滚完毕,资源锁释放。
  • TM 收到全部准备成功,此时向所有 RM 发起提交事务,RM 执行xa commit 完成提交,释放资源。

通过这样讲解和实操之后,我们将会对分布式事务有了更深和更加形象化的理解,再结合其他文章的概念描述,将会相得益彰。

5.参考