XA 事务水很深,小伙子我怕你把握不住!

[TOC]

分布式事务系列继续!

前面松哥和大家聊了 Seata 中的 TCC 模式以及 AT 模式,没看的小伙伴可以先看看:

今天我们来继续学习 XA 事务!

Seata 中支持四种不同的事务模式:AT、TCC、XA 以及 Saga,这四种不同的事务模式中,XA 是最与众不同的一个!为什么这么说,相信读完本文你就了解了。

1. 什么是 XA 规范

1.1 什么是两阶段提交

我们先来稍微回顾一下两阶段提交。

先来看下面一张图:

这张图里涉及到三个概念:

  • AP:这个不用多说,AP 就是应用程序本身。
  • RM:RM 是资源管理器,也就是事务的参与者,大部分情况下就是指数据库,一个分布式事务往往涉及到多个 RM。
  • TM:TM 就是事务管理器,创建分布式事务并协调分布式事务中的各个子事务的执行和状态,子事务就是指在 RM 上执行的具体操作。

那么什么是两阶段(Two-Phase Commit, 简称 2PC)提交?

两阶段提交说白了道理很简单,松哥举个简单例子来和大家说明两阶段提交:

比如下面一张图(在五分钟带你体验一把分布式事务!so easy!一文中大家曾经见过这张图):

五分钟带你体验一把分布式事务!so easy!一文中,我们在 Business 中分别调用 Storage 与 Order、Account,这三个中的操作要同时成功或者同时失败,但是由于这三个分处于不同服务,因此我们只能先让这三个服务中的操作各自执行,三个服务中的事务各自执行就是两阶段中的第一阶段。

第一阶段执行完毕后,先不要急着提交,因为三个服务中有的可能执行失败了,此时需要三个服务各自把自己一阶段的执行结果报告给一个事务协调者(也就是前面文章中的 Seata Server),事务协调者收到消息后,如果三个服务的一阶段都执行成功了,此时就通知三个事务分别提交,如果三个服务中有服务执行失败了,此时就通知三个事务分别回滚。

这就是所谓的两阶段提交。

总结一下:两阶段提交中,事务分为参与者(例如上图的各个具体服务)与协调者(上文案例中的 Seata Server),参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是要提交操作还是中止操作,这里的参与者可以理解为 RM,协调者可以理解为 TM。

不过 Seata 中的各个分布式事务模式,基本都是在二阶段提交的基础上演化出来的,因此并不完全一样,这点需要小伙伴们注意。

1.2 什么是 XA 规范

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。

XA 规范描述了全局的事务管理器与局部的资源管理器之间的接口。 XA规范的目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。

XA 规范使用两阶段提交来保证所有资源同时提交或回滚任何特定的事务。

XA 规范在上世纪 90 年代初就被提出。目前,几乎所有主流的数据库都对 XA 规范提供了支持。

XA 事务的基础是两阶段提交协议。需要有一个事务协调者来保证所有的事务参与者都完成了准备工作(第一阶段)。如果协调者收到所有参与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。MySQL 在这个 XA 事务中扮演的是参与者的角色,而不是协调者(事务管理器)。

MySQL 的 XA 事务分为内部 XA 和外部 XA。外部 XA 可以参与到外部的分布式事务中,需要应用层介入作为协调者;内部 XA 事务用于同一实例下跨多引擎事务,由 Binlog 作为协调者,比如在一个存储引擎提交时,需要将提交信息写入二进制日志,这就是一个分布式内部 XA 事务,只不过二进制日志的参与者是 MySQL 本身。 MySQL 在 XA 事务中扮演的是一个参与者的角色,而不是协调者。

2. MySQL 中的 XA

接下来松哥通过一个简单的例子先给大家看下 MySQL 中的 XA 是怎么玩的。

2.1 两阶段事务提交

比如说我们上文中的转账操作,我用 MySQL 中的 XA 事务来和大家演示一下从一个账户中转出 10 块钱:

上面这段事务提交是一个两阶段事务提交的案例。

具体执行步骤如下:

  1. XA START "transfer_money":这个表示开启一个 XA 事务,后面的字符串是事务的 xid,这是一个唯一字符串,开启之后,事务的状态变为 ACTIVE
  2. update account set amount=amount-10 where account_no='A'; 这个表示执行具体的 SQL。
  3. XA END "transfer_money":这个表示结束一个 XA 事务,此时事务的状态转为 IDLE
  4. XA PREPARE "transfer_money":这个将事务置为 PREPARE 状态。
  5. XA COMMIT "transfer_money":这个用来提交事务,提交之后,事务的状态就是 COMMITED。

最后一步,可以通过 XA COMMIT 来提交,也可以通过 XA ROLLBACK 来回滚,回滚后事务的状态就是 ROLLBACK。

另外第四步可以省略,即一个 IDLE 状态的 XA 事务可以直接提交或者回滚。

