一、前言
在单个数据库实例时候,我们可以使用一个数据源的事务(本地事务 )来保证事务内的多个操作要么全部执行生效,要么全部不生效。在多数据库实例节点时候,我们对多个实例的数据源进行操作时候就没办法把多个操作放到一个大的事务内来管理了,因为多个实例操作的是不同的数据源,而数据库自带的事务是针对单个数据源来说的,这时候就需要分布式事务了。
本 Chat 主要讲解分布式事务的原理,主要包含下面内容:
- 何为分布式事务二阶段提交协议,二阶段提交存在哪些缺点?
- 何为分布式事务三阶段提交协议,三阶段提交相比二阶段提交存在哪些优点?
- 何为分布式事务 TCC 编码模式,并结合蚂蚁金服的 XTS 进行概要介绍。
- MySQL 中基于 XA 实现的分布式事务。
- 事务管理器 Atomikos 实现的分布式事务。
二、本地事务与分布式事务
本地事务是特定于资源的,比如一个本地事务是与一个具体 JDBC 数据库连接相关的,一个本地事务关联一个数据库连接,本地事务 JDBC 编程骨骼框架一般是如下所示:
private static final String cmdUpdateAccountFrom = "update account_from set money = money - 50 where id =1";
private static final String cmdUpdateAccountTo = "update account_to set money = money + 50 where id =1";
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
//(1)获取数据库连接
conn = dataSource.getConnection();
conn.setAutoCommit(false);
//(2)account_from表减去50元
stmt = conn.prepareStatement(cmdUpdateAccountFrom);
int result = stmt.executeUpdate();
System.out.println("account_from sub 50:" + result);
//if(true) throw new RuntimeException("error");
//(3)account_to表加上50元
stmt = conn.prepareStatement(cmdUpdateAccountTo);
result = stmt.executeUpdate();
System.out.println("account_from add 50:" + result);
//(4)提交事务
conn.commit();
} catch (Exception e) {
e.printStackTrace();
//(5)回滚事务
if (null != conn) {
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
} finally {
//(6)关闭数据库连接
if (null != conn) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
如上代码,假设同一个数据库里面有表 account_from
和表 account_to
,现在要想把id=1的用户从表 account_from
转移 50 元钱到表 account_to
,可知代码(1)首先获取了数据库的一个连接(事务与具体数据库连接相关)并设置为手动提交事务。
代码(2)把表 account_from
中 id=1 的记录的金额字段减去 50(并没有提交),代码(3)把表 Account_to
中 id=1 的记录金额字段加上 50(并没有提交),在执行代码(2)和(3)后在可重读和读已提交隔离级别下,由于不存在脏读(脏读),所以其他事务看到不更新结果,必须到代码(4)则提交事务后才可以看到。
转账操作必须是原子性的,也就是 account_from
的扣款和 Account_to
表的加款必须同时成功,同时失败。这里两个操作被包含到了同一个数据库连接对应的事务里面,如果更新 account_from
表成功后,在更新 account_to
表前抛出了异常,会执行代码(5)回滚整个事务,即会把 Account_from
的扣款操作回滚。
这里我们模拟的用户的两个账户是在同一个数据库的两个表里,这时候使用本地事务就可以保证转账业务的正确性,但是真实情况下,用户的不同账户很可能是在不同的数据库,比如在上海的用户账号向深圳的账号转账,这时候用户不同账号存放在不同的数据库的表里面,所以本地事务就无能为力了,因为本地事务是与具体某个数据库的连接相关的,本地事务无法管理跨越多个数据库资源的事务,这时候就需要分布式事务(也叫全局事务)了。
分布式事务可以保证多个数据库对应的数据源操作的原子性,但是分布式事务不仅仅是可以保证多个数据库资源的原子性,还可以是数据源操作与远程 RPC 操作的原子性(本地事务提交与远程 RPC 调用同时成功,同时失败),数据源与消息发送的原子性(本地事务提交与发送消息同时成功,同时失败)。更广义的是说分布式事务可以保证对多个资源操作的原子性。
在本地事务时候,事务本身就具有 ACID 特性,而在分布式事务中事务的 CID 是使用每个分布式事务参与者的本地事务自身的 CID 来实现,而事务的 A 原子性则使用比如二阶段,三阶段,TCC 编码协议来保证。
严格遵守 ACID 的分布式事务我们称为刚性事务,二阶段协议可以认为是刚性事务。而遵循 BASE 理论(基本可用:在故障出现时保证核心功能可用,软状态:允许中间状态出现,最终一致性:不要求分布式事务打成中时间点数据都是一致性的,但是保证达到某个时间点后,数据就处于了一致性了)的事务我们称为柔性事务,其中 TCC 编程模式就属于柔性事务。
三、二阶段提交协议(2PC 协议)
3.1 二阶段提交协议
在二阶段提交协议中分布式事务由事务发起者、资源管理器(参与者)、事务协调者组成,下面我们看看二阶段协议内容:
- 第一阶段
分布式事务发起方向事务协调器发起分布式事务,协调器则向所有事务参与者发起准备请求,事务参与者接受到请求后,执行本地事务,但是不提交。如果所有事务参与者都返回了准备 OK 到事务协调器,则事务协调器准备进入第二阶段:
如果有一个参与者返回准备失败或者协调器等待超时后还没有收到参与者的反馈,则事务协调器向所有参与者发起事务回滚请求,事务参与者收到请求后回滚执行的本地事务,然后分布式事务结束:
- 第二阶段
事务协调器向所有事务参与者发起提交事务的请求,事务参与者接受到请求后,执行本地事务的提交操作,执行完毕后释放事务对应的锁和其它资源。如果事务协调器收到所有参与者提交 OK 则分布式事务结束。
二阶段协议是个标准协议,协议只是规定了分布式事务使用两阶段来做,其中第一步可以认为是投票,第二阶段认为是做决策,由于第二步要么全部提交要么全部回滚,所以可以认为二阶段协议是强一致性协议。
二阶段协议并没有规定具体如何实现,比如事务协调器是作为一个单独应用存在,还是与事务发起方部署在一个应用的?事务参与者是单独的应用还是与发起方在同一个应用?并且并没有考虑异常情况,比如第二阶段如果有部分参与者返回提交失败或者由于网络原因返回了提交 OK,但是事务协调器没有收到,该怎么处理?
常见的实现上是有一个事务补偿机制,有个专门的系统来定时去扫描没有正常完成的分布式事务,然后重试失败的分支事务或者对已经提交的分支事务做回滚或者报警由人工来处理。
3.2 二阶段提交协议的缺点
二阶段是一个比较经典的实现分布式事务原子性的协议,目前分布式事务实现上大多都是使用二阶段协议来实现的。
但是二阶段协议存在一个同步阻塞的问题:在二阶段的第一阶段所有参与者接受到事务协调器的事务准备请求后,会在本地开启并执行事务,但是没有提交事务;所有参与者等待第二阶段事务协调器发出事务提交或者回滚后才会提交或者回滚事务。
而在这期间所有参与者开启的本地事务一直存在,也就是一直把相应的资源锁定了(比如本地事务要更新一行数据,则在一阶段开启事务后,事务提交或者回滚之前都一直通过行锁锁定了这行数据),导致其他需要访问这行数据的事务阻塞等待。
更具体的一个例子,假如在第一阶段事务协调器给 10 个参与者发送准备请求,其中 9 个参与者正确接受了,并开启了本地事务锁定了具体的资源,而剩下一个参与者或者由于网络问题没有收到准备请求,或者接受到了但是本事事务执行失败,或者执行正常但是给事务协调器的回执由于网络原因没有被协调器收到等,则事务协调器发现其中一个参与者返回准备失败或者等待超时后还没收到那一个参与者的回执则会通知所有的参与者执行回滚操作。也就是在具体回滚前,其他 9 个参与者白白的锁定了本地资源,成功的阻止了在开启事务后、回滚之前其他事务访问锁定的资源,这显然很浪费。
另外资源阻塞还在下面情况有所体现:当第一阶段参与者接受准备请求后,接受者开启事务并且发送准备 OK 给协调者,这时候如果协调者挂了,并且是永久性的挂了,那么这些开启了事务的参与者将一直占用事务资源。这时候参与者将收不到提交或者回滚命令,参与者就会处于不知所措的状态(是一直持有开启事务还是超时后回滚还是超时后自动提交?)。
四、三阶段提交协议(3PC)
三阶段协议把二阶段的第一阶段在细分为 2 阶段,相比二阶段是阻塞协议,三阶段是非阻塞协议,三阶段正常逻辑图如下:
此图来源:维基百科
对应事务协调器来说,接受到一个分布式事务请求后,会向所有事务参与者发起 canCommit 请求,然后协调器处于 waiting 状态。
如果协调器收到参与者反馈的 NO 消息,或者等待超时没有收到参与者的反馈,则协调器向所有参与者发起 abort 请求,分布式事务异常结束。否者如果协调器在超时时间窗口到达前收到了所有参与者的反馈的 YES 消息,则协调器向所有参与者发送 preCommit 消息,然后协调器处于 prepared 状态。
如果协调器收到了所有参与者返回的 ACK 消息,则协调器状态从 prepared 转换到 commit 待提交状态,然后向所有参与者发起 doCommit 请求;否者如果协调器等待超时还没有收到参与者的返回或者有些参与者返回了 NO,则协调器向所有参与者发起回滚请求,然后协调器处于 Done 状态。
如果协调器收到了所有参与者返回的 havaCommitted 消息则协调器状态转换为 done, 分布式事务正常结束。如果协调器收到参与者返回的 NO 信息或者没有收到参与者反馈,则向所有参与者发起回滚请求。
对应参与者来说,第一阶段如果参与者接受到协调器发来的 canCommitted 请求,如果参与者接受该请求,则向协调器发送 YES 反馈,然后状态转换到 prepared;如果参与者不接受该请求,则向协调器发送 NO 消息,然后自己处于 abort 状态。如果参与者一直没有收到 preCommited 请求,则超时后会处于 abort 状态。如果参与者接受到了协调器发来的 abort 请求,则参与者也处于 abort 请求。
第二阶段如果参与者收到协调器发来的 preCommit 请求,执行 OK 后会发送 ACk 到协调器,然后等待最终的提交或者回滚。如果参与者接受到 preCommit 请求后协调器挂了或者参与者等待超时都没收到协调器发来的提交或者回滚请求,则参与者默认执行自动提交。
第三阶段如果参与者接受到了协调器发来的 doCommit 请求,则参与者会执行具体提交,然后返回 havaCommitted 信息。
以上是三阶段的内容,下面我们看看三阶段相比二阶段如何实现的非阻塞:
在提交阶段,两阶段提交协议无法可靠地从协调器和参与者的故障中恢复。如果只有协调器挂了,并且所有参与者没有收到提交消息,这时候根据参与者的状态可以判断出参与者没有执行提交,新选取的协调器发送提交命令到参与者就可以恢复了。但是,如果协调器和一部分参与者都挂了,则挂掉的参与者可能是第一个被通知提交的,并且实际上已经完成了提交,则这时候即使选择了新的协调器,协调器由于已经处于 commit 状态了,则必须阻塞直到所有参与者给予了反馈,而之前的参与者已经完成了提交,不会在给协调器发送回执了。
三阶段提交协议通过引入 Prepared to commit(第二阶段)状态来消除此问题。如果协调器在发送 preCommited 消息之前挂了,则参与者将一致同意该操作被中止(协调器超时后会发送 abort 命令,参与者超时后会处于 abort 状态),这时候分布式事务就结束了,并且没有占用任何资源。并且在所有参与者确认他们准备提交之前,协调器不会发出 doCommit 消息。如果在协调器发送 doCommit 消息后,协调器挂了,则参与者在超时后会自动提交。新选择出来的协调器在等待超时后也会发送回滚信息到参与者,而不会一直等待。
但是三阶段协议也会存在数据不一致性问题,比如由于网络原因,协调器发送的 abort 响应没有及时被参与者接收到,而参与者在等待超时之后执行了 commit 操作,而其它参与者接受到了 abort 命令执行了回滚,这时候数据就处于不一致状态了。从上面介绍,我们知道无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。
注:三阶段协调第一阶段并不会让参与者锁定资源,而是在所有参与者都同意的时候在第二阶段进行资源的锁定,然后通过引入超时后自动 abort 或者自动 commit 来避免了永久阻塞。但是由于三阶段比较复杂,并且该协议至少需要三次往返才能完成,至少需要三次往返时间(RTT),这可能使完成每笔交易的长时间延迟,目前还没有找到基于三阶段协议的实现的分布式事务实现,另外三阶段协议本身也只是一个协议,并没有具体规定如何实现。
五、TCC 编码模式
TCC 编程模式本质上也是一种二阶段协议,不同在于 TCC 编程模式需要与具体业务耦合,下面首先看下 TCC 编程模式步骤:
- 所有事务参与方都需要实现 try、confirm、cancle 接口。
- 事务发起方向事务协调器发起事务请求,事务协调器调用所有事务参与者的 try 方法完成资源的预留,这时候并没有真正执行业务,而是为后面具体要执行的业务预留资源,这里完成了一阶段。
- 如果事务协调器发现有参与者的 try 方法预留资源时候发现资源不够,则调用所有参与方的 cancle 方法回滚预留的资源,需要注意 cancle 方法需要实现业务幂等,因为调用失败后(比如网络原因参与者接受到了请求,但是由于网络原因事务协调器没有接受到回执)会重试。
- 如果事务协调器发现所有参与者的 try 方法返回都 OK,则事务协调器调用所有参与者的 confirm 方法,该方法不做资源检查,直接进行具体的业务操作,confirm 方法也需要做业务幂等。
- 如果协调器发现所有参与者的 confirm 方法都 OK 了,则分布式事务结束。
- 如果协调器发现有些参与者的 confirm 方法失败了,或者由于网络原因没有收到回执,则协调器会进行重试。这里如果重试一定次数后还是失败,会怎么样那?常见的是做事务补偿。
蚂蚁金服基于 TCC 实现了 XTS(云上叫 DTS),目前在蚂蚁金服云上有对外输出,这里我们来结合其提供的一个例子来具体理解 TCC 的含义,以下引入蚂蚁金服云实例:
“首先我们假想这样一种场景:转账服务,从银行 A 某个账户转 100 元钱到银行 B 的某个账户,银行 A 和银行 B 可以认为是两个单独的系统,也就是两套单独的数据库。
我们将账户系统简化成只有账户和余额 2 个字段,并且为了适应 DTS 的两阶段设计要求,业务上又增加了一个冻结金额(冻结金额是指在一笔转账期间,在一阶段的时候使用该字段临时存储转账金额,该转账额度不能被使用,只有等这笔分布式事务全部提交成功时,才会真正的计入可用余额)。按这样的设计,用户的可用余额等于账户余额减去冻结金额。这点是理解参与者设计的关键,也是 DTS 保证最终一致的业务约束。”
这个例子中 try 阶段并没有对银行A和B数据库中的余额字段做操作,而是对冻结金额做的操作,对应A银行预留资源操作是对冻结金额加上 100 元,这时候A银行账号上可用钱为余额字段-冻结金额;对应 B 银行的操作是对冻结金额上减去 100,这时候 B 银行账号上可用的钱为余额字段-冻结金额。
如果事务协调器调用银行 A 和银行 B 的 try 方法有一个失败了(比如银行 A 的账户余额不够了),则调用 cancle 进行回滚操作(具体是对冻结金额做反向操作)。如果调用 try 方法都 OK 了,则进入 confirm 阶段,confirm 阶段则不做资源检查,直接做业务操作,对应银行 A 要在账户余额减去 100,然后冻金额减去 100;对应银行 B 要对账户余额字段加上 100,然后冻结金额加上 100。
最关心的,如果 confirm 阶段如果有一个参与者失败了,该如何处理?其实上面的 try,cancle,confirm 操作是都是 xts-client 做的,还有一个叫做 xts-server 的专门做事务补偿的,这个 xts-server 是定时去检测失败的分布式事务。
关于 XTS 的原理简要概述,下面是一个示例图:
本图来自蚂蚁金服云
XTS 从架构上分为 xts-client 和 xts-server 两部分,其中前者是一个嵌入客户端应用的 Jar 包,主要负责事务数据的写入和处理;后者是一个独立的系统,主要负责异常事务的恢复,客户端主要步骤:
- 发起分布式事务的客户端(发起方)在本地开启一个事务。
- 事务内第一步调用 XTS 客户端的方法开启分布式事务(内部首先在数据库主事务表里面创建一个主事务记录,并标示事务开始)。
- 事务内第二步调用各个分布式事务参与者的 try 方法(在调用 try 前在数据库参与者表里面插入一个活动记录),上面过程可以认为是 2 阶段的第一阶段。
- 如果第一阶段某个参与者的 try 方法执行失败了(抛了异常),由于参与者是在发起者本地事务内执行的,则本地事务回滚,内部调用所有参与者的 cancle 方法,分布式事务结束。
- 如果所有参与者 try 方法都 OK,修改主事务记录的状态标示第一阶段 OK。这样我们根据主事务记录的状态就可以知道第一阶段是否 OK 了。如果 try 方法都 OK,那么进入第二阶段,执行所有参与者的 commit 方法。这里有两个结果,所有都提交 ok(删除主事务记录和参与者记录)或者有些失败了。到这里第二步完成了。
上面说的都是在 xts-client完成,那么 xts-server 是做啥的那?其实是用来做事务恢复的,在第二阶段如果某些参与者 commit 方法失败了,需要事务恢复系统(如果存在主事务记录并且状态为标示第一阶段 OK 则需要重试)定时在重试,直到成功。更多原理以及名词解释大家可以移步到官方文档讲的已经比较清楚了,这里不再讲解了。
TCC 是对二阶段的一个改进,try 阶段通过预留资源的方式避免了同步阻塞资源的情况,但是 TCC 编程需要业务自己实现 try、confirm、cancle 方法,对业务入侵太大,实现起来也比较复杂。
六、MySQL 中基于 XA 实现的分布式事务
6.1 XA 协议
首先我们来简要看下分布式事务处理的 XA 规范
可知 XA 规范中分布式事务有 AP、RM、TM 组成:
- 其中应用程序(Application Program ,简称 AP):AP 定义事务边界(定义事务开始和结束)并访问事务边界内的资源。
- 资源管理器(Resource Manager,简称 RM):RM 管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含比如数据库、文件系统、打印机服务器等。
- 事务管理器(Transaction Manager ,简称TM):负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等。
XA 主要规定了 RM 与 TM 之间的交互,下面来看下 XA 规范中定义的 RM 和 TM 交互的接口:
本图来着 参考文章 XA 规范 25 页
- xa_start 负责开启或者恢复一个事务分支,并且关联 XID 到调用线程
- xa_end 负责取消当前线程与事务分支的关联
- xa_prepare 负责询问 RM 是否准备好了提交事务分支
- xa_commit 通知 RM 提交事务分支
- xa_rollback 通知 RM 回滚事务分支
XA 协议是使用了二阶段协议的,其中:
- 第一阶段 TM 要求所有的 RM 准备提交对应的事务分支,询问 RM 是否有能力保证成功的提交事务分支,RM 根据自己的情况,如果判断自己进行的工作可以被提交,那就就对工作内容进行持久化,并给 TM 回执 OK;否者给 TM 的回执 NO。RM 在发送了否定答复并回滚了已经的工作后,就可以丢弃这个事务分支信息了。
- 第二阶段 TM 根据阶段 1 各个 RM prepare 的结果,决定是提交还是回滚事务。如果所有的 RM 都 prepare 成功,那么 TM 通知所有的 RM 进行提交;如果有 RM prepare 回执 NO 的话,则 TM 通知所有 RM 回滚自己的事务分支。
也就是 TM 与 RM 之间是通过两阶段提交协议进行交互的。
6.2 MySQL 中 XA 实现
MYSQL 的数据库存储引擎 InnoDB 的事务特性能够保证在存储引擎级别实现 ACID,而分布式事务让存储引擎级别的事务扩展到数据库层面,甚至扩展到多个数据库之间,这是通过两阶段提交协议来实现的,MySQL 5.0 或者更新版本开始支持 XA 事务,从下图可知 MySQL 中只有 InnoDB 引擎支持 XA 协议:
MySQL 中存在两种 XA 事务,一种是内部 XA 事务主要用来协调存储引擎和二进制日志,一种是外部事务可以参与到外部分布式事务中(比如多个数据库实现的分布式事务),本节我们主要讨论外部事务。
在 MySQL 数据库分布式事务中,MySQL 是 XA 事务过程中的资源管理器(RM),TM 是连接 MySQL 服务器的客户端。MySQL 数据库是作为 RM 存在的,在分布式事务中一般会涉及到至少两个 RM,所以我们说的 MySQL 支持 XA 协议是说 mysql 作为 RM 来说的,也就是说 MySQL 实现了 XA 协议中 RM 应该具有的功能;需要注意的是 MySQL 中只有当隔离级别为 Serializable 时候才能使用分布式事务,所以需要使用
set global tx_isolation='serializable',session tx_isolation='serializable';
设置数据库隔离级别(具体可以参考本地事务)。
下面我们来看看在 MySQL 数据库单个节点运行 XA 事务,首先来看下 MySQL 下 XA 事务语法:
其中 xid 是一个全局唯一的 id 标示一个分支事务,每个分支事务有自己的全局唯一的一个 id,是一个字符串。
然后确认下 MySQL 是否启动了 XA 功能:
可知启动了,下面具体看一个实例:
其中首先使用 XA START ‘xid’
启动了一个 XA 事务,并把它置于 ACTIVE 状态。
- 对于一个 ACTIVE 状态的 XA 事务,我们可以执行构成分支事务的多条 SQL 语句,也就是指定分支事务的边界,然后执行一个
XA END ‘xid’
语句,XA END
把事务放入 IDLE 状态,也就是结束事务边界,在xa start
和xa end
之间的语句就构成了本分支事务的一个事务范围。当调用xa end ‘xid1’
后由于结束了事务边界,所以这时候如何在执行 SQL 语句会抛出ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state
错误,也就是当分支事务处于 IDLE 状态时候不允许执行没有包含到分支事务边界里面的其他 SQL。 - 对于一个 IDLE 状态 XA 事务,可以执行一个
XA PREPARE
语句或一个XA COMMIT…ONE PHASE
语句,其中XA PREPARE
把分支事务放入 PREPARED 状态。在此点上的XA RECOVER
语句将在其输出中包括事务的 xid 值,因为XA RECOVER
会列出处于 PREPARED 状态的所有 XA 事务。XA COMMIT…ONE PHASE
用于预备和提交事务,也就是转换为一阶段协议,直接提交事务。 - 对于一个 PREPARED 状态的 XA 事务, 可以执行
XA COMMIT
语句来提交或者执行XA ROLLBACK
来回滚 XA 事务。
其中二阶段协议中第一阶段是执行 xa prepare
时候,这时候 MySQL 客户端(TM)向 MySQL 数据库服务器(RM)发出 “prepare 准备提交” 请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成“可以提交”,然后把结果返回给事务管理器。
如果第一阶段中数据库 prepare 成功,那么 MySQL 客户端(TM)向数据库服务器发出“commit” 请求,数据库服务器把事务的“可以提交”状态改为“提交完成”状态,然后返回应答。如果在第一阶段内数据库的操作发生了错误,或者 MySQL 客户端(RM)收不到数据库的回应,则认为事务失败,执行 rollback 回撤所有数据库的事务。
上面例子是在一个数据库节点上运行的一个分支事务,演示了单个数据库上执行 xa 分支事务的流程,但是当操作多个数据库资源时候通常都是使用编程语言,比如 Java 的 JTA 来完成 MySQL 的分布式事务的,下面一个例子用来演示:
首先添加依赖
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
代码:
public class XaDemo {
public static MysqlXADataSource getDataSource(String connStr, String user, String pwd) {
try {
MysqlXADataSource ds = new MysqlXADataSource();
ds.setUrl(connStr);
ds.setUser(user);
ds.setPassword(pwd);
return ds;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] arg) {
String connStr1 = "jdbc:mysql://192.168.0.1:3306/test";
String connStr2 = "jdbc:mysql://192.168.0.2:3306/test";
try {
//从不同数据库获取数据库数据源
MysqlXADataSource ds1 = getDataSource(connStr1, "root", "123456");
MysqlXADataSource ds2 = getDataSource(connStr2, "root", "123456");
//数据库1获取连接
XAConnection xaConnection1 = ds1.getXAConnection();
XAResource xaResource1 = xaConnection1.getXAResource();
Connection connection1 = xaConnection1.getConnection();
Statement statement1 = connection1.createStatement();
//数据库2获取连接
XAConnection xaConnection2 = ds2.getXAConnection();
XAResource xaResource2 = xaConnection2.getXAResource();
Connection connection2 = xaConnection2.getConnection();
Statement statement2 = connection2.createStatement();
//创建事务分支的xid
Xid xid1 = new MysqlXid(new byte[] { 0x01 }, new byte[] { 0x02 }, 100);
Xid xid2 = new MysqlXid(new byte[] { 0x011 }, new byte[] { 0x012 }, 100);
try {
//事务分支1关联分支事务sql语句
xaResource1.start(xid1, XAResource.TMNOFLAGS);
int update1Result = statement1.executeUpdate("update account_from set money=money - 50 where id=1");
xaResource1.end(xid1, XAResource.TMSUCCESS);
//事务分支2关联分支事务sql语句
xaResource2.start(xid2, XAResource.TMNOFLAGS);
int update2Result = statement2.executeUpdate("update account_to set money= money + 50 where id=1");
xaResource2.end(xid2, XAResource.TMSUCCESS);
// 两阶段提交协议第一阶段
int ret1 = xaResource1.prepare(xid1);
int ret2 = xaResource2.prepare(xid2);
// 两阶段提交协议第二阶段
if (XAResource.XA_OK == ret1 && XAResource.XA_OK == ret2) {
xaResource1.commit(xid1, false);
xaResource2.commit(xid2, false);
System.out.println("reslut1:" + update1Result + ", result2:" + update2Result);
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
如上代码对两个机器上的数据库进行转账操作。
七、事务管理器 Atomikos 实现的分布式事务
Atomikos 是对上节介绍的 Java JTA 的一个封装,Atomikos 产品分两个版本:
- TransactionEssentials:开源的免费产品
- ExtremeTransactions:上商业版,需要收费
本文我们使用开源的产品,本文写了一个 SpringBoot demo,目录结构:
首先需要在 pom 添加下面依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.25</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jta-atomikos -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
<version>1.2.1.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.atomikos/transactions-jta -->
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jta</artifactId>
<version>4.0.6</version>
</dependency>
然后第三部分的 applicationContext.xml 内容如下:
<!-- mysql数据源1 -->
<bean id="mysqlDataSource1" class="com.atomikos.jdbc.AtomikosDataSourceBean"
init-method="init" destroy-method="close">
<description>master xa datasource</description>
<property name="uniqueResourceName">
<value>master</value>
</property>
<property name="xaDataSourceClassName"
value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource" />
<property name="xaProperties">
<props>
<prop key="user">root</prop>
<prop key="password">123456</prop>
<prop key="URL">jdbc:mysql://192.168.0.1:3306/test</prop>
</props>
</property>
<property name="poolSize" value="10" />
</bean>
<!-- mysql数据源2 -->
<bean id="mysqlDataSource2" class="com.atomikos.jdbc.AtomikosDataSourceBean"
init-method="init" destroy-method="close">
<description>slave xa datasource</description>
<property name="uniqueResourceName">
<value>slave</value>
</property>
<property name="xaDataSourceClassName">
<value>com.mysql.jdbc.jdbc2.optional.MysqlXADataSource</value>
</property>
<property name="xaProperties">
<props>
<prop key="user">root</prop>
<prop key="password">123456</prop>
<prop key="URL">jdbc:mysql://192.168.0.2:3306/test</prop>
</props>
</property>
<property name="poolSize" value="10" />
</bean>
<!-- atomikos事务管理器 -->
<bean id="atomikosTransactionManager"
class="com.atomikos.icatch.jta.UserTransactionManager"
init-method="init" destroy-method="close">
<description>UserTransactionManager</description>
<property name="forceShutdown">
<value>true</value>
</property>
</bean>
<!-- atomikos用户事务管理 -->
<bean id="atomikosUserTransaction"
class="com.atomikos.icatch.jta.UserTransactionImp">
<property name="transactionTimeout" value="300" />
</bean>
<!-- spring 事务管理器 -->
<bean id="springTransactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager">
<ref bean="atomikosTransactionManager" />
</property>
<property name="userTransaction">
<ref bean="atomikosUserTransaction" />
</property>
</bean>
<!-- mysql数据源1对应的jdbc模板 -->
<bean id="Mysql1JdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg>
<ref bean="mysqlDataSource1" />
</constructor-arg>
</bean>
<!-- mysql数据源2对应的jdbc模板 -->
<bean id="Mysql2JdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg>
<ref bean="mysqlDataSource2" />
</constructor-arg>
</bean>
<context:annotation-config />
<tx:annotation-driven
transaction-manager="springTransactionManager" />
其中首先创建了两个 MySQL 数据库对应的 XA 数据源,然后创建了 Atomikos 事务管理器,和用户事务管理器,然后创建了 Spring 事务管理器,最后分别创建了 XA 数据源对应的 JDBC 模板用来具体执行 SQL。
其中第二部分是具体使用 JDBC 模板执行 SQL,TestBoImpl 的代码如下:
@Configuration
public class TestBoImpl implements TestBo {
@Resource(name = "Mysql1JdbcTemplate")
private JdbcTemplate mysql1JdbcTemplate;
@Resource(name = "Mysql2JdbcTemplate")
private JdbcTemplate mysql2JdbcTemplate;
@Override
public boolean branchOneExecute() {
mysql1JdbcTemplate.execute("update account_from set money=money - 50 where id=1");
return true;
}
@Override
public boolean branchTwoExecute() {
mysql2JdbcTemplate.execute("update account_to set money= money + 50 where id=1");
return true;
}
}
其中第一部分是 Boot 的启动类,代码如下:
@RestController
@SpringBootApplication(exclude = { MybatisAutoConfiguration.class })
@ComponentScan(basePackages = { "com.learn.java.bo.impl" })
@ImportResource("applicationContext.xml")
public class App {
@Resource(name = "springTransactionManager")
private JtaTransactionManager txManager;
@Autowired
private TestBo testBo;
@RequestMapping("/home") String home() {
return "Hello World!";
}
@RequestMapping("/test")
String test() {
UserTransaction userTransaction = txManager.getUserTransaction();
try {
userTransaction.begin();
testBo.branchOneExecute();
// if(false)
// throw new RuntimeException();
testBo.branchTwoExecute();
try {
userTransaction.commit();
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
try {
userTransaction.rollback();
} catch (Exception e1) {
e1.printStackTrace();
}
}
return "Hello World!";
}
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
其中方法 test 是测试类,其中首先调用 userTransaction.begin();
开启了一个 XA 分布式事务,然后具体执行不同数据库的事务分支,然后调用 userTransaction.commit();
提交事务。
2019年01月23日,阿里开源了分布式事务框架GTS,有兴趣可以去了解,实践一下