07、分布式事务 实战 - XA 强一致性分布式事务原理

一、X/Open DTP 模型与 XA 规范

X/Open DTP 模型是 X/Open 组织定义的分布式事务标准规范,这个规范定义了分布式事务处理的一套规范和 API,具体的实现由各厂商负责。本节对 X/Open DTP 模型与 XA 规范进行简单介绍

1.DTP 模型

DTP模型主要定义了 3 个核心组件,分别是应用程序、资源管理器和事务管理器,三者之间的关系如下图所示:

 

  • 应用程序用于定义事务边界,即定义事务的开始和结束,并且在事务边界内对资源进行操作
  • 资源管理器也称为事务参与者,如数据库、文件系统等,并提供访问资源的方式
  • 事务管理器也称为事务协调者,负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等操作

2.XA 规范

下面简要介绍 XA 规范:

1、 xa_start:负责开启或恢复一个事务分支,并且管理XID到调用线程;
2、 xa_end:负责取消当前线程与事务分支的关联;
3、 xa_prepare:负责询问资源管理器是否准备好提交事务分支;
4、 xa_commit:负责通知资源管理器提交事务分支;
5、 xa_rollback:负责通知资源管理器回滚事务分支;
6、 xa_revocer:负责列出需要恢复的XA事务分支;

3.JTA 规范

JTA(Java Transaction API)为 J2EE 平台提供了分布式事务服务的能力。JTA 规范是 XA 规范的 Java 版,即把 XA 规范中规定的 DTP 模型交互接口抽象成 Java 接口中的方法,并规定每个方法要实现什么样的功能,其架构如下图所示:

 

JTA定义的接口如下:

1、 javax.transaction.TransactionManager:事务管理器,负责事务的begin、commit、rollback等命令;
2、 javax.transaction.UserTransaction:用于声明一个分布式事务;
3、 javax.transaction.TransactionSynchronizationRegistry:事务同步注册;
4、 javax.transaction.xa.XAResource:定义资源管理器提供给事务管理器操作的接口;
5、 javax.transaction.xa.Xid:事务XID接口;

事务管理器提供者:实现 UserTranssaction、TransactionManager、Transaction、TransactionSynchronizationRegistry、Synchronization、XID 接口,通过与 XAResource 接口交互来实现分布式事务

资源管理器提供者:XAResource 接口需要由资源管理器实现,该接口中定义了一些方法,这些方法会被事务管理器调用

1、 start方法:开启事务分支,对应XA底层实现是XASTART;
2、 end方法:结束事务分支,对应XA底层实现是XAEND;
3、 prepare方法:准备提交分支事务,对应XA底层实现是XAPREPARE;
4、 commit方法:提交分支事务,对应XA底层实现是XACOMMIT;
5、 rollback方法:回滚分支事务,对应XA底层实现是XAROLLBACK;
6、 recover方法:列出所有处于PREPARED状态的事务分支,对应XA底层实现是XARECOVER;

4.XA 二阶段提交

一阶段:执行 XA PREPARE 语句。事务管理器通知各个资源管理器准备提交它们的事务分支。资源管理器受到通知后执行 XA PREPARE 语句

二阶段:执行 XA COMMIT/ROLLBACK 语句。事务管理器根据各个资源管理器的 XA PREPARE 语句执行结果,决定是提交事务还是回滚事务。如果所有的资源管理器都预提交成功,那么事务管理器通知所有的资源管理器执行 XA 提交操作;如果有资源管理器的 XA PREPARE 语句执行失败,则由事务管理器通知所有资源管理器执行 XA 回滚操作

二、MySQL 对 XA 规范的支持

MySQL 从 5.0.3 版本开始支持 XA 分布式事务,且只有 InnoDB 存储引擎支持。MySQL Connector/J 从 5.0.0 版本开始直接提供对 XA 的支持。需要注意的是,在 DTP 模型中,MySQL 属于资源管理器,而一个完整的分布式事务中一般会存在多个资源管理器,由事务管理器来统一协调。因此,这里所说的 MySQL 对 XA 分布式事务的支持,一般指的是单台 MySQL 实例如何执行自己的事务分支

