如何设计实现一个通用的分布式事务框架?( 二 )


2.2. 拦截TCC服务的Try/Confirm/Cancel业务方法的执行,根据其异常信息可否知道其RM本地事务是否commit/rollback了呢?
基本上很难做到,为什么这么说?
第一,事务是可以在多个(本地/远程)服务之间互相传播其事务上下文的,一个业务方法(Try/Confirm/Cancel)执行完毕并不一定会触发当前事务的commit/rollback操作 。
比如,被传播事务上下文的业务方法,在它开始执行时,容器并不会为其创建新的事务,而是它的调用方参与的事务,使得二者操作在同一个事务中;同样,在它执行完毕时,容器也不会提交/回滚它参与的事务的 。
因此,这类业务方法上的异常情况并不能反映他们是否生效 。不接管Spring的TransactionManager,就无法了解事务于何时被创建,也无法了解它于何时被提交/回滚 。
第二、一个业务方法可能会包含多个RM本地事务的情况 。
比如:A(REQUIRED)->B(REQUIRES_NEW)->C(REQUIRED),这种情况下,A服务所参与的RM本地事务被提交时,B服务和C服务参与的RM本地事务则可能会被回滚 。
第三、并不是抛出了异常的业务方法,其参与的事务就回滚了 。
Spring容器的声明式事务定义了两类异常,其事务完成方向都不一样:系统异常(一般为Unchecked异常,默认事务完成方向是rollback)、应用异常(一般为Checked异常,默认事务完成方向是commit) 。
二者的事务完成方向又可以通过@Transactional配置显式的指定,如rollbackFor/noRollbackFor等 。
第四、Spring容器还支持使用setRollbackOnly的方式显式的控制事务完成方向;
最后,自行拦截业务方法的拦截器和Spring的事务处理的拦截器还会存在执行先后、拦截范围不同等问题 。
例如,如果自行拦截器执行在前,就会出现业务方法虽然已经执行完毕但此时其参与的RM本地事务还没有commit/rollback 。
TCC事务框架的定位应该是一个TransactionManager,其职责是负责commit/rollback事务 。
而一个事务应该commit、还是rollback,则应该是由Spring容器来决定的:
Spring决定提交事务时,会调用TransactionManager来完成commit操作;Spring决定回滚事务时,会调用TransactionManager来完成rollback操作 。
接管Spring容器的TransactionManager,TCC事务框架可以明确的得到Spring的事务性指令,并管理Spring容器中各服务的RM本地事务 。
否则,如果通过自行拦截的机制,则使得业务系统存在TCC事务处理、RM本地事务处理两套事务处理逻辑,二者互不通信,各行其是 。
这种情况下要协调TCC全局事务,基本上可以说是缘木求鱼,本地事务尚且无法管理,更何谈管理分布式事务?
三、TCC事务框架应该具备故障恢复机制
一个TCC事务框架,若是没有故障恢复的保障,是不成其为分布式事务框架的 。
分布式事务管理框架的职责,不是做出全局事务提交/回滚的指令,而是管理全局事务提交/回滚的过程 。
它需要能够协调多个RM资源、多个节点的分支事务,保证它们按全局事务的完成方向各自完成自己的分支事务 。
这一点,是不容易做到的 。因为,实际应用中,会有各种故障出现,很多都会造成事务的中断,从而使得统一提交/回滚全局事务的目标不能达到,甚至出现”一部分分支事务已经提交,而另一部分分支事务则已回滚”的情况 。
比较常见的故障,比如:业务系统服务器宕机、重启;数据库服务器宕机、重启;网络故障;断电等 。这些故障可能单独发生,也可能会同时发生 。
作为分布式事务框架,应该具备相应的故障恢复机制,无视这些故障的影响是不负责任的做法 。
一个完整的分布式事务框架,应该保障即使在最严苛的条件下也能保证全局事务的一致性,而不是只能在最理想的环境下才能提供这种保障 。退一步说,如果能有所谓“理想的环境”,那也无需使用分布式事务了 。
TCC事务框架要支持故障恢复,就必须记录相应的事务日志 。事务日志是故障恢复的基础和前提,它记录了事务的各项数据 。
TCC事务框架做故障恢复时,可以根据事务日志的数据将中断的事务恢复至正确的状态,并在此基础上继续执行先前未完成的提交/回滚操作 。
四、TCC事务框架应该提供Confirm/Cancel服务的幂等性保障
一般认为,服务的幂等性,是指针对同一个服务的多次(n>1)请求和对它的单次(n=1)请求,二者具有相同的副作用 。
在TCC事务模型中,Confirm/Cancel业务可能会被重复调用,其原因很多 。


推荐阅读