内容简介:Spring的分布式事务实现-使用和不使用XA(翻译)
说到 Java 实现分布式系统,网上的很多资料很多都会引用Java World上的一篇文章 Distributed transactions in Spring, with and without XA 。虽然这篇文章发布于2009年,现在的分布式系统也比那时候复杂了很多,但是对于分布式事务的实现来说,其实现方法也离不开这篇文章中介绍的方法。所以这里对这篇文章做一个翻译,希望能对大家理解分布式事务有所帮助。翻译的过程中,我将根据我的理解来表述原文的意思,而不是一句一句的直接翻译。因为本人水平有限,如果表述的有不恰当甚至错误的地方,希望读者能及时指出。
有关Spring事务和JTA的介绍,可以参考另一篇文章 REST微服务的分布式事务实现-分布式事务以及JTA介绍 。
在Java系统中实现分布式事务,通常都会使用JTA,而Spring为我们提供了一个通用的事务抽象来使用JTA。由于JTA遵从XA规范,也就是两阶段提交。由于实现XA的成本非常高,所以我们就可以在有些情况下来使用非XA的方式实现分布式事务。
这篇文章中,提出了7中实现分布式事务的方法,有使用XA的方法,也有不使用XA的方法。这几种方法的顺序是以安全性和可靠性来排序。第一种方法就是最安全可靠的,能完全保证数据的一致性和原子性。越往后,可靠性和数据的一致性就会降低。但是,性能却基本上是相反,越往后的实现方法,系统的性能会越高。
数据的原子性
本文所说的分布式事务,说的是一个系统需要操作多个数据资源的时候,实现的事务,而不是多个服务之间实现分布式事务。而这个’资源’需要支持事务,就需要它实现 rollback()
, commit()
方法。而支持XA的资源,就需要实现 XAResource
接口里面定义的方法,否则就无法实现两阶段提交。
在一个典型的例子中,一个系统需要同时操作数据库和MQ,比如,从JMS中监听一个消息,进行一系列操作后更新数据库,大致流程如下:
- Start messaging transaction
- Receive message
- Start database transaction
- Update database
- Commit database transaction
- Commit messaging transaction
如果在第4步发生错误:
- Start messaging transaction
- Receive message
- Start database transaction
- Update database, fail!
- Roll back database transaction
- Roll back messaging transaction
这时候,数据的操作会被回滚,消息也会被重新放回消息中间件中,然后再重新触发这个方法,开始一个新的事务。这样就保证了数据的原子性,也就是都提交成功,或都失败。
但是,这里所说的原子性,只有在使用XA的情况下才能保证。如果我们使用XA,虽然这里有 database transaction
和 messaging transaction
,但是XA会保证他们在一个事务中提交。
7种实现方式
使用XA和两阶段提交
上面说了,使用XA,可以保证 database transaction
和 messaging transaction
在一个事务中提交,这是由XA的两阶段提交来实现的。也正是因为XA事务的提交分为两个阶段提交,才会产生性能问题。首先,它为了两阶段提交需要做很多额外的操作;其次,因为在一个事务中,会通过锁等机制来保证隔离性。现在有2个数据库的操作在一个事务中,这就使得锁的时间大大加长。
如果使用Spring的事务,它对JTA进行了抽象,我们就可以在不修改业务代码的情况下,在Spring本地事物和JTA事务之前方便的切换。
使用XA和一阶段优化
有些情况下,如果只有一个资源,如只操作一个数据库,又使用了JTA,那么一些应用服务器会针对这种情况做优化,使用一阶段提交来使用XA。
XA和最后资源博弈
不知道这样翻译对不对。这种方式的意思是说,如果一个系统需要访问2个资源,即使它们都支持XA的事务,但是,只在一个资源上启用XA,提交的时候,将没有启用XA的资源的提交放在最后。这样,用非XA的事物来控制前面的XA的事物提交。这样就在一定程度上避免的事务造成的资源竞争。
上面三种都是属于使用XA实现,接下来的几种方式,就是不是用XA的方式。
共享资源
这种方式,简单来说,就是对于2个资源,在底层实际上使用同一个资源。例如,一个系统使用一个DB一个MQ,对于一些消息中间件,它支持使用数据库来作为它的存储层。这样,我们对JMS和DB都使用底层的同一个资源,这样就能使用同一个资源上的事务。如ActiveMQ就支持使用数据库作为存储。
如果使用以前的XML方式的配置,可以用如下方式来配置:
<beanid="connectionFactory"class="org.apache.activemq.ActiveMQConnectionFactory" depends-on="brokerService"> <propertyname="brokerURL"value="vm://localhost?async=false"/> </bean> <beanid="brokerService"class="org.apache.activemq.broker.BrokerService"init-method="start" destroy-method="stop"> ... <propertyname="persistenceAdapter"> <beanclass="org.apache.activemq.store.jdbc.JDBCPersistenceAdapter"> <propertyname="dataSource"> <beanclass="com.springsource.open.jms.JmsTransactionAwareDataSourceProxy"> <propertyname="targetDataSource"ref="dataSource"/> <propertyname="jmsTemplate"ref="jmsTemplate"/> </bean> </property> <propertyname="createTablesOnStartup"value="true"/> </bean> </property> </bean>
这里设置了activeMQ的BrokerService,给他设置一个数据库的代理存储。然后在使用 jmsTemplate
时,启用它的 sessionTransacted
。
如果要使用其他消息中间件,或其他资源,需要其提供这种方式才能使用。而且,还需要考虑使用数据库作为存储引起的性能改变。
最大努力一次提交
为了说明这种方式,还是看之前的实例,即在一个系统中使用数据库和消息中间件,业务流程如下:
- Start messaging transaction
- Receive message
- Start database transaction
- Update database
- Commit database transaction
- Commit messaging transaction
在这里,有两个事务,分别是DB的和JMS的事务,事务的开启和提交都是相互独立的。我们依次提交这两个事务,只要第二个事务顺利提交,整个方法就能够保证数据的一致性。实际上,在绝大多数情况下,只要数据库和MQ能够正常访问,这也确实能够保证。所以,这种方式就叫’最大努力’一次提交。(两个事务都是一阶段提交)。
使用这种方式,事物提交的顺序是非常重要的。假设在提交 messaging transaction
的时候发生错误,这时数据库的事务已经提交,无法回滚,但是消息的事务被回滚,那么这一条消息会被重新放回队列中,该业务方法会被再次触发,再次在一个新的事务中处理。但是,这时数据的处理已经完成,只是最后JMS的事物提交出错,那么就需要通过防止重复提交的方式,来避免数据库的再次处理。修改后的流程如下:
- Start messaging transaction
- Receive message
- if (! duplicate trigger) {
- Start database transaction
- Update database
- Commit database transaction
- Commit messaging transaction
也就是在处理重复触发的方法的时候,略过DB操作,直接消费消息并提交。
所以,我们必须保证 messaging transaction
的提交放在后面,才能够保证数据最终的一致性。
这种方式虽然保险在两个数据资源上分别使用一次提交,但是,对于成熟的消息中间件来说,读取消息后,提交事务(也就是对该消息的消费发送确认)发生错误的概率应该很小,比如在读取消息后、在确认之前MQ服务器发生错误或网络故障。即使出错,这个消息也不会丢失,我们也可以通过其他方式处理重复提交。
在另一篇文章 REST微服务的分布式事务实现-基于消息中间件 中有专门针对这种方式有详细介绍。
Spring和message-driven POJOs
(这个不知道该怎么翻译了。。。)
上面说的共享资源的方式是JMS使用DB作为存储,而这种方式是配置JMS的链接,让它的事物和DB的事物同步。它通过这种方式配置:
<beanid="connectionFactory" class="org.springframework.jms.connection.TransactionAwareConnectionFactoryProxy"> <propertyname="targetConnectionFactory"> <beanclass="org.apache.activemq.ActiveMQConnectionFactory"depends-on="brokerService"> <propertyname="brokerURL"value="vm://localhost"/> </bean> </property> <propertyname="synchedLocalTransactionAllowed"value="true"/> </bean>
它使用 TransactionAwareConnectionFactoryProxy
配置JMS的 ConnectionFactory
并通过AOP来实现JMS的事务和DB的事务同步,也就是DB事务的提交也会触发JMS事务的提交。
这种方式应该是同时使用MQ和DB时实现分布式事务的最好的方式,既不会影响MQ数据存储的性能,也能通过一次提交实现对两个资源的同步。
链式事务管理
这种方式也是Spring提供的,可以将两个或多个数据库资源的事务串联到一起,来公用一个 TransactionManager
来实现对多个资源的事务。配置方式如下:
<beanid="transactionManager"class="com.springsource.open.db.ChainedTransactionManager"> <propertyname="transactionManagers"> <list> <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <propertyname="dataSource"ref="dataSource"/> </bean> <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <propertyname="dataSource"ref="otherDataSource"/> </bean> </list> </property> </bean>
可以看出,它是针对多个数据库资源实现事务。使用这种方式时,在Spring事务提交的时候,它会依次(按照定义的顺序逆序)调用里面的多个Connection的 commit()
方法,如果业务方法出错,就会依次调用 rollback()
方法。
既然是依次执行,就还是有可能会出现先前的提交成功,之后的提交失败,所以还是会有事务失败的可能。
如何选择
有那么多种方式,该如何选择?首先,你需要对这些实现方式、背后的原理、实现逻辑都有一个清晰的认识,然后再根据自己的具体情况,选择合适的实现方式。
Spring的团队推荐使用 最大努力一次提交
的方式(在做这些那篇文章的时候,也就是2009年。不过,似乎现在Spring也推荐这种方式)。也就是使用一个事务管理器,依次提交两个资源的事物。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用Redis实现分布式锁
- 使用Redis实现分布式锁
- Spring Boot中使用WebSocket总结(三):使用消息队列实现分布式WebSocket
- [译] Spring 的分布式事务实现 — 使用和不使用 XA — 第一部分
- 使用注解形式实现 Redis 分布式锁
- Hadoop分布式文件系统使用指南
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
面向模式的软件体系结构(卷1) (平装)
Frank Buschmann、Regine meunier、Hans Rohnert、Peter Sommerlad、Michael Stal / 贲可荣、郭福亮 / 机械工业出版社 / 2003-1 / 45.0
一起来看看 《面向模式的软件体系结构(卷1) (平装)》 这本书的介绍吧!