1.MySQL XA 事务的语法

1、 XA{START|BEGIN}xid[JOIN|RESUME]:开启XA事务,注意如果使用的是XASTART,那么不支持[JOIN|RESUME]语句;
2、 XAENDxid[SUSPEND[FORMIGRATE]]:结束一个XA事务,不支持[SUSPEND[FORMIGRATE]]语句;
3、 XAPREPARExid:准备提交XA事务(如果使用了一阶段提交,该过程可以省略);
4、 XACOMMITxid[ONEPHASE]:提交XA事务;
5、 XAROLLBACKxid:回滚XA事务;
6、 XARECOVER[CONVERTXID]:列出所有处于Prepare阶段的XA事务;

2.MySQL XID 详解

在MySQL 的事务语法中的最后都会跟上 XID,MySQL 作为事务分支标识符是在 XA 规范中定义的。XID 的结构描述如下:

#define XIDDATASIZE 128
#define MAXGTRIDSSIZE 64
#define MAXBQUALSIZE 64
struct xid_t {
	long formatID;
	long gtrid_length;
	long bqual_length;
	char data[XIDDATASIZE];
};
typedef struct xid_t XID;
extern int ax_reg(int, XID *, long);
extern int ax_unreg(int, long);

我们可以看到,在 XID 中有 4 个字段,下面分别解释各字段的含义:

1、 formatID:记录gtrid、bqual的格式,类似Memcached中flags字段的作用XA规范中通过一个结构体约定了XID的组成部分,但没有规定data中存储的gtrid、bqual的内容应该是什么格式;
2、 gtrid_length:全局事务标识符(GlobalTransactionIdentifier),最大不能超过64字节;
3、 bequal_length:分支限定符(BranchQualifier),最大不能超过64字节;
4、 data:XID的值,即gtrid和bqual拼接后的内容在XID的结构体中,没有gtrid和bqual,只有gtrid_length、bqual_length由于二者的内容都存储在data中,因此我们可以根据data反推出gtrid和bqual举例来说,假设gtrid为abc,bqual为def,那么gtrid_length=3,bqual_length=3,data=abcdef反推的时候,从data[0]到data[gtrid_length-1]之间的部分就是gtrid的值,从data[gtrid_length]到data[gtrid_length+bqual_length-1]部分就是bqual的值;

3.MySQL XA 事务的状态

MySQL XA 事务状态是正确执行 XA 事务的关键。每次执行 MySQL 的 XA 事务语句都会修改 XA 事务的状态,进而执行不同的 XA 语句。XA 事务状态流程如下图所示:

 

  • 在 XA START 和 XA END 之间执行的是业务 SQL 语句,无论是否执行成功, 都应该执行 XA END
  • 在 IDLE 状态下的事务可以直接执行 XA COMMIT,这里我们可以这样理解,当只有一个资源管理器的时候,可以直接退化成一阶段提交
  • 只有状态为 Failed 的时候,才能执行 XA ROLLBACK 进行 XA 事务回滚
  • XA 事务和非 XA 事务(即本地事务)是互斥的。例如,已经执行了 XA START 命令来开启一个 XA 事务,则本地事务不会被启动,直到 XA 事务被提交或回滚为止。相反的,如果已经使用 START TRANSACTION 启动了一个本地事务,则 XA 语句不能被使用,直到该事务被提交或回滚为止

4.MySQL XA 的问题

MySQL 低于 5.7 版本会出现的问题

1、 已经预提交的事务,在客户端退出或者服务宕机的时候,二阶段提交的事务会被回滚;
2、 在服务器故障重启提交后,相应的BinLog会丢失;

MySQL 5.6 版本在客户端退出的时候,会自动回滚已经准备好的事务,这是因为,对于处于 Prepare 状态的事务,MySQL 是不会记录 BinLog 的(官方解释为减少 fsync,起到优化的作用),Prepare 状态以前的操作信息都保存在连接的 IO_CACHE 中,如果此时客户端退出了,那么 BinLog 信息会被丢弃,重启后会丢失数据

