翻译自 http://www.javaworld.com/javaworld/jw-01-2009/jw-01-spring-transactions.html?page=1
在Spring中常常使用JTA以及XA协议来实现分布式事务,不过我们也有其他选项。最佳实现取决于你的应用场景,比如使用什么类型的资源,如何在性能、安全、可靠性和数据完整性之间权衡。在这个系列文章中,来自SpringSource的David Syer将详细讲解Spring应用程序可以使用的7种分布式事务模式,其中3种使用XA,4种不使用XA。
级别: 中级
Spring框架对JTA(Java事务应用接口即Java Transaction API)的支持,使得应用程序可以在没有运行J2EE容器的情况下使用分布式事务和XA协议。然而即便如此,XA对于管理员来说仍然是昂贵并且可能会是不可靠或者笨重的。它可能会带来初次体验的惊喜,但最终不少类型的应用都会避免使用它。
为了帮助你理解实现分布式事务的各种方法,我将分析7种分布式事务处理模式,提供具体的示范代码,我将按照安全性和可靠性由高到低的方式来逐个表述,从那些在最常用情况下能最大程度保证数据完整性和原子性的方法开始。当你依次往下阅读时,注意事项和使用限制会逐渐增多。另外这些模式基本上也是按照运行成本倒叙安排(从成本最高的开始)。这些模式都是架构或者技术模式,而不是业务模式, 所以我不会关注于具体业务,而只是提供说明各个模式如何工作的简短代码。
注意只有开始的3个模式使用了XA,而这些由于性能原因可能并不合适或不可接受。我并不会扩展讨论XA模式,因为XA已经在其他文章中被充分讨论,尽管我在第一个模式中提供了简单的示范代码。通过阅读这篇文章,你将学习到你能通过分布式事务做到什么,不能做什么以及何时如何避免使用XA - 以及什么情况下应该用XA。
分布式事务和原子性
分布式事务(distributed transaction) 指的是包含多个事务资源的事务。事务资源指的是比如和关系型数据库和消息中间件通讯的连接器。通常这样的资源提供一些API比如begin()
, rollback()
, commit()
. 在Java编程中, 事务资源通常显露为一个由底层平台提供的工厂产品:对于数据库而言,它是一个由DataSource产生的连接,或者Java Persistence API (JPA) EntityManager
;对于Java Message Service (JMS)而言,它是一个会话(Session)
.
典型的一个用例, JMS消息触发一个数据库更新。一个成功的交互序列如下所示:
- 开始一个消息事务
- 获取消息
- 开始数据库事务
- 更新数据库
- 提交数据库事务
- 提交消息事务
如果一个数据库错误比如在更新时遇到约束冲突,理想的交互序列看起来如下:
1. 开始消息事务
2. 接收消息
3. 开始数据库事务
4. 更新数据库, 失败!
5. 回滚数据库事务
6. 回滚消息事务
如上的例子中, 消息在最后的回滚动作发生后传回中间件,并在某个时刻被另外一个事务所接收。这通常是好事,否则你可能无法记录失败。(自动重试和异常处理的机制不在本文讨论范围)
上面两个程序流最重要的特征在于它们是原子性的, 形成单个逻辑事务,要么全部成功,要么全部失败。
但究竟怎么保证程序流和上面的两种情况类似呢?必须使用到一些事务资源之间的同步,这样如果一个提交,那么全部提交,反之亦然。由于包含了多个事务资源,所以事务是分布式的,如果不采取同步措施,那么一定不会是原子性的。分布式事务技术和概念上的困难均在于资源之间的同步(或缺少同步)。
下面讨论的前面的3个模式是基于XA协议的。由于这些模式被广泛讨论过,我不会涉及太多的细节。熟悉XA模式的读者可以直接跳到共享事务资源模式(Shared Transaction Resource pattern)。
使用两阶段提交方式的完整XA(Full XA with 2PC)
如果需要‘防弹级别’的保护比如事务需要从服务中断中恢复, 甚至包括服务器崩溃,那么完整XA(Full XA)是你唯一的选择。在这个例子中用来同步事务的共享资源是一个特殊的事务管理器,用来协调使用XA协议的进程的信息。在Java中,从开发者角度来看,协议是通过JTA UserTransaction来暴露的。
做为一个系统接口,XA的能力大部分开发者尚不了解。他们需要知道有这么一个XA,能做什么,有什么代价,如何使用事务资源。代价来自于两阶段提交协议two-phase commit (2PC) ,事务管理器使用该协议来确保所有的资源在事务结束前对处理结果达成一致。
如果是Spring应用,会使用Spring JtaTransactionManager和Spring声明式事务管理(declarative transaction management) 来隐藏底层同步的技术细节。使用XA或不用XA之间的区别仅在于配置工厂资源:数据源(DataSource)实例,和应用程序事务管理器。这篇文档包含一个示范应用(atomikos-db项目)演示了这个配置。数据源(DataSource)实例和事务管理器是仅有的XA-或JTA-相关元素。
想了解示范应用是如何工作的,可以运行com.springsource.open.db下面的单元测试用例。MultipleDataSourceTests类插入数据到两个数据源中,然后使用Spring整合支持功能回滚这个事务,如列表1中所示:
列表1. 事务回滚
- @Transactional
- @Test
- public void testInsertIntoTwoDataSources() throws Exception {
- int count = getJdbcTemplate().update(
- "INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", 0,
- "foo");
- assertEquals(1, count);
- count = getOtherJdbcTemplate()
- .update(
- "INSERT into T_AUDITS (id,operation,name,audit_date) values (?,?,?,?)",
- 0, "INSERT", "foo", new Date());
- assertEquals(1, count);
- // Changes will roll back after this method exits
- }
上述操作在发生更新错误的时候,函数退出时会执行数据回滚。
然后MulipleDataSourceTests
检查两个操作是否都被回滚,如列表2所示:
列表2. 确认回滚
- @AfterTransaction
- public void checkPostConditions() {
- int count = getJdbcTemplate().queryForInt("select count(*) from T_FOOS");
- // This change was rolled back by the test framework
- assertEquals(0, count);
- count = getOtherJdbcTemplate().queryForInt("select count(*) from T_AUDITS");
- // This rolled back as well because of the XA
- assertEquals(0, count);
- }
为了更好的理解Spring事务管理是如何工作以及如何配置的,请查阅Spring参考指南(Spring Reference Guide)。
使用1阶段提交优化的XA (XA with 1PC Opitimization)
这个模式是一个优化方案,很多事务管理器用来避免如果事务只包含单个资源的时候,两阶段提交(2PC)所带来的过度开销,应用服务器需要能判断出这种情况。
XA和最后资源策略(the Last Resource Gambit)
很多XA事务管理器的另外一个特性是,对于除了一个资源外所有其他资源均为XA兼容的情况,它们同样能提供和全部为XA资源一样的故障恢复保证。这是通过资源排序以及使用非XA资源做为最后的决定性投票。如果它提交失败, 那么所有其他资源可以被回滚。这和100%的无漏洞(bulletproof)模式很接近 -- 但不完全是。而且当它失败时,不会留下太多跟踪信息,除非采用额外的措施(如同一些高端解决方案所实现的那样)。
共享事务资源模式(Shared Transaction Resource pattern)
在一些系统中,一个很好的可以降低复杂性以及提高吞吐量的模式是通过确保所有的事务资源都被同一个资源所支持,从而完全移除对XA的使用依赖。这显然不是一个所有场景都适用的通用模式,但和XA一样健壮而且快得多。分享式事务资源模式是无漏洞的(bulletproof),但是仅适用于特定的平台和处理场景。
关于这个模式,一个简单的,很多人都熟悉的例子是,在一个使用ORM的组件和一个使用JDBC组件之间共享数据库连接。这通常发生在当你使用支持一些ORM工具比如Hibernate, EclipseLink,以及Java Persistence API (JPA)的Spring事务管理器的时候。同样的事务可以安全的跨越于ORM和JDBC组件之间,通常由上面的控制事务的服务层方法所驱动。
另外一个有效使用该模式的例子是基于消息驱动的单个数据库更新(如同本文介绍中所举的例子一样)。消息中间件系统需要存储它们的数据在某个地方,通常是一个关系型数据库。为实现给模式,所需要的只是为消息系统指定业务数据将使用同一个数据库。这个模式依赖于消息中间件供应商暴露其存储策略的细节,这样可以把消息中间件配置为指向统一的数据库和挂靠到相同的事务。
不是所有的供应商都提供这么简单的支持。做为可选方案,可以使用Apache ActiveMQ做为消息中间件并在消息代理中插入存储策略,这对几乎所有的数据库都可以正常工作。一旦你了解其中的窍门,配置起来还是蛮简单的。这在本文的shared-jms-db示范工程中有过说明。应用程序代码(在这里是单元测试代码)不需要知道具体使用的模式,因为它是通过Spring配置声明来启用的。
这个例子中的单元测试代码,SynchronousMessageTriggerAndRollbackTests
验证是否所有的功能在使用同步消息接收机制时都能正常工作。testReceiveMessageUpdateDatabase方法接收两个消息并且使用它们在数据库中插入两条数据。
当这个方法退出时,测试框架会回滚这个事务,这样你可以确认消息和数据库更新都被成功回滚,如列表3所示:
列表3. 确认消息和数据库更新的回滚
@AfterTransaction public void checkPostConditions() { assertEquals(0, SimpleJdbcTestUtils.countRowsInTable(jdbcTemplate, "T_FOOS")); List<String> list = getMessages(); assertEquals(2, list.size()); }
最重要的配置是ActiveMQ的持久化策略(persistence strategy),把消息系统连接到相同的业务数据源,并且Spring JmsTemplate上的标志用来接收消息。列表4展示了如何配置ActiveMQ持久化策略:
列表4. ActiveMQ持久化配置
<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>
列表5展示了用来接收消息的Spring
JmsTemplate
上的标志:
列表5. 设置JmsTemplate
事务属性
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> ... <!-- This is important... --> <property name="sessionTransacted" value="true" /> </bean>
sessionTransacted=true
,JMS会话事务API将永远不会被调用而且消息接收不能回滚。这里的重要因素是,带有特殊async=false参数的内嵌代理以及数据源的一个封装层,两者一起确保ActiveMQ使用和Spring相同的事务JDBC连接。一个共享数据库资源某些时候可以由已有的独立资源合成而来。特别是如果它们在相同的RDBMS平台里。企业级的数据库供应商都支持同义词(或当量)概念,这里一个数据模式(schema,Oracle的术语)中的表在另外的模式中被声明为同义词。通过这样的方式,该平台里被物理分割的数据可以被同一个JDBC客户端连接所操作。比如,一个基于ActiveMQ的共享资源模式在实际系统中的实现(和上面的示范不同)通常会包括为消息和业务数据创建同义词。
性能和JDBCPersistenceAdapter
ActiveMQ社区的一些人宣称JDBCPersistenceAdapter存在性能问题
。然而,很多项目和正式系统使用ActiveMQ和关系数据库。从这些例子中可以获知应该使用一个该适配器的日志(journaled) 版本来提高性能。这对于共享资源模式并不合适(因为日志本身是一个新的事务资源)。然而,关注点仍在JDBCPersistenceAdapter上。
事实上有理由认为使用一个共享资源可能会比日志方式有更好的性能。这是一个在Spring和ActiveMQ工程师团队中活跃的研究领域。
另外一个不使用消息机制(多数据库)的共享资源技术是使用Oracle数据库链接特性来把两个数据库模式一起链接在RDBMS平台级别上。这可能会需要应用代码的修改,或者创建同义词,因为指向一个链接数据库的表别名包含链接的名字。
最大努力单阶段提交模式(Best Efforts 1PC pattern)
最大努力单阶段提交模式运用比较普遍,但开发人员必须意识到在某些情况下会出现事务失败。这是一个非XA模式,包含一个同步的单阶段的一些资源的提交。由于未使用2PC,它不可能和XA事务一样安全,但如果参与者认识到其中的设计折衷,那么这个模式通常情况下已经足够好。很多高容量,高并发处理事务系统是这样设计以提高性能。
基本的想法是尽可能的延迟一个事务中资源操作的提交,这样仅有的失败可能是基础设施出问题(而不是业务处理错误)。依赖于这个模式的系统基本上假定系统基础设施的失败很少会发生,这样他们能为了更高的并发处理量而承担这样的失败风险。如果业务处理服务也同样设计为幂等的,那么在实践中很少犯错。
为了帮你更好的理解这个模式和分析失败的后果,我将使用消息驱动数据库更新为例。
这个事务中的两个资源被反复计算。消息事务开始于第一个数据库的操作之前,而以相反的顺序结束(或者提交或者回滚)。所以这个案例中的成功序列和本文开始的时候所列举的例子相同:
- 开始消息事务
- 接收消息
- 开始数据库事务
- 更新数据库
- 提交数据库事务
- 提交消息事物
事实上,前4个步骤地顺序并不重要,除了消息必须在数据库更新之前被接收,而且每个事务必须开始于它所对应的资源被使用之前。所以下面的顺序一样有效:
- 开始消息事务
- 开始数据库事务
- 接收消息
- 更新数据库
- 提交数据库事务
- 提交消息事务
关键是最后两个步骤很重要:他们必须在最后执行。为什么顺序是重要的,主要是技术方面的原因,但顺序本身是由业务需求决定的。顺序告诉你某一个事务性资源在这种情况下是特殊的;它包含了如何进行其他资源的工作。这是一个业务排序:系统不能自动做出辨别(尽管如果两种资源分别是消息和数据库的时候,经常是这个顺序)。顺序很重要的原因在于处理失败回滚。最常见的故障情况下(目前为止)是业务处理的失败(坏数据,编程错误,等等)。在这种情况下,两种事务可以被容易的操控来响应异常和进行回滚。在这种情况下,业务数据的完整性被保留,操作序列类似于本文开头所描述的案例。
这个用于触发回滚的确切机制并不重要;有若干可选方案。重要的是提交或者回滚动作的发生和资源的业务操作顺序相反。在示范应用中,消息事务必须最后提交,因为业务处理的指令包含在那个资源中。之所以这很重要,是由于在这个失败(很少发生)中第一个提交成功第二个失败。由于从设计上而言,所有的业务处理在这个时间点已经结束,出现部分失败的唯一原因只能是消息中间件的基础设施出了问题。
注意如果数据库资源提交失败,那么效应只是一个回滚。所以仅有的非原子性失败模式是第一个事务提交后第二个进行回滚。更一般的,如果事务中有n个资源,那么有n-1个这样的失败模式,导致回滚某数据后部分资源处于一个不一致(已提交)的状态。在上面这个消息-数据库的案例中,
这种故障模式的结果是,该消息被回滚,然后回到另一个事务,即使它已被成功地处理了。因此你可以放心的假设可能发生的更糟糕的事情是,消息被重复传递。更一般的情况,由于事务前期的资源被认为可能潜在包含后续资源的处理信息,最终结果一般被称为消息重复(duplicate message)。
一些人觉得消息重复很少发生所以无需重视。但为了对业务数据的正确性和一致性更为自信,你需要在业务逻辑中意识到这一点。如果业务处理意识到消息会重复发送,要做的不过是(通常会有一些额外的成本,但远少于2PC)是检查数据是否已被处理,如果有则什么也不做。这在专业上有时被称为幂等业务服务(Idempotent Business Service)模式。
示例代码包括利用该模式同步事务性资源的两个例子。我将依次讨论它们,然后看看其他一些可选项。
Spring和消息驱动POJOs(Spring and message-driven POJOs)
在示例代码best-jms-db项目中,参与者都使用主流的配置选项,遵循最大努力单阶段提交(Best Efforts 1PC)模式。想法是发送到一个队列中的消息被异步侦听器所选择,并且用于将数据插入到数据库中的表。
TransactionAwareConnectionFactoryProxy
-- Spring中被用于这个模式的一个部件 -- 在这里是关键因素。这个配置把ConnectionFactory包装在一个装饰器中来处理事务同步,而不是使用原有的ConnectionFactory。具体细节在文件jms-context.xml中,如列表6所示:
列表6. 配置一个TransactionAwareConnectionFactoryProxy
来包装ActiveMQ提供的JMS ConnectionFactory
- <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>
ConnectionFactory不需要知道和哪个事务管理器同步,因为当需要时只有一个事务处于激活状态,这是在Spring内部处理的。驱动(driving)事务是由一个通用的DataSourceTransactionManager配置在data-source-context.xml中。这个组件需要了解事务管理器是JMS侦听容器,将轮询和接收消息:
- <jms:listener-container transaction-manager="transactionManager" >
- <jms:listener destination="async" ref="fooHandler" method="handle"/>
- </jms:listener-container>
fooHandler
和method告诉侦听容器在一个“异步”消息到来时该调用哪个组件的哪个方法。该处理器程序实现如下,接受一个字符串作为输入信息,并用它来插入一个记录:
- public void handle(String msg) {
- jdbcTemplate.update(
- "INSERT INTO T_FOOS (ID, name, foo_date) values (?, ?,?)", count.getAndIncrement(), msg, new Date());
- }
为模拟失败,这个代码使用了一个
FailureSimulator
切面(aspect)。
它检查消息内容是否会失败,以什么样的方式失败。列表7中的maybeFail()方法,在FooHandler处理完消息,但在事务结束前被调用,因此它可以影响事务的结果:
列表7. maybeFail()
方法
- @AfterReturning("execution(* *..*Handler+.handle(String)) && args(msg)")
- public void maybeFail(String msg) {
- if (msg.contains("fail")) {
- if (msg.contains("partial")) {
- simulateMessageSystemFailure();
- } else {
- simulateBusinessProcessingFailure();
- }
- }
- }
simulateBusinessProcessingFailure()方法在数据库访问失败时抛出一个DataAccessException。当这个方法被触发时,期望全部回滚所有数据库和消息事务。这种场景在示范项目的
AsynchronousMessageTriggerAndRollbackTests
单元测试中测试过。
该simulatemessagesystemfailure()法模拟了一个失败的消息传递系统的削弱潜在的JMS会话。预期的结果,这是一部分提交:数据库的工作保持承诺但消息回滚。这是在asynchronousmessagetriggerandpartialrollbacktests单元测试。
示例包还包括一个完全成功提交的事务的单元测试,在AsynchronousMessageTriggerSunnyDayTests
类中。
同一个JMS配置和相同的业务逻辑,也可用于同步设置,这里消息是在一个锁住的业务逻辑内部调用中被接收,而不是代理给一个侦听容器。这种方法在best-jms-db
示范项目中演示过。顺利提交和完整回滚的案例分别在SynchronousMessageTriggerSunnyDayTests和SynchronousMessageTriggerAndRollbackTests
中被测试。