简化的分布式事务框架设计

Posted by KC on January 8, 2011

一、关于XA及其实现

这篇文章源于最近对对分布式事务的分析总结和设计、应用经验的分享。关于分布式事务,为什么要用分布式事务就不多说了,无非就是为了ACID。目前的分布式事务规范主要源于X/Open的XA模型,JOTM是它的一个比较实在的实现并且兼容于JTA。但我们在实际应用中,并没有选择使用JOTM也没有去实现XA的规范。也许某些时候一个更轻量级的设计更符合我们的实际要求,而且使用数据库层的预提交也可能会产生一些不必要的效率上的损失,另外该规范对实际选用的数据库及其版本本身也要求多多(例如MySQL要支持XA的话版本至少要为5.0.3并且使用InnoDB存储引擎)。在这种情况下我们或许可以选择自己实现一套,作为X/A的简化版本,但是在数据库选择和效率方面可以更进一步的版本,总体思想和XA规范还是一致的:两阶段提交。在这种情况下我们也不再兼容JTA相关接口,不依赖于数据库和驱动程序是否支持分布式事务,而是采用“模拟”的方式实现了两阶段提交。另外我们除了两阶段提交的方式之外也可能提供另外一种方式:可补偿事务机制。这种机制应该是建立在保证幂等性的基础上,资源管理器需要提供业务补偿方法,在事务执行失败的时候,事务管理器以异步的方式调用业务补偿方法尝试对业务活动进行补偿。

在标准的XA规范里,两阶段提交的过程中,第一阶段各资源管理器执行预提交,并给调用者返回结果,如果每个资源管理器的预提交都是正常的,第二阶段由事务管理器发起提交操作;如果有各资源管理器预提交过程中有一个发生失败,那么资源管理器会使整个事务的所有参与者都执行回滚。全程所操作的分支事务操作都会持久化到日志中,以便在提交失败或其他异常状况出现后重新发起审查和恢复操作,确保事务一致性。在XA中的Database和驱动程序自身能够支持两阶段提交的特性。


二、实现我们的分布式事务系统

DTS实现之于XA,两阶段的思想是一样的。主要的区别除了实际接口和实现不同之外,DTS对两阶段的实现只是对数据库预提交和提交/回滚过程的一个模拟,所以,DTS也不需要数据库及其JDBC驱动程序必须内部支持分布式事务。所有的操作其实都是通过一个应用层来进行模拟的。

也就是说,在DTS中所谓第一阶段的预提交其实都已经是数据库层面的真正的提交,即使数据库及其JDBC驱动支持分布式事务,我们也不使用它;而第二阶段的提交操作根据各参与者而定,参与者应用系统可以去更改一个标志位以便表示第一阶段的“预提交”已经完成了,实际上甚至可以什么都不做。也就是说只要有数据库操作,都是实质上的提交。因此回滚操作也只是一个反向的数据库DML操作,把第一阶段中insert的数据delete掉,delete的数据insert回去,或者把第一阶段update的数据重新update回去。在DTS中并不存在XA规范中的那种本质上的数据库“预提交”操作,这种操作都是我们的应用在“模拟”的。

系统结构

DTS从系统结构上看,横向可以把它分成三个层:控制层、模型层、存储层。另外,还有一个纵向贯穿的回查恢复程序,如图:

存储层

存储层主要是DAO和数据对象,所负责的是将整个事务过程中的主事务和分支事务执行情况持久化的工作,以便在事务第二阶段提交/回滚失败或其他异常时进行回复操作。以非主库的形式为例,DTS需要存储的内容存放在数据库中的两张表:BUSINESS_ACTIVITY和BUSINESS_ACTION中,前者存储分布式事务主事务记录,后者存储分布式事务分支事务记录。

模型层

两个主要的领域模型:主事务(Activity)和分支事务(Action)。

主事务:描述整个业务活动,例如一次交易可以为一个主事务,一次积分互换也可以为一个主事务。

分支事务:主事务执行过程中可以进行分解,一个主事务的完成可能需要多个分支事务都正常完成,例如每个资源管理器对应着每个独立的分支事务。例如在交易过程中资金支付是一个分支事务,红包支付是一个分支事务,积分支付也是一个分支事务。

