内容简介: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分布式文件系统使用指南
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Mathematica Cookbook
Sal Mangano / O'Reilly Media / 2009 / GBP 51.99
As the leading software application for symbolic mathematics, Mathematica is standard in many environments that rely on math, such as science, engineering, financial analysis, software development, an......一起来看看 《Mathematica Cookbook》 这本书的介绍吧!