我们来看下面一张流程图:

从这张图里我们可以看出,事务可以一步提交,也可以两阶段提交,都是支持的。如果是两阶段提交,prepare 之后,其实是在等其他的资源管理器(RM)反馈结果。

2.2 事务直接提交

松哥再给大家演示一下事务一步提交:

这个就比较简单,没啥好说的。

这块再跟大家介绍另外一个 XA 事务相关的命令 XA RECOVER,如下图:

XA RECOVER 可以列出所有处于 PREPARE 状态的 XA 事务,其他状态的事务则都不会列出来,如上图。

2.3 小结

在用一个客户端环境下,XA 事务和本地(非 XA )事务互相排斥,如果已经通过 XA START 来开启一个事务,则本地事务不会被启动,直到 XA 事务被提交或者被回滚为止。

相反的,如果已经使用 START TRANSACTION 启动一个本地事务,则 XA 语句不能被使用,直到该事务被提交或者回滚为止,而且 XA 事务仅仅被 InnoDB 存储引擎支持。

3. Seata 中的 XA

3.1 Seata 中的 XA 模式

我们先来看一点理论知识,3.2 小节我们再来看代码实践。

通过上面的介绍,大家已经知道了 MySQL 中的 XA 事务是怎么回事了,Seata 中的 XA 模式其实就是在 MySQL 中 XA 模式的基础上实现的。Seata 中的 XA 模式就是在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。

我们来看下面一张图:

我来大概说一下这个执行步骤:

  1. 首先由 TM 开启全局分布式事务。
  2. 各个业务 SQL 分别放在不同的 XA 分支中进行,具体执行的流程就是 XA Start->业务 SQL->XA End,这个流程跟我 2.1 小节和大家演示的 MySQL 中 XA 事务的流程是一致的。
  3. 分支中的 XA 事务执行完成后,执行 XA prepare,并将自己执行的状态报告给 TC。
  4. 其他的分支事务均按照 2、3 步骤来执行。
  5. 当所有分支事务都执行完毕后,TC 也收到了各个分支事务报告上来的执行状态,如果所有状态都 OK,则 TC 通知所有 RM 执行 XA Commit 完成事务的最终提交,否则 TC 通知所有 RM 执行 XA Rollback 进行事务回滚。

这就是 Seata 中的 XA 模式!只要小伙伴们理解了 2.2 小节中 MySQL 的 XA 模式,那么 Seata 中的 XA 模式就很好理解了。

3.2 代码实践

接下来我们来看一下代码实践,通过一个具体的案例来看下 Seata 中的 XA 模式。

这里我们依然是使用官方的案例,案例的业务和 AT 的业务是一样的,也是一个下单案例,如果小伙伴们对这个案例的业务不熟悉,可以先看看五分钟带你体验一把分布式事务!so easy!一文。

不过还是我之前说的,官方案例中的依赖较多,下载容易出错,并且有些依赖存在版本冲突,需要自行解决之后才能运行起来,所以松哥这里自己也整理了一套,如下:

小伙伴们可以在公众号后台回复 seata-demo 下载这个案例,下载之后可以直接运行。

这里案例代码这块的问题,另外数据库这块也需要我们提前准备下。

首先大家看到案例中有一个 sql 目录,这个目录下有对应的数据库脚本 all_in_one.sql,我们在数据库中创建一个名为 seata_xa 的数据库,然后在数据库中执行该脚本。

执行完成后,会生成三张表:

和 AT 模式相比,这里就少了一张 undo_log 表,原因很简单,AT 二阶段回滚用的是反向补偿(通过更新语句将数据复原),而 XA 则是利用数据库自己的 XA 模式,通过 XA ROLLBACK 命令回滚的,所以 XA 模式不需要 undo_log 表。

接下来分别修改 account-xaorder-xastorage-xa 以及 business-xa 四个模块的 application.properties 配置文件,将数据库连接地址改对,如下:

1
2
3
spring.datasource.url=jdbc:mysql:///seata_xa?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123

修改完成后,准备工作就算做好啦,接下来就是项目启动了(business-xa 启动成功后会自动完成数据的初始化)。

首先需要确保你的 seata-server 是启动状态的,seata-server 的启动还需要 eureka 的支持,这个大家具体可以参考五分钟带你体验一把分布式事务!so easy!一文。

然后分别启动 account-xaorder-xastorage-xa 以及 business-xa 模块。

项目启动成功后,然后访问如下地址完成一个购买操作:

1
http://localhost:8084/purchase

这是一个成功的购买操作,即事务的二阶段会提交而不是回滚,访问后浏览器响应如下:

接下来我们可以添加一个访问参数 count,表示购买的商品数量,这里设置要购买的商品数量为 999(实际上并没有这么多商品),因此在事务二阶段提交的时候会回滚。