MySQL 高版本的优化

事务在Prepare 阶段就完成了写 BinLog 的操作(通过新增一种名为 XA_prepare_log_event 的 event 类型来实现)

三、XA 规范的问题思考

XA二阶段规范虽然提供了分布式事务的解决思路与方案,但是自身也存在很多问题

1.XA 规范的缺陷

下面使用两个 MySQL 实例,完成一次 XA 分布式事务,如下图所示:

 

XA规范中每个分支事务的执行都是同步的,并且只会存在一个事务协调者,由于网络的不稳定性,可能会出现数据不一致的问题。总体来说,XA 分布式事务会存在如下几个问题

同步阻塞

全局事务内部包含多个独立的事务分支,这些事务分支要么都成功,要么都失败。各个事务分支的 ACID 特性共同构成了全局事务的 ACID 特性,即单个事务分支支持的 ACID 特性被提升到分布式事务的范畴。即使在非分布式事务中,如果对读操作很敏感,我们也需要将事务隔离级别设置为串行化。而分布式事务更是如此,可重复读隔离级别不足以保证分布式事务的一致性。如果我们使用 MySQL 来支持 XA 分布式事务,那么最好将事务隔离级别设置为串行化。串行化是 4 个事务隔离级别中最高的级别,也是执行效率最低的级别

单点故障

一旦协调者事务管理器发生故障,参与者资源管理器会一直阻塞下去。尤其在两阶段提交的第二个阶段,如果协调者发生故障,那么所有的参与者都将处于锁定事务资源的状态中,无法继续完成事务操作(如果是协调者宕机,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

数据不一致

在Commit 阶段,当协调者向参与者发送 commit 请求后,发生了局部网络异常或者在发送 commit 请求的过程中,协调者发生了故障,会导致只有一部份参与者接收到了 commit 请求。而这部分参与者接到 commit 请求之后就会执行 commit 操作,但是其他部分未接到 commit 请求的参与者无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象

2.解决 XA 数据不一致的问题

日志存储

记录XA 事务在每个流程中的执行状态,是解决 XA 数据不一致问题的关键。至于日志应该存储在哪里,使用什么存储,则根据具体需求确定,一般推荐采用中心化的存储方式,比如数据库。下表是一个简单的事务日志的数据结构:

 

自定义事务回复

事务恢复,首先通过 XA recovery 命令从资源管理器中获取需要被恢复的事务记录,然后根据 XID 匹配应用程序中存储的日志,根据事务状态进行提交或回滚,大体流程如下图所示:

 

4.解决事务管理器的单点故障问题

解决事务管理器的单点故障问题,我们一般会想到集群部署和注册中心。实际上,注册中心检测服务是否可用也是需要时间的。目前业界大致有两种解决方式,一种是去中心化部署(事务管理器嵌套在业务系统中),一种是中心化部署,如下图所示:

 

  • 去中心化部署:事务管理器嵌套在应用程序里面,不再单独部署。集群模式中事务角色由应用程序来解决
  • 中心化部署:事务管理器单独部署,然后与应用程序进行远程通信。集群模式中事务角色依赖其自身解决

四、主流的解决方案

XA规范虽已提出多年,但在开源社区中,完整的解决方案并不是很多,下面给大家简单介绍几个目前开源社区中主流的 XA 分布式事务解决方案

Atomikos 解决方案

Atomikos 由免费的社区版本和收费的商业版本

  • 官网地址:https://www.atomikos.com
  • 社区版源码地址:https://github.com/atomikos/transactions-essentials
  • 社区版与商业版对比:https://www.atomikos.com/Main/CompareSubscriptions?done_form=1

Hmily 解决方案

Hmily 是国内 Dromara 开源社区提供的一站式分布式事务解决方案

  • 官网地址:https://dromara.org
  • 项目源码地址:https://github.com/dromara/hmily

Narayana 解决方案

Narayana 是 Jboos 团队提供的 XA 分布式事务解决方案

  • 官网地址:http://narayana.io
  • 项目源码地址:https://github.com/jbosstm/narayana