分布式事务

boneix 2017-09-15

在soa的架构中,假设有注册用户服务A,A做的事很简单,就是insert一条记录到user表,并且调用赠送用户积分服务B,B做的事也很简单,insert一条记录到coin表。服务A和B是分别部署在两台不同的机器,有可能发生的一种情况就是A在调用B的时候,B插入coin表一条记录并返回成功标志通过网络传输给A,假设在网络传输中,网络堵塞,这时A调用B的结果就是超时失败,A将会回滚user表,但此时B服务的coin表是成功插入记录的,这样就出现数据不一致了。也正因为网络是不可靠的,分布式要面临很多问题。

1 JTA(XA)事务【二阶段提交】

对于一个应用一个数据源这种情况,使用传统的jdbc事务,conn.commit()提交,conn.rollback()回滚,在conn的生命周期内事务有效,对应的是java.sql.*。

对于一个应用多个数据源这种情况,jdbc事务就不行了,只能使用JTA事务了,对应的的是javax.transaction.*。实际上jta的底层是xa协议,必须使用支持xa的数据库连接驱动,对应的是javax.sql.*。 

XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase等各大数据库厂家都提供对XA的支持。XA协议采用两阶段提交方式来管理分布式事务。XA接口提供资源管理器与事务管理器之间进行通信的标准接口。XA协议包括两套函数,以xa_开头的及以ax_开头的。

用JTA界定事务,那么就需要有一个实现javax.sql.XADataSource,javax.sql.XAConnection和javax.sql.XAResource接口的JDBC驱动程序。一个实现了这些接口的驱动程序将可以参与JTA事务。一个XADataSource对象就是一个XAConnection对象的工厂。XAConnection是参与JTA事务的JDBC连接。要使用JTA事务,必须使用XADataSource来产生数据库连接,产生的连接为一个XA连接。XA连接(javax.sql.XAConnection)和非XA(java.sql.Connection)连接的区别在于:XA可以参与JTA的事务,而且不支持自动提交。mysql对应的XADataSource是com.mysql.jdbc.jdbc2.optional.MysqlXADataSource。

一般情况下java ee服务器比如jboss才会支持jta,那如果使用tomcat如何使用JTA呢,有以下两种方式:

(1) 独立的第三方开源JTA实现比如JOTM,Atomikos(可以和spring结合使用)。

(2) 引用应用服务器(如jboss。Tomcat是Servlet容器,但它也提供了JNDI的实现)的JNDI数据源,间接实现JTA事务管理(可以结合srping或hibernate)。

XA协议这种二阶段提交有很强的一致性,肯定会各种资源锁定和互相等待,系统开销比较大,在系统开发过程中应慎重考虑是否确实需要。对于多个独立应用互相调用的情况,也就是开头举的例子,XA就没办法了,下面讲讲对于这种情况的解决方案。

2 TCC(事务补偿机制)

TCC是分布式事务实现的一种方式,分别对应Try、Confirm和Cancel三种操作。TCC其实也算两阶段提交,只是工作在应用层而不是资源层。

Try: 尝试执行业务,

-完成所有业务检查(一致性)。

-预留必须业务资源(准隔离性)。

Confirm: 确认执行业务,

-真正执行业务。

-不作任何业务检查。

-只使用Try阶段预留的业务资源。

-Confirm操作满足幂等性。

Cancel: 取消执行业务,

-释放Try阶段预留的业务资源。

-Cancel操作满足幂等性。

现在已经有一些TCC的开源实现了:

tcc-transactionhttps://github.com/changmingxie/tcc-transaction

ByteTCChttps://github.com/liuyangming/ByteTCC

TCC跟业务逻辑结合的比较紧密,开发成本比较高。如果业务需要的不是实时一致性的事务,可以考虑最终一致性的解决方案。

3 基于消息的最终一致性事务    

以开头举的例子来说,注册用户服务A除了insert一条记录到user表,同时insert一条积分消息记录到message表,然后启动一个独立进程扫描message表,调用服务B,B返回成功就删除当前message记录,失败了就重试n次,重试多次后仍然失败就只能人工干预处理了。进一步的方案是去掉message表,改成往消息中间件(比如ActiveMQRabbitMQ)发消息,这种方案真正实现了两个服务的真正解耦,解耦的关键就是异步消息和消息持久化机制。

A调用服务B的时候,有可能因为网络或者其它原因超时失败(实际上有可能B还在执行中或者已经执行成功),A就会发起第二次调用,这就可能导致最后coin表里有几条重复的积分记录。所以接口B需要满足幂等性(可以A生成coin表的id主键然后传给B,由数据库来保证),幂等性是指业务方法重复调用多次产生的业务结果与调用一次产生的业务结果相同,简单点讲所有提供的业务服务,不管是正向还是逆向的业务服务,都必须要支持重试。因为服务调用失败这种异常必须考虑到,不能因为服务的多次调用而导致业务数据的累计增加或减少。

幂等性的实现方式可以是:

(1) 通过唯一键值做处理,即每次调用的时候传入唯一键值,通过唯一键值判断业务是否被操作,如果已被操作,则不再重复操作。

(2) 通过状态机处理,给业务数据设置状态,通过业务状态判断是否需要重复执行。

推荐一些文章:

详解Mysql分布式事务XA(跨数据库事务):http://blog.csdn.net/soonfly/article/details/70677138

数据库三种连接PooledConnection,XAConnection,Connection(连接三剑客):http://blog.csdn.net/turkeyzhou/article/details/3071683

如何用消息系统避免分布式事务:http://blog.jobbole.com/89140/

说说分布式事务(三):https://segmentfault.com/a/1190000005969526

Java事务与JTA:http://blog.csdn.net/codepest/article/details/8437661

JTA 深度历险 - 原理与实现:https://www.ibm.com/developerworks/cn/java/j-lo-jta/index.html

TCP三次握手、四次握手内容整理:https://blog.csdn.net/qq_18425655/article/details/52163228

补充:其实最好的分布式事务解决方案就是不要分布式事务,如果出现了分布式事务,说明服务的业务领域划分是不合理的。服务化架构是一套松耦合的架构,服务的拆分原则是服务内部高内聚,服务之间低耦合。

感谢阅读!!!

相关推荐