1
http://localhost:8084/purchase?count=999

访问后浏览器响应如下:

这就是通过 Seata XA 模式来处理分布式事务。

3.3 代码简析

可能有小伙伴觉得 XA 和 AT 很像,代码层面上确实很像!但是原理却差着十万八千里。

和 Seata AT 模式相比,Seata XA 模式主要是修改一下数据源,大家可以注意,account-xaorder-xa 以及 storage-xa 三个微服务的数据源都是 DataSourceProxyXA,以 order-xa 模式为例,数据源配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class OrderXADataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Bean("dataSourceProxy")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);

// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}
@Bean("jdbcTemplate")
public JdbcTemplate jdbcTemplate(DataSource dataSourceProxy) {
return new JdbcTemplate(dataSourceProxy);
}
}

可以看到,这里使用的数据源并非原生的数据源,而是经过包装后的 DataSourceProxyXA。

如果仅仅从代码层面来讲,把 DataSourceProxyXA 换成 DataSourceProxy 就是 AT 模式了,把 DataSourceProxy 换成 DataSourceProxyXA 就是 XA 模式了,就是这么简单!

这个案例中其他地方的代码基本上都是 AT 模式一样,所以我这里也就不啰嗦了。不过有一点需要注意,就是 business-xa 模块的数据源并没有使用 DataSourceProxyXA,原因在于该模块中配置数据源主要是为了初始化数据,并不涉及分布式事务。

另外需要注意,在各个分支事务上,需要通过 @Transactional 注解来开启事务,开启之后,就会按照 XA 的那一套来,否则出问题了该服务不会回滚,默认情况下,只有 account-xa 模块加了该注解,小伙伴们在具体测试过程中,可以给其他模块也加上该注解,并进行测试查看效果。

4. XA 的几个问题

XA 模式有几个被人广泛诟病的问题,我们一起来了解下。

  1. 数据锁定

当使用 XA 事务时,数据在整个事务处理过程结束前,都被锁定,读写都按隔离级别的定义约束起来。这确实是 XA 模式的一个劣势,不过这也是获得更高隔离性和全局一致性所要付出的代价。松哥前面和大家分享的补偿型事务处理机制(AT、TCC)虽然不存在这个问题,但是却牺牲了隔离性。AT 模式使用全局锁保障基本的写隔离,实际上也是锁定数据的,只不过锁在 TC 侧集中管理,解锁效率高且没有阻塞的问题。

  1. 协议阻塞

XA prepare 后,分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。议的阻塞机制本身并不是问题,关键问题在于协议阻塞遇上数据锁定,如果一个参与全局事务的资源 “失联” 了(收不到分支事务结束的命令),那么它锁定的数据,将一直被锁定,进而可能因此产生死锁。这是 XA 协议的核心痛点,也是 Seata 引入 XA 模式要重点解决的问题,Seata 中主要是解决了失联问题,并通过增加自解锁机制来解决这个问题。

  1. 性能差

这可能是被诟病最多的地方了,XA 模式性能的损耗主要来自两个方面:一方面,事务协调过程,增加单个事务的 RT;另一方面,并发事务数据的锁冲突,降低吞吐。

和不使用分布式事务支持的运行场景比较,性能肯定是下降的,这点毫无疑问。本质上,事务(无论是本地事务还是分布式事务)机制就是拿部分 性能的牺牲 ,换来 编程模型的简单。与同为业务无侵入的 AT 模式比较:

  • 同样运行在 Seata 定义的分布式事务框架下,XA 模式并没有产生更多事务协调的通信开销。
  • 并发事务间,如果数据存在热点,产生锁冲突,这种情况,在 AT 模式(默认使用全局锁)下同样存在的。

所以,在影响性能的两个主要方面,XA 模式并不比 AT 模式有非常明显的劣势。

AT 模式性能优势主要在于:集中管理全局数据锁,锁的释放不需要 RM 参与,释放锁非常快;另外,全局提交的事务,完成阶段 异步化。

5. 总结

在当前的技术发展阶段,不存一个分布式事务处理机制可以完美满足所有场景的需求。

一致性、可靠性、易用性、性能等诸多方面的系统设计约束,需要用不同的事务处理机制去满足。

Seata 项目最核心的价值在于:构建一个全面解决分布式事务问题的标准化平台。

基于 Seata,上层应用架构可以根据实际场景的需求,灵活选择合适的分布式事务解决方案。

参考资料:

  1. https://seata.io/zh-cn/blog/seata-xa-introduce.html
  2. https://www.cnblogs.com/duyanming/p/7326960.html

喜欢这篇文章吗?扫码关注公众号【江南一点雨】【江南一点雨】专注于 SPRING BOOT+微服务以及前后端分离技术,每天推送原创技术干货,关注后回复 JAVA,领取松哥为你精心准备的 JAVA 干货!

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×