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中监听一个消息,进行一系列操作后更新数据库,大致流程如下:

  1. Start messaging transaction
  2. Receive message
  3. Start database transaction
  4. Update database
  5. Commit database transaction
  6. Commit messaging transaction

如果在第4步发生错误:

  1. Start messaging transaction
  2. Receive message
  3. Start database transaction
  4. Update database, fail!
  5. Roll back database transaction
  6. Roll back messaging transaction

这时候,数据的操作会被回滚,消息也会被重新放回消息中间件中,然后再重新触发这个方法,开始一个新的事务。这样就保证了数据的原子性,也就是都提交成功,或都失败。

但是,这里所说的原子性,只有在使用XA的情况下才能保证。如果我们使用XA,虽然这里有database transactionmessaging transaction,但是XA会保证他们在一个事务中提交。

7种实现方式

使用XA和两阶段提交

上面说了,使用XA,可以保证database transactionmessaging 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方式的配置,可以用如下方式来配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"
depends-on="brokerService">
<property name="brokerURL" value="vm://localhost?async=false" />
</bean>
<bean id="brokerService" class="org.apache.activemq.broker.BrokerService" init-method="start"
destroy-method="stop">
...
<property name="persistenceAdapter">
<bean class="org.apache.activemq.store.jdbc.JDBCPersistenceAdapter">
<property name="dataSource">
<bean class="com.springsource.open.jms.JmsTransactionAwareDataSourceProxy">
<property name="targetDataSource" ref="dataSource"/>
<property name="jmsTemplate" ref="jmsTemplate"/>
</bean>
</property>
<property name="createTablesOnStartup" value="true" />
</bean>
</property>
</bean>

这里设置了activeMQ的BrokerService,给他设置一个数据库的代理存储。然后在使用jmsTemplate时,启用它的sessionTransacted

如果要使用其他消息中间件,或其他资源,需要其提供这种方式才能使用。而且,还需要考虑使用数据库作为存储引起的性能改变。

最大努力一次提交

为了说明这种方式,还是看之前的实例,即在一个系统中使用数据库和消息中间件,业务流程如下:

  1. Start messaging transaction
  2. Receive message
  3. Start database transaction
  4. Update database
  5. Commit database transaction
  6. Commit messaging transaction

在这里,有两个事务,分别是DB的和JMS的事务,事务的开启和提交都是相互独立的。我们依次提交这两个事务,只要第二个事务顺利提交,整个方法就能够保证数据的一致性。实际上,在绝大多数情况下,只要数据库和MQ能够正常访问,这也确实能够保证。所以,这种方式就叫’最大努力’一次提交。(两个事务都是一阶段提交)。

使用这种方式,事物提交的顺序是非常重要的。假设在提交messaging transaction的时候发生错误,这时数据库的事务已经提交,无法回滚,但是消息的事务被回滚,那么这一条消息会被重新放回队列中,该业务方法会被再次触发,再次在一个新的事务中处理。但是,这时数据的处理已经完成,只是最后JMS的事物提交出错,那么就需要通过防止重复提交的方式,来避免数据库的再次处理。修改后的流程如下:

  1. Start messaging transaction
  2. Receive message
  3. if (! duplicate trigger) {
  4. Start database transaction
  5. Update database
  6. Commit database transaction
  7. }
  8. Commit messaging transaction

也就是在处理重复触发的方法的时候,略过DB操作,直接消费消息并提交。

所以,我们必须保证messaging transaction的提交放在后面,才能够保证数据最终的一致性。

这种方式虽然保险在两个数据资源上分别使用一次提交,但是,对于成熟的消息中间件来说,读取消息后,提交事务(也就是对该消息的消费发送确认)发生错误的概率应该很小,比如在读取消息后、在确认之前MQ服务器发生错误或网络故障。即使出错,这个消息也不会丢失,我们也可以通过其他方式处理重复提交。

在另一篇文章 REST微服务的分布式事务实现-基于消息中间件 中有专门针对这种方式有详细介绍。

Spring和message-driven POJOs

(这个不知道该怎么翻译了。。。)
上面说的共享资源的方式是JMS使用DB作为存储,而这种方式是配置JMS的链接,让它的事物和DB的事物同步。它通过这种方式配置:

1
2
3
4
5
6
7
8
9
<bean id="connectionFactory"
class="org.springframework.jms.connection.TransactionAwareConnectionFactoryProxy">
<property name="targetConnectionFactory">
<bean class="org.apache.activemq.ActiveMQConnectionFactory" depends-on="brokerService">
<property name="brokerURL" value="vm://localhost"/>
</bean>
</property>
<property name="synchedLocalTransactionAllowed" value="true" />
</bean>

它使用TransactionAwareConnectionFactoryProxy配置JMS的ConnectionFactory并通过AOP来实现JMS的事务和DB的事务同步,也就是DB事务的提交也会触发JMS事务的提交。

这种方式应该是同时使用MQ和DB时实现分布式事务的最好的方式,既不会影响MQ数据存储的性能,也能通过一次提交实现对两个资源的同步。

链式事务管理

这种方式也是Spring提供的,可以将两个或多个数据库资源的事务串联到一起,来公用一个TransactionManager来实现对多个资源的事务。配置方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean id="transactionManager" class="com.springsource.open.db.ChainedTransactionManager">
<property name="transactionManagers">
<list>
<bean
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="otherDataSource" />
</bean>
</list>
</property>
</bean>

可以看出,它是针对多个数据库资源实现事务。使用这种方式时,在Spring事务提交的时候,它会依次(按照定义的顺序逆序)调用里面的多个Connection的commit()方法,如果业务方法出错,就会依次调用rollback()方法。

既然是依次执行,就还是有可能会出现先前的提交成功,之后的提交失败,所以还是会有事务失败的可能。

如何选择

有那么多种方式,该如何选择?首先,你需要对这些实现方式、背后的原理、实现逻辑都有一个清晰的认识,然后再根据自己的具体情况,选择合适的实现方式。

Spring的团队推荐使用最大努力一次提交的方式(在做这些那篇文章的时候,也就是2009年。不过,似乎现在Spring也推荐这种方式)。也就是使用一个事务管理器,依次提交两个资源的事物。

坚持原创技术分享,您的支持将鼓励我继续创作!