控制层

业务系统需要调用此层中的控制器(BusinessActivityControlService)进行发起分布式事务,这是整个分布式事务启动的标志。在此控制器内部,DTS组装全局唯一的分布式事务ID(TX_ID),然后调用业务活动管理器(BusinessActivityManager)进行事务的初始化动作:往主事务表中插入一条记录,然后结束。事务发起方继续执行业务逻辑,当下一个参与者被调用的时候,拦截器(BusinessActionInterceptor)会将此调用拦截,通过方法注解(Annotation)获得该参与者分支事务的必要属性(名称、类型、提交方法、回滚方法等)并通过控制器和管理器执行注册、存储,将此分支事务加入到主事务中。z

回查程序

在事务调用执行正常的情况下,事务可以正常执行,如果能保证这些调用永远不会出现异常情况,则分布式事务的机制是没有实际作用的。当我们考虑异常情况的时候,例如可能会有某些分支事务提交或者回滚失败,这时候我们需要发起回查。回查的依据就来源于事务发起者和参与者应用系统被调用过程中写入主库或者misc库中的事务记录。如上面的系统结构图所示,回查的接口以Jar包依赖的方式已经定义好,分布式事务发起者需要实现此接口。

而回查也需要有一套独立于事务管理器和各资源管理器的一套独立系统,该系统会以定时任务的形式不断查询BUSINESS_ACTIVITY表,如果其中有状态为Unknown的记录,会尝试向事务发起者进行回查,并根据回查结果和BUSINESS_ACTION表中的记录调用对应的参与者的提交或者回滚接口,从而保证在分布式事务的。

系统调用关系

事务发起者,事务参与者和DTS之间的调用序列图如下:

事务一般是由业务系统发起的,例如前台会发起交易的主事务,积分产品发起积分兑换的主事务,然后调用各个参与者,例如交易核心,积分核心,红包核心,资金账务核心等等

业务系统产生一个事物必须赋予该事务一个全局(所有参与到分布式事务场景中的系统之间)唯一的ID号作为标识。该事务号会在以及提交/回滚的时候被使用,以便定位到此事务应该向谁进行回查或者让谁进行提交/回滚。

系统部署关系

事务的参与者数量可能是不一致的,此图以两个参与者为例,实际系统可以参照通用积分(一个积分核心系统,一个积分账务系统)。事务发起方一般为业务系统,DTS DAEMON为回查系统,当事务执行出现异常情况时向事务发起者发起回查并调用各事务参与者进行提交或者回滚事务。事务的参与者数量可能是不一致的,此图以两个参与者为例,实际系统可以参照通用积分(一个积分核心系统,一个积分账务系统)。事务发起方一般为业务系统,DTS DAEMON为回查系统,当事务执行出现异常情况时向事务发起者发起回查并调用各事务参与者进行提交或者回滚事务。


三、最佳实践

在系统部署关系示意图中我们看到,在和DTS回查系统的调用中,分布式事务发起者需要提供一个回查接口,分布式事务参与者需要提供一个提交接口和一个回滚接口。如何写好这几个和DTS系统关联的接口呢?

分布式事务发起者回查接口实现

发起者需提供回查接口。考虑到系统可能要向DTS提供多个不同服务的回查接口,但同事我们也希望系统使用统一的对外接口,即对于DTS来说,无论DTS想回查发起者的哪个服务,都只需要调用发起者的同一个服务即可。

在这里,例如发起者提供统一的businessActivityStateResolver服务给外部统一调用。并以OSGI扩展点的方式提供将多个BusinessActivityStateResolver对象以Key-Value(Key值是BusinessType)的存在服务实例Bean的属性中,这样DTS以不同的DTS_ID过来积分产品回查的时候,积分产品就能够以统一的接口应对,根据DTS_ID的前半段(就是BusinessType)找到对应的Resolver并有该Resolver进行实际的回查工作。关系如下图:

分布式事务参与者提交/回滚接口实现

参与者应用系统需要提供的接口除了自己的业务服务以外,还需要提供一份给DTS进行调用的提交/回滚接口。

下图为参与者实现提交回滚接口的模式。