一个TCC事务框架要解决的当然是分布式事务的管理。TCC交易机制介绍请参考TCC交易机制介绍。虽然TCC事务模型说起来简单,但是基于TCC实现一个通用的分布式事务框架远比看起来要复杂得多,不仅仅是简单的调用Confirm/Cancel服务。本文将以Spring容器为例,尝试分析一下在实现一个通用的TCC分布式事务框架时需要注意的一些问题。1、TCC全局交易必须基于RM本地交易来实现。TCC服务由Try/Confirm/Cancel服务组成。Try/Confirm/Cancel服务在执行时,会访问资源管理器(ResourceManager,以下简称RM)访问数据。这些访问操作必须参与RM本地事务,以便提交或回滚更改的数据。这不难理解。考虑如下场景:假设图中服务B不基于RM本地事务(以RDBS为例,可以通过设置auto-commit为true来模拟),那么一旦执行了[B:Try]操作midwayFailed,当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本地事务。当后续TCC事务框架决定回滚全局事务时,根本不需要执行[B:Cancel]操作,知道“[B:Try]操作涉及的RM本地事务已经回滚”。也就是说,在基于RM本地事务实现TCC事务框架时,一个TCC服务的cancel业务要么执行要么不执行,不需要考虑部分执行的情况。2.TCC事务框架应该接管Spring容器的TransactionManager。基于RM本地事务的TCC事务框架,每个Try/Confirm/Cancel业务可以看做一个原子服务:提交一个RM本地事务,所有参与RM本地事务的Try/Confirm/Cancel事务都Confirm/取消业务操作生效;否则,两者都不生效。掌握每个RM本地事务的状态及其与Try/Confirm/Cancel业务方法的对应关系。基于此,TCC事务框架可以有效构建TCC全局事务。RM上TCC服务的Try/Confirm/Cancel业务方法的数据访问操作,RM本地事务由Spring容器的PlatformTransactionManager提交/回滚,TCC事务框架想知道RM本地的状态事务,只能通过接管Spring的事务管理器功能。2.1.为什么TCC事务框架需要掌握RM本地事务的状态?首先,根据TCC机制的定义,TCC事务通过执行Cancel服务来实现回滚的效果。仔细分析,这里隐含一个事实:只有有效的Try业务操作才需要执行相应的Cancel业务操作。也就是说,只有Try业务操作涉及的RM本地事务被提交,后续TCC全局事务回滚时才需要执行相应的Cancel业务操作。否则,如果Try业务操作涉及的RM本地事务回滚,则后续TCC全局事务回滚时,其Cancel服务无法执行。这时候如果盲目执行Cancel服务,会导致数据不一致。其次,确认/取消业务操作必须保证生效。Confirm/Cancel业务操作也涉及RM数据访问操作,其参与的RM本地事务也必须提交。TCC事务框架在将TCC全局事务标记为完成之前,需要知道Confirm/Cancel业务操作中涉及的所有RM本地事务都已成功提交。如果TCC事务框架误判了参与RM本地事务的Confirm/Cancel业务的状态,会造成全局事务不一致。最后,对于未完成的TCC全局变量,TCC事务框架必须重试提交/回滚操作。重试时,会再次调用各个TCC服务的Confirm/Cancel业务操作。如果之前某个服务的Confirm/Cancel业务已经生效(其参与的RM本地事务已经提交),重试时不要再调用。否则,如果多次调用其Confirm/Cancel业务,就会出现“服务幂等性”的问题。2.2.拦截TCC服务的Try/Confirm/Cancel业务方法的执行。根据异常信息,能否知道RM本地事务是否commit/rollback?基本上很难做到。你为什么这么说?首先,事务可以在多个(本地/远程)服务相互传播它们的事务上下文时,业务方法(Try/Confirm/Cancel)的执行不一定会触发当前事务的提交/回滚操作。例如,当传播的事务上下文的业务方法开始执行时,容器不会为其创建新的事务,而是为其调用者参与的事务,这样这两个操作就在同一个事务中;同样,在执行结束时,容器不会提交/回滚它参与的事务。因此,此类业务方法的异常并不能反映它们是否有效。如果不接管Spring的TransactionManager,就无法知道事务何时创建,也无法知道何时提交/回滚。其次,一个业务方法可能包含多个RM本地事务。例如:A(REQUIRED)->B(REQUIRES_NEW)->C(REQUIRED),此时A服务涉及的RM本地事务提交时,服务B、C涉及的RM本地事务可能会被提交回滚.第三,不是业务方法抛出异常,它参与的事务被回滚。Spring容器的声明式事务定义了两类异常,它们的事务完成方向不同:系统异常(通常是Unchecked异常,默认事务完成方向是rollback),应用程序异常(通常是Checked异常,默认事务完成方向是犯罪)。两者的事务完成方向可以通过@Transactional配置显式指定,比如rollbackFor/noRollbackFor等。第四,Spring容器也支持使用setRollbackOnly显式控制事务完成的方向;最后,自己拦截业务方法的拦截器和Spring的事务处理的拦截器也存在执行顺序和拦截范围等问题。比如先执行自拦截器,会出现业务方法虽然执行了,但是它参与的RM本地事务还没有commit/rollback。TCC事务框架的定位应该是一个TransactionManager,其职责是负责commit/rollback事务。一个事务是提交还是回滚,应该由Spring容器来决定:当Spring决定提交事务时,会调用TransactionManager完成提交操作;当Spring决定回滚事务时,会调用TransactionManager完成回滚操作。TCC事务框架接管了Spring容器的TransactionManager,可以清晰的获取Spring的事务指令,管理Spring容器中各个服务的RM本地事务。否则,如果使用自拦截机制,业务系统会有两套事务处理逻辑:TCC事务处理和RM本地事务处理。在这种情况下,有必要协调TCC的全球事务。基本上可以说本地事务都管不了,更别说分布式事务了。3、TCC事务框架应该有故障恢复机制。一个TCC事务框架,如果没有故障恢复的保证就不是分布式事务框架。分布式事务管理框架的职责不是制定全局事务提交/回滚指令,而是管理全局事务提交/回滚的过程。需要能够协调多个RM资源和多个节点的分支事务,保证它们按照全局事务的完成方向完成自己的分支事务。这并不容易做到。因为,在实际应用中,会出现各种各样的故障,其中很多会导致事务中断,从而无法实现全局事务统一提交/回滚的目标,甚至出现“部分分支事务已提交,其他分支事务已提交”的情况。已经承诺了。”回滚”情况。比较常见的故障,如:业务系统服务器宕机、重启;数据库服务器宕机、重启;网络故障;断电等。这些故障可能单独出现,也可能组合出现。作为一个分布式事务框架,它应该具备相应的故障恢复机制,忽视这些故障的影响是不负责任的。一个完整的分布式事务框架应该保证即使在最苛刻的条件下也能保证全局事务的一致性,而不仅仅是在最理想的环境下。退一步说,如果真有所谓的“理想环境”,那就没必要用分布式事务了。为了支持TCC事务框架中的故障恢复,必须记录相应的事务日志。事务日志是故障恢复的基础和前提,它记录了事务的各种数据。TCC事务框架在做故障恢复时,可以根据事务日志的数据,将中断的事务恢复到正确的状态,并在此基础上继续执行之前未完成的提交/回滚操作。关注微信公众号:Java技术栈,后台回复:架构,可以获得我整理的N篇架构教程,都是干货。4.TCC交易框架应该为Confirm/Cancel服务提供幂等保证。一般认为服务的幂等性是指多次(n>1)次请求同一个服务和一次(n=1)次请求。)请求,两者具有相同的副作用。在TCC事务模型中,Confirm/Cancel业务可能会因为多种原因被重复调用。比如全局事务提交/回滚时,会调用各个TCC服务的Confirm/Cancel业务逻辑。在执行这些确认/取消服务时,可能会出现网络中断等故障,导致全球交易无法完成。因此,故障恢复机制后面仍然会重新提交/回滚这些未完成的全局事务,从而再次调用参与全局事务的各个TCC服务的Confirm/Cancel业务逻辑。由于Confirm/Cancel业务可能会被多次调用,因此需要保证其幂等性。那么,TCC事务框架是否应该提供幂等性保证呢?还是业务系统本身应该保证幂等性?我个人认为TCC事务框架应该提供幂等性保证。如果只是少数服务有这个问题,那么也有可能是业务系统的原因;但是,这是一种公共问题。毫无疑问,所有TCC服务的Confirm/Cancel业务都存在幂等性问题。TCC服务的公共问题应该由TCC交易框架来解决;而且,如果把业务系统需要考虑的问题考虑到负责幂等性,你会发现这无疑增加了业务系统的复杂度。5、TCC事务框架不能盲目依赖Cancel服务来回滚事务。上面说到,TCC事务通过Cancel服务回滚Try服务的机制隐含了一个事实:Try操作已经生效。也就是说,只有当Try操作涉及的RM本地事务已经提交时,才需要执行Cancel操作进行回滚。未执行的Try业务,或已执行但其RM本地事务回滚的Try业务,不得执行回滚其Cancel业务。因此,当全局事务回滚时,TCC事务框架应该根据TCC服务的Try服务的执行情况,选择合适的处理机制。而不是盲目执行Cancel业务,否则会导致数据不一致。TCC服务的Try操作是否生效应该是TCC事务框架知道的,因为其Try业务涉及的RM事务也是由TCC事务框架提交/回滚的(前提是TCC事务框架接管了Spring的事务管理器)。建议:不懂分布式事务?我给你解释一次。因此,当TCC事务回滚时,TCC事务框架可以考虑以下处理策略:如果TCC事务框架发现某个服务的Try操作的本地事务没有提交,则直接回滚,则无需执行服务业务的注销;如果TCC事务框架发现某个服务的Try操作的本地事务已经回滚,则不需要执行该服务的cancel服务;如果TCC事务框架发现某个服务的Try操作还没有执行,那么就没有必要再执行该服务的取消服务。简而言之,TCC事务框架应该保证:有效的Try操作应该被它的Cancel操作回滚;无效的Try操作不应执行其Cancel操作。这不是幂等性可以解决的问题。上面说过,幂等性是指一个服务执行一次和执行n次(n>0)的影响是一样的。但是不被执行和被执行的效果肯定是不一样的,不属于幂等的范畴。第六,Cancel业务与Try业务并行,甚至先于Try操作完成。这应该算是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]不能执行,至少不能take效果(已经执行过RM的本地事务还需要回滚)。但是,当[B:Cancel]处理完(跳过执行,或者执行后回滚其RM本地事务),[B:Try]操作完成并生效(其RM本地事务提交成功),这会使[虽然提供了B:Cancel],没有起到回撤[B:Try]的作用,导致数据不一致。因此,在这种情况下,TCC框架需要:将[B:Try]的本地事务标记为rollbackOnly,防止其生效;禁止它再次将事务上下文传递给其他远程分支,否则问题会在其他分支上出现;相应的,[B:Cancel]也不一定要执行,至少不能生效。当然,TCC事务框架也可以简单的选择阻塞[B:Cancel]的处理,待[B:Try]执行完毕后再判断是否执行[B:Cancel]。但是由于这种处理方式需要等待,处理效率会比较差。同样的情况也会出现在confirm业务中,但是confirm业务中出现的处理逻辑会和cancel业务中出现的处理逻辑不同。TCC框架必须保证:Confirm业务在Try业务之后执行。如果发现并行,则只能屏蔽对应的Confirm业务操作;进入Confirm执行阶段后,不能在同一个全局事务局部事务中提交新Try操作的RM。7、TCC服务的复用性是不是比较差?TCC事务机制的定义决定了一个服务需要提供三种业务实现:Try业务、Confirm业务、Cancel业务。有人可能因此认为TCC服务的复用性差。怎么说呢,如果把Try/Confirm/Cancel业务逻辑单独复用,它的复用性当然不好。Try/Confirm/Cancel逻辑作为TCC服务的一部分,不能单独作为组件复用。Try、Confirm和Cancel服务一起构成一个组件。如果要重用它,应该重用整个TCC服务组件,而不是单独的Try/Confirm/Cancel服务。8、TCC服务是否需要对外暴露三个服务接口?不需要。和普通服务一样,TCC服务只需要暴露一个接口,就是它的Try服务。提供Confirm/Cancel业务逻辑只是因为需要全局事务的commit/rollback,所以Confirm/Cancel业务只需要被TCC事务框架发现,不需要被其他调用它的业务服务感知。也就是说,当业务系统的其他服务需要调用TCC服务时,根本不需要知道是不是TCC服务。因为,TCC服务只能通过其Try业务被其他业务服务调用,Confirm/Cancel业务不能被其他业务服务直接调用。9、TCC服务A的Confirm/Cancel服务能否调用其依赖的TCC服务B的Confirm/Cancel服务?最好不要这样做。首先,没有必要。TCC服务A依赖TCC服务B,那么[A:Try]已经将事务上下文传播给[B:Try],TCC事务框架就可以调用各自的Confirm/Cancel业务;其次,如果Confirm/Cancel业务允许调用其他服务的话,就有可能再次发起新的TCC全局事务。如果这样递归下去,全局的事务关系就会变得混乱不可控。对于TCC全局事务,应该在Try操作阶段尽可能传播事务上下文。Confirm/Cancel操作阶段只需要完成各自Try业务操作的确认操作/补偿操作,不适合远程调用,更不适合对外传播事务上下文。综上所述,本文倾向于认为实现一个通用的TCC分布式事务管理框架是比较复杂的。一般业务系统如果需要使用TCC事务机制,不建议自行设计实现。在这里推荐一个开源的TCC分布式事务管理器ByteTCChttps://github.com/liuyangming/ByteTCCByteTCC是基于Try/Confirm/Cancel机制实现的,可以与Spring容器无缝集成,兼容Spring的声明式事务管理.对dubbo框架和SpringCloud提供开箱即用的支持,可以满足多数据源、跨应用、跨服务器等多种分布式事务场景的需求。
