如果需要近乎完美的防护,2PC绝对是不二之选,但是它的性能损耗是显而易见的,这也是为什么大家谈分布式事务而闻之色变的原因。此处我们不谈性能,只谈Spring是如何支持2PC XA的。Spring为我们提供了JtaTransactionManager和声明式事务,将复杂的同步细节进行了托管。我们只需要关注DataSource的配置,例如通过开源的JTA实现 Atomikos 来配置DataSource。
1PC可以看做是单一事务资源下2PC的的优化版本,许多的事务管理器通过这种模式来优化2PC的过度开销。但是并不是单一事务资源,就表示一定走的是1PC,因为XA事务管理器会根据通讯时间的长度,来判断网络通讯的危险期,即使连接一个数据库也应该使用两阶段提交,如果通讯危险期很短,多个事务资源也有可能使用一阶段提交。
目前市面上绝大多数的XA事务管理都有一个特点,那就是不论资源是部分XA兼容还是所有的都XA兼容,事务管理器都能够提供相同的事务保障。当资源经过排序,并且非XA资源通过投票机制,可以保证事务失败时所有的资源都能回滚。它对事务的保证是可以完全放心的,但是如果事务失败不会留下太多信息,如果想获取这些信息,就需要一些高级实现了。
前面介绍了三种基于XA协议的模式,不过从性能的方面来考虑,这些模式可能不能让我们满意,尤其是特别关注并发的互联网业务。我们可以了解并学习它,知道何时、如何避免使用XA,何时又必须使用。当然也有开发能力很强的团队,自己研发了两阶段提交事务框架,例如支付宝。
这个模式就是将所有的事务资源绑定到一个相同的资源,从而实现分布式事务,提升系统吞吐量。该模式的使用场景是受限制的,不能拿来处理所有的用例,但是它如XA一样坚固可靠。为了详细说明该模式,我通过两个案例来阐述。
我们在使用spring的声明式事务的时候,通常都是由service层来控制的,这点是基础共识。这样不论我们是使用ORM还是JDBC抽象框架,只要底层的Connection是共享的,那么我们即使配置了多个上层的模板(Mybatis or JdbcTemplate),事务的安全就能够得到保证。这便是共享资源模式最简单最直接的体现,只是我们平时没有关注而已。
这种模式就是将消息中间件的Connection和数据库的Connection交给一个代理,这个代理依赖于消息中间厂商的存储策略细节,并且不是所有的供应商都提供这种模式。这种模式的使用并不常见,因为我们更喜欢用 Best Efforts 1PC pattern
中消息驱动模式,因为它更简单。下面的配置可以让我们窥一斑而见全豹,如果想更清晰的了解消息驱动的单Database的更新,请下载原文的代码研究。
<bean id="brokerService" class="org.apache.activemq.broker.BrokerService"
init-method="start" destroy-method="stop">
<property name="brokerName" value="broker" />
<!-- Enable this if you need to inspect with JMX -->
<property name="useJmx" value="false" />
<property name="transportConnectorURIs">
<list>
<value>vm://localhost</value>
</list>
</property>
<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>
该模式应用相当广泛,这也是我最喜欢的模式之一。虽然在安全性上不如XA,但是在大数据、高并发等性能上却得到了极大的提升。那我们如何判断我们的系统是否可以使用该模式,建议参考下面的两点:
只要满足以上两点,事务发生错误的可能性就非常小了。为了简单的说明此模式,我结合Rabbitmq和Mysql来说明,先简单的来一段配置。
<bean id="jsonMessageConverter"
class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter"/>
<rabbit:connection-factory id="rabbitConnectionFactory"
addresses="10.11.25.222"
username="yangguo"
password="yangguo"/>
<rabbit:template id="rabbitTemplate" connection-factory="rabbitConnectionFactory"
message-converter="jsonMessageConverter" channel-transacted="true"/>
再来两段Java代码
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void provierSuccess() {
User user = new User();
user.setUserName("yangguo");
user.setPassword("yangguo");
userMapper.insert(user);
rabbitTemplate.convertAndSend("amq.topic", null, user);
}
上面代码处理的业务逻辑是将用户插入到用户表的同时向消息队列发一条消息,插入DB和发送消息到MQ是包在一个事务中。下面的代码则是消费消息,也是包在一个事务中。
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void consumeSuccess() {
User user= (User) rabbitTemplate.receiveAndConvert("test-queue");
friendMapper.update(user);
}
代码其实很简单,首先配置一个支持事务的RabbitTemplate,主要设置 channel-transacted="true"
,当然Mysql DataSource和Mybatis的配置,我就省略了,然后剩下的事情就交给Spring的Transaction吧。不过原文是使用ActiveMQ来讲解的,不过思想都是一样的。当然作者在原文中还解释了一种链式事务管理器,其实上面的例子也是一种链式思路,链式事务管理器非常注重顺序性,因为回滚和业务顺序正好相反。现阶段的spring事务处理,默认就是一种链式思路,只要你在service上面加了@Transaction或者在XML里面配置了切面,那么处于切面中的方法中的分布式资源就会受到spring事务管理器的托管。这也是最大努力一次提交模式流行的真正原因,因为它就是Spring事务管理器的核心。当然如果我们使用消息队列的Topic模式,是可以绕过 消息驱动的单Database更新
的,因为我们只要能够实现最终一致性就OK了。
这个模式是需要特殊的业务场景,理想情况是非事务或者本地事务的逻辑是边缘业务,该业务具备幂等性或者从逻辑的角度来说具备幂等性。当然该模式可以是分布式事务和非事务的组合,也可以全部都是非事务逻辑,还可以是分布式事务和本地事务的组合。这种模式需要细致的分析,只有对业务充分了解,才能确定哪些可以走非事务或者本地事务。当然业务出错,你可以使用补偿机制,不过请注意通用的事务补偿通常都很麻烦。