分布式事务XA和JTA
在分布式系统中,事务的ACID原则是否能够保证呢?
答案是不能,Not even close! 以原子性为例,在有多个系统的分布式系统中,一个分布式事务是在不同的系统内部执行的,我们没有办法保证它们能够同时完成,或者都不做。
Spring中本地事务和全局事务
- 本地事务
是由Spring容器创建和维护的事务。例如在使用JDBC事务操作数据库的时候,spring容器会在需要的时候创建事务的上下文,开启一个JDBC的事务,然后调用业务方法,执行完成后,调用commit方法;然后在出错的时候调用资源的rollback方法。还有事务的传播、隔离等也都是由Spring容器来提供。本地事务只能针对一个资源实现完全的事务控制。如果要在一个本地事务中操作两个资源(例如两个数据库),实际上先后在两个数据库的Connection上调用commit()方法去提交。 - 全局事务(外部事务)
就是spring只负责通过事务的接口来开始事务、提交事务、回滚事务,而具体的操作还是得有外部提供的事务管理的模块或组件来执行和维护。例如我们使用JBoss来运行我们的web应用,然后在JBoss上配置了JTA的事务。那么事务的具体管理和维护就是由JBoss提供的事务管理模块来进行。
jmsTransaction.begin(); // get transactions from jms session
dbTransaction.begin(); // get transactions from JDBC connection
try {
orderRepository.save(order);
jmsTemplate.convertAndSend("order:need_to_pay", dto);
dbTransaction.commit();
jmsTransaction.commit();
} catch(Exception e) {
dbTransaction.rollback();
jmsTransaction.rollback();
}
这样,如果上述代码在jmsTransaction.commit();的时候出错,这时候数据库的事务已经提交,就无法回滚。如果这时候这个方法被重新执行,数据库的操作就会被重复执行。
如果我们使用外部事务,那么这里就不会针对两个资源出现两个事务,而是只有一个事务,来统一管理多个资源。如果在多个资源上的事务出错了,外部的事务也能够保证回滚,这是通过事务的两阶段提交(2PC)来实现。使用JTA实现的事务正是这种外部事务。
由于JTA使用两阶段提交来实现多个资源之间的事务,这就会带来很大的性能问题。因为它要同步多个资源的事务,对每个资源使用两阶段提交,这就使得这个事务所花的时间比本地事务多很多。而且在这个时间段内,由于事务的隔离性,可能会造成长时间的资源占用,使得其它的事务无法同步访问该资源上的一些数据。
XA
XA是由X/Open组织提出的分布式事务的架构(或者叫协议)。XA架构主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。也就是说,在基于XA的一个事务中,我们可以针对多个资源进行事务管理,例如一个系统访问多个数据库,或即访问数据库、又访问像消息中间件这样的资源。这样我们就能够实现在多个数据库和消息中间件直接实现全部提交、或全部取消的事务。XA规范不是java的规范,而是一种通用的规范,
什么是JTA
JTA(Java Transaction API),是J2EE的编程接口规范,它是XA协议的JAVA实现。它主要定义了:
一个事务管理器的接口javax.transaction.TransactionManager,定义了有关事务的开始、提交、撤回等操作。
一个满足XA规范的资源定义接口javax.transaction.xa.XAResource,一种资源如果要支持JTA事务,就需要让它的资源实现该XAResource接口,并实现该接口定义的两阶段提交相关的接口。
JTA事务
Java 事务编程接口(JTA:Java Transaction API)和 Java 事务服务 (JTS;Java Transaction Service) 为 J2EE 平台提供了分布式事务服务。分布式事务(Distributed Transaction)包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager )。我们可以将资源管理器看做任意类型的持久化数据存储;事务管理器承担着所有事务参与单元的协调与控制。JTA 事务有效的屏蔽了底层事务资源,使应用可以以透明的方式参入到事务处理中;但是与本地事务相比,XA 协议的系统开销大,在系统开发过程中应慎重考虑是否确实需要分布式事务。若确实需要分布式事务以协调多个事务资源,则应实现和配置所支持 XA 协议的事务资源,如 JMS、JDBC 数据库连接池等。使用 JTA 处理事务的示例如***意:connA 和 connB 是来自不同数据库的连接)
要想使用用 JTA 事务,那么就需要有一个实现 javax.sql.XADataSource 、 javax.sql.XAConnection 和 javax.sql.XAResource 接口的 JDBC 驱动程序。一个实现了这些接口的驱动程序将可以参与 JTA 事务。一个 XADataSource 对象就是一个 XAConnection 对象的工厂。XAConnection 是参与 JTA 事务的 JDBC 连接。
要使用JTA事务,必须使用XADataSource来产生数据库连接,产生的连接为一个XA连接。
XA连接(javax.sql.XAConnection)和非XA(java.sql.Connection)连接的区别在于:XA可以参与JTA的事务,而且不支持自动提交。
JTA的优点很明显,就是提供了分布式事务的解决方案,严格的ACID。
虽然JTA事务是Java提供的可用于分布式事务的一套API,但是不同的J2EE平台的实现都不一样,并且都不是很方便使用,所以,一般在项目中不太使用这种较为负责的API。现在业内比较常用的分布式事务解决方案主要有异步消息确保型、TCC、最大努力通知
public void transferAccount() {
UserTransaction userTx = null;
try{
// 获得 Transaction 管理对象
userTx = (UserTransaction)getContext().lookup("\
java:comp/UserTransaction");
// 启动事务
userTx.begin();
// 将 A 账户中的金额减少 500
stmtA.execute(sql1);
// 将 B 账户中的金额增加 500
stmtB.execute(sql2);
// 提交事务
userTx.commit();
// 事务提交:转账的两步操作同时成功(数据库 A 和数据库 B 中的数据被同时更新)
} catch(SQLException sqle){
// 发生异常,回滚在本事务中的操纵
userTx.rollback();
// 事务回滚:转账的两步操作完全撤销
//( 数据库 A 和数据库 B 中的数据更新被同时撤销)
} catch(Exception ne){
e.printStackTrace();
}
}
- 开始事务的时候,TransactionManager 会创建一个 Transaction 事务对象(标志着事务的开始)并把此对象通过 ThreadLocale 关联到当前线程,支持事务的数据源与普通的数据源是不同的,它实现了额外的 XADataSource 接口。
- 当对一个资源进行操作的时候,实现了XADataSource接口的资源会去检测当前线程是否有之前设置的Transaction事务对象,有就把当前的XAResource资源加入到事务中,
public void commit() throws RollbackException, HeuristicMixedException,
HeuristicRollbackException, SecurityException,
IllegalStateException, SystemException {
// 得到当前事务中的所有事务资源
List<XAResource> list = getAllEnlistedResouces();
// 通知所有的事务资源管理器,准备提交事务
// 对于生产级别的实现,此处需要进行额外处理以处理某些资源准备过程中出现的异常
for(XAResource xa : list){
xa.prepare();
}
// 所有事务性资源,提交事务
for(XAResource xa : list){
xa.commit();
}
}
分布式系统的原则
对于分布式系统来说,很难有一个类似ACID这样的标准
CAP定理,包括以下几个方面:
- 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。比如说,在购票流程处理的过程中,如果用户看到自己的余额以及被扣了,那么它应该也能看到票夹里的票、以及支付完成的订单。
- 可用性(A):可用性是指系统提供的服务必须一直处于可用的状态,包括每个请求都应该在一定的时间内返回结果。它包括时间和结果两个条件,也就是说,即使出现错误、超时等问题,也应该是一定的时间内给用户反馈。
- 分区容错性(P):如果集群系统中有一部分服务发生故障,仍然能够保证对外提供满足一致性和可用性的服务。也就是说,集群中一部分节点故障后,集群整体还是能响应客户端的读写请求。
由于分布式系统形式的多样性和复杂性,如果想完全满足上述的原则设计一个分布式系统,几乎是不可能的。首先,分布式服系统就是要把系统的各个部分部署到不同的服务器上,那我们就必须要通过分区容错来避免由于网络、机器故障等原因造成的问题。所以分区容错性是必不可少的,否则可用性都无法保证。
对于可用性来说,如果我们要严格保证可用性,即使是在分区容错性得到保障的前提下,所有的服务都是可用的,有时候,我们也需要通过异步的方式来处理一些业务,这就会造成数据的不一致。如已经从用户账户上扣费,但是票还没有转移完成等。
再来看一致性,是否有办法能够实现呢?那我们就需要先来看看几种一致性:
- 强一致:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这就像本地事务的原子性(A)和隔离性(I)的统一。在分布式系统中,如果一个业务处理需要多个系统都更新数据,那就要求多个系统的更新同时完成。但是,因为它们的不同的系统,‘同时完成‘需要服务间的协作、同步才能完成,在完成之前,用户不能看到更新后的数据,也不能看到更新前的(因为要强一致),所以用户只能等待。这就违背了可用性;同时,为了保证强一致性,需要做很多额外的工作,又大大增加了出错的可能性。所以在分布式系统中,强一致性一般都无法实现。
- 弱一致性:系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。例如上面的例子,我们有几个子系统,当订单系统生成订单,然后交由用户系统处理的时候,这时候用户就能够看到自己的新的订单。当票务系统处理票的转移的时候,用户能看到已经扣费,但是又看不到票夹里的票。虽然这个时间可能很短,但是也是存在的。
- 最终一致性:弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。例如上面的例子中, 我们用异步方式处理用户的购票,先生成订单、扣费,异步处理票,返回给用户结果,这时用户看到的订单状态是正在处理,只有整个流程处理完了,用户才能看到订单结束,并且能看到买到的票。除了异步操作造成的一致性问题以外,还有在某一个节点发生故障的情况下,通过重试、取消等机制,或者人工参与,使得系统的数据也能最终达到一个一致的状态。
在一般的分布式系统的设计中,我们大都以最终一致性为目标,来设计我们的分布式事务。这既能保证系统的可用性和容错性,也能在绝大多数情况下保证数据的弱一致性,并且在少数出错或网络高延迟的情况下,也能保证数据的最终一致性。
参考:http://codin.im/2017/05/14/rest-micro-services-distributed-trasaction-1-jta/