本文做者:Eraygit
一个TCC事务框架须要解决的固然是分布式事务的管理。关于TCC事务机制的介绍,能够参考TCC事务机制简介。github
TCC事务模型虽说起来简单,然而要基于TCC实现一个通用的分布式事务框架,却比它看上去要复杂的多,不仅是简单的调用一下Confirm/Cancel业务就能够了的。数据库
本文将以Spring容器为例,试图分析一下,实现一个通用的TCC分布式事务框架须要注意的一些问题。服务器
TCC服务是由Try/Confirm/Cancel业务构成的,其Try/Confirm/Cancel业务在执行时,会访问资源管理器(Resource Manager,下文简称RM)来存取数据。网络
这些存取操做,必需要参与RM本地事务,以使其更改的数据要么都commit,要么都rollback。架构
这一点不难理解,考虑一下以下场景:框架
假设图中的服务B没有基于RM本地事务(以RDBS为例,可经过设置auto-commit为true来模拟),那么一旦[B:Try]操做中途执行失败,TCC事务框架后续决定回滚全局事务时,该[B:Cancel]则须要判断[B:Try]中哪些操做已经写到DB、哪些操做尚未写到DB.分布式
假设[B:Try]业务有5个写库操做,[B:Cancel]业务则须要逐个判断这5个操做是否生效,并将生效的操做执行反向操做。学习
不幸的是,因为[B:Cancel]业务也有n(0<=n<=5)个反向的写库操做,此时一旦[B:Cancel]也中途出错,则后续的[B:Cancel]执行任务更加繁重。设计
由于相比第一次[B:Cancel]操做,后续的[B:Cancel]操做还须要判断先前的[B:Cancel]操做的n(0<=n<=5)个写库中哪几个已经执行、哪几个尚未执行.
这就涉及到了幂等性问题,而对幂等性的保障,又极可能还须要涉及额外的写库操做,该写库操做又会由于没有RM本地事务的支持而存在相似问题。。。
可想而知,若是不基于RM本地事务,TCC事务框架是没法有效的管理TCC全局事务的。
反之,基于RM本地事务的TCC事务,这种状况则会很容易处理。
[B:Try]操做中途执行失败,TCC事务框架将其参与RM本地事务直接rollback便可。后续TCC事务框架决定回滚全局事务时,在知道“[B:Try]操做涉及的RM本地事务已经rollback”的状况下,根本无需执行[B:Cancel]操做。
换句话说,基于RM本地事务实现TCC事务框架时,一个TCC型服务的cancel业务要么执行,要么不执行,不须要考虑部分执行的状况。
基于RM本地事务的TCC事务框架,能够将各Try/Confirm/Cancel业务当作一个原子服务:一个RM本地事务提交,参与该RM本地事务的全部Try/Confirm/Cancel业务操做都生效;反之,则都不生效。
掌握每一个RM本地事务的状态以及它们与Try/Confirm/Cancel业务方法之间的对应关系,以此为基础,TCC事务框架才能有效的构建TCC全局事务。
TCC服务的Try/Confirm/Cancel业务方法在RM上的数据存取操做,其RM本地事务是由Spring容器的PlatformTransactionManager来commit/rollback的,TCC事务框架想要了解RM本地事务的状态,只能经过接管Spring的事务管理器功能。
2.1. 为何TCC事务框架须要掌握RM本地事务的状态?
首先,根据TCC机制的定义,TCC事务是经过执行Cancel业务来达到回滚效果的。仔细分析一下,这里暗含一个事实:只有生效的Try业务操做才须要执行对应的Cancel业务操做。
换句话说,只有Try业务操做所参与的RM本地事务被commit了,后续TCC全局事务回滚时才须要执行其对应的Cancel业务操做
不然,若是Try业务操做所参与的RM本地事务被rollback了,后续TCC全局事务回滚时就不能执行其Cancel业务,此时若盲目执行Cancel业务反而会致使数据不一致。
其次,Confirm/Cancel业务操做必须保证生效。Confirm/Cancel业务操做也会涉及RM数据存取操做,其参与的RM本地事务也必须被commit。
TCC事务框架须要在确切的知道全部Confirm/Cancel业务操做参与的RM本地事务都被成功commit后,才能将标记该TCC全局事务为完成。
若是TCC事务框架误判了Confirm/Cancel业务参与RM本地事务的状态,就会形成全局事务不一致。
最后,未完成的TCC全局,TCC事务框架必须从新尝试提交/回滚操做。重试时会再次调用各TCC服务的Confirm/Cancel业务操做。
若是某个服务的Confirm/Cancel业务以前已经生效(其参与的RM本地事务已经提交),重试时就不该该再次被调用。不然,其Confirm/Cancel业务被屡次调用,就会有“服务幂等性”的问题。
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事务框架,如果没有故障恢复的保障,是不成其为分布式事务框架的。
分布式事务管理框架的职责,不是作出全局事务提交/回滚的指令,而是管理全局事务提交/回滚的过程。
它须要可以协调多个RM资源、多个节点的分支事务,保证它们按全局事务的完成方向各自完成本身的分支事务。
这一点,是不容易作到的。由于,实际应用中,会有各类故障出现,不少都会形成事务的中断,从而使得统一提交/回滚全局事务的目标不能达到,甚至出现”一部分分支事务已经提交,而另外一部分分支事务则已回滚”的状况。
比较常见的故障,好比:业务系统服务器宕机、重启;数据库服务器宕机、重启;网络故障;断电等。这些故障可能单独发生,也可能会同时发生。
做为分布式事务框架,应该具有相应的故障恢复机制,无视这些故障的影响是不负责任的作法。
一个完整的分布式事务框架,应该保障即便在最严苛的条件下也能保证全局事务的一致性,而不是只能在最理想的环境下才能提供这种保障。退一步说,若是能有所谓“理想的环境”,那也无需使用分布式事务了。
TCC事务框架要支持故障恢复,就必须记录相应的事务日志。事务日志是故障恢复的基础和前提,它记录了事务的各项数据。
TCC事务框架作故障恢复时,能够根据事务日志的数据将中断的事务恢复至正确的状态,并在此基础上继续执行先前未完成的提交/回滚操做。
通常认为,服务的幂等性,是指针对同一个服务的屡次(n>1)请求和对它的单次(n=1)请求,两者具备相同的反作用。
在TCC事务模型中,Confirm/Cancel业务可能会被重复调用,其缘由不少。
好比,全局事务在提交/回滚时会调用各TCC服务的Confirm/Cancel业务逻辑。执行这些Confirm/Cancel业务时,可能会出现如网络中断的故障而使得全局事务不能完成。
所以,故障恢复机制后续仍然会从新提交/回滚这些未完成的全局事务,这样就会再次调用参与该全局事务的各TCC服务的Confirm/Cancel业务逻辑。
既然Confirm/Cancel业务可能会被屡次调用,就须要保障其幂等性。
那么,应该由TCC事务框架来提供幂等性保障?仍是应该由业务系统自行来保障幂等性呢?
我的认为,应该是由TCC事务框架来提供幂等性保障。若是仅仅只是极个别服务存在这个问题的话,那么由业务系统来负责也是能够的;
然而,这是一类公共问题,毫无疑问,全部TCC服务的Confirm/Cancel业务存在幂等性问题。TCC服务的公共问题应该由TCC事务框架来解决;
并且,考虑一下由业务系统来负责幂等性须要考虑的问题,就会发现,这无疑增大了业务系统的复杂度。
前文以及提到过,TCC事务经过Cancel业务来对Try业务进行回撤的机制暗含了一个事实:Try操做已经生效。
也就是说,只有Try操做所参与的RM本地事务已经提交的状况下,才须要执行其Cancel操做进行回撤。没有执行、或者执行了可是其RM本地事务被rollback的Try业务,是必定不能执行其Cancel业务进行回撤的。
所以,TCC事务框架在全局事务回滚时,应该根据TCC服务的Try业务的执行状况选择合适的处理机制。而不能盲目的执行Cancel业务,不然就会致使数据不一致。
一个TCC服务的Try操做是否生效,这是TCC事务框架应该知道的,由于其Try业务所参与的RM事务也是由TCC事务框架所commit/rollbac的(前提是TCC事务框架接管了Spring的事务管理器)。
因此,TCC事务回滚时,TCC事务框架可考虑以下处理策略:
总之,TCC事务框架应该保障:
这应该算TCC事务机制特有的一个难以想象的陷阱。
通常来讲,一个特定的TCC服务,其Try操做的执行,是应该在其Confirm/Cancel操做以前的。
Try操做执行完毕以后,Spring容器再根据Try操做的执行状况,指示TCC事务框架提交/回滚全局事务。而后,TCC事务框架再去逐个调用各TCC服务的Confirm/Cancel操做。
然而,超时、网络故障、服务器的重启等故障的存在,使得这个顺序会被打乱。好比:
上图中,假设[B:Try]操做执行过程当中,网络闪断,[A:Try]会收到一个RPC远程调用异常。
A不处理该异常,致使全局事务决定回滚,TCC事务框架就会去调用[B:Cancel],而此刻A、B之间网络恰好已经恢复。若是[B:Try]操做耗时较长(网络阻塞/数据库操做阻塞),就会出现[B:Try]和[B:Cancel]两者并行处理的现象,甚至[B:Cancel]先完成的现象。
这种状况下,因为[B:Cancel]执行时,[B:Try]还没有生效(其RM本地事务还没有提交),所以,[B:Cancel]是不能执行的,至少是不能生效(执行了其RM本地事务也要rollback)的。
然而,当[B:Cancel]处理完毕(跳过执行、或者执行后rollback其RM本地事务)后,[B:Try]操做完成又生效了(其RM本地事务成功提交),这就会使得[B:Cancel]虽然提供了,但却没有起到回撤[B:Try]的做用,致使数据的不一致。
因此,TCC框架在这种状况下,须要:
固然,TCC事务框架也能够简单的选择阻塞[B:Cancel]的处理,待[B:Try]执行完毕后,再根据它的执行状况判断是否须要执行[B:Cancel]。不过,这种处理方式由于须要等待,因此,处理效率上会有所不及。
一样的状况也会出如今confirm业务上,只不过,发生在Confirm业务上的处理逻辑与发生在Cancel业务上的处理逻辑会不同。
TCC框架必须保证:
TCC事务机制的定义,决定了一个服务须要提供三个业务实现:Try业务、Confirm业务、Cancel业务。
可能会有人所以认为TCC服务的复用性较差。怎么说呢,要是将 Try/Confirm/Cancel业务逻辑单独拿出来复用,其复用性固然是很差的。
Try/Confirm/Cancel 逻辑做为TCC型服务中的一部分,是不能单独做为一个组件来复用的。Try、Confirm、Cancel业务共同才构成一个组件,若是要复用,应该是复用整个TCC服务组件,而不是单独的Try/Confirm/Cancel业务。
不须要。TCC服务与普通的服务同样,只须要暴露一个接口,也就是它的Try业务。
Confirm/Cancel业务逻辑,只是由于全局事务提交/回滚的须要才提供的,所以Confirm/Cancel业务只须要被TCC事务框架发现便可,不须要被调用它的其余业务服务所感知。
换句话说,业务系统的其余服务在须要调用TCC服务时,根本不须要知道它是否为TCC型服务。
由于,TCC服务能被其余业务服务调用的也仅仅是其Try业务,Confirm/Cancel业务是不能被其余业务服务直接调用的。
最好是不要这样作。
首先,没有必要。TCC服务A依赖TCC服务B,那么[A:Try]已经将事务上下文传播给[B:Try]了,后续由TCC事务框架来调用各自的Confirm/Cancel业务便可;
其次,Confirm/Cancel业务若是被容许调用其余服务,那么它就有可能再次发起新的TCC全局事务。如此递归下去,将会致使全局事务关系混乱且不可控。
TCC全局事务,应该尽可能在Try操做阶段传播事务上下文。Confirm/Cancel操做阶段仅须要完成各自Try业务操做的确认操做/补偿操做便可,不适合再作远程调用,更不能再对外传播事务上下文。
综上所述,本文倾向于认为,实现一个通用的TCC分布式事务管理框架,仍是相对比较复杂的。通常业务系统若是须要使用TCC事务机制,并不推荐自行设计实现。
这里,给你们推荐一款开源的TCC分布式事务管理器ByteTCC。
https://github.com/liuyangming/ByteTCC
ByteTCC基于Try/Confirm/Cancel机制实现,可与Spring容器无缝集成,兼容Spring的声明式事务管理。提供对dubbo框架、Spring Cloud的开箱即用的支持,可知足多数据源、跨应用、跨服务器等各类分布式事务场景的需求。
END
欢迎长按下图关注公众号:石杉的架构笔记!
公众号后台回复资料,获取做者独家秘制学习资料
石杉的架构笔记,BAT架构经验倾囊相授