07_ByteTCC源码分析 0.4.x版本

一. 说明

这篇博客仅供我自己复习使用,不保证内容100%正确,使用的是ByteTCC 0.4.x版本,配合Spring Cloud使用。

github地址: https://github.com/liuyangming/ByteTCC/wiki

反正是写给我自己的,就不去写如何使用了,以后若是想用,直接参考官网提供的Demo即可。

ByteTCC似乎在引导我们开发人员把大量的业务操作直接放到try中,一旦出错,便使用cancel来回滚,而confirm仅仅只是做确认操作,表示try执行完毕,真正的对数据产生了影响。这一点与我之前理解的在try中做资源锁定,把业务逻辑放到confirm中做有着非常大的差异。
之所以会有这样的体会,是因为我的业务场景内,服务A在调用服务B、C、D、E许多其它服务,调用服务C时需要依靠调用服务B返回的结果,调用服务D需要使用调用服务C的返回结果。起初我考虑把业务逻辑写到confirm中,但是写着写着总觉得哪里不对,ByteTCC框架设计理念是在try阶段传播事务上下文,如果我贸然把服务A调用服务B、C的业务逻辑写到了confirm,这里可能就要重新开启一个分布式上下文了,所以看起来在confirm阶段不适合再做远程调用。
我甚至猜测,正是因为上述理由,导致ByteTCC敢在confirm阶段执行失败后,不做cancel,而是不断的重试confirm,因为ByteTCC框架认为,既然能走到confirm阶段,那么try阶段必然成功,放在try阶段的远程服务调用和主要业务逻辑(比如涉及到本地事务)肯定也提交了(这个是TM控制本地事务来保证的),而confirm阶段,往往放的是资源确认之类的操作,一般不会执行失败,就算偶尔失败了,大概率是网络阻塞、抖动之类的问题导致的,大不了重试几次就好了。

二. ByteTCC启动时做了哪些事情?

首先,随着项目启动,Spring会读取启动类上使用的@ImportResource注解,将bytetcc-supports-springcloud.xml中配置的所有Bean给创建出来,并导入Spring容器。

ByteTCC自定义了数量众多的类,值得注意的类有:

  1. CompensableCoordinatorController
    提供了prepare、commit、forget、recover、rollback等接口,这些接口不是用来给我们调用的,而是留给ByteTCC的TM来调用,实现对分布式事务的控制。这个类被标注了@Controller给注释了。
  2. CompensableAnnotationValidator
    用于项目启动后,扫描和验证当前项目中有哪些类上使用了@Compensable注解。
  3. CompensableFeignBeanPostProcessor
    为FeignInvocationHandler做了一个动态代理。还记得Feign为远程调用的服务分别做了一个匿名的动态代理类,所有对远程服务的调用请求都会被FeignInvocationHandler拦截下来,而ByteTCC在FeignInvocationHandler的外面,又套了一层拦截,把对Feign的所有操作都给拦截下来,对应的拦截类是CompensableFeignHandler,那么想都不用想,对远程服务的调用肯定是先走CompensableFeignHandler,再走FeignInvocationHandler,最后由FeignLoadBalancer根据负载均衡算法,通过Ribbon提供的ServerOperation把请求发给远程服务。

接着,除了创建对象外,ByteTCC对数据源也进行了封装,其实就是对原生的Datasource做了一个动态代理,所有对原生Datasource的请求会先走LocalXADatasource(由ByteTCC提供),底层再走原生Datasource的方法。(当然了,我们自己也可以做DruidDatasource,套在原生Datasource上,外面再套一层LocalXADatasource) 。

最后,伴随着项目的启动,ByteTCC还创建了一堆后台线程,比较重要的线程有:

  1. CompensableWork
    用于系统启动后尝试恢复事务,以及运行期间不断的尝试恢复中断的事务。
  2. CleanupWork
    做一些清理工作,比如RecoveredResource的forget()方法,也就是delete from bytejta where xid = ?删除bytejta表中已经执行完毕的事务对应的记录。
  3. SamleTransactionLogger
    记录分布式事务执行时的日志

三. 接收到外部请求后,主业务服务是如何向从业务服务发起try请求的?

接收到外部请求后,主业务服务主要经历了以下几个步骤:

3.1 ByteTCC框架提供的CompensableHandlerInterceptor preHandle( )

对于主业务服务而言,首次接收到请求,这个拦截器并没有什么帮助,因为它主要是用来解析header中存放的分布式事务的信息。由于现在是首次接到请求,所以请求中根本就没有分布式事务的信息。

3.2 ByteTCC框架提供的CompensableMethodInterceptor invoke()

ByteTCC为被@Compensable修饰的类做了动态代理,现在外部请求想要调用这个类的方法,那么请求当然会被拦截。这里其实就是在开启一个分布式事务,创建分布式事务的id,以及分布式事务的上下文(TransactionContext)等等。

3.3 spring-tx提供的TransactionInterceptor invoke( )

被调用的方法肯定会被@Transactional修饰,所以spring-tx肯定会做一个动态代理,用于实现对这个方法的事务管理。TransactionInterceptor 的invoke()方法,老生常谈了,不就是先创建事务,然后执行业务逻辑,有报错执行回滚,没报错事务提交么。这里就是创建了一个本地事务,接着执行业务逻辑。想想看,主业务服务的本地业务逻辑被执行,这个方法内会去调用从业务服务的方法,只要涉及到调用远程服务的方法,必然要走Feign的动态代理,走Ribbon,所以接下来请求一定会被CompensableFeignHandler给拦截下来。

4. ByteTCC框架提供的CompensableFeignHandler invoke( )

这个方法内代码一大坨,比如重构了负载均衡器,核心代码就一句: this.delegate.invoke(proxy, method, args);delegate就是HardCodedTarget,这玩意不就是Feign底层用来把请求封装成Request,交给LoadBalancerFeignClient,最后一步一步的借助Ribbon来发送请求的,请求发送的过程学了很多遍,不赘述了。

但值得注意的是,在通过ribbon的负载均衡选择机器时,ByteTCC框架为其做了一道拦截器,在真正的发送请求之前,会走CompensableInterceptorImpl的beforeSendRequest( )方法,

这里面有一句非常重要的代码

boolean participantEnlisted = transaction.enlistResource(descriptor);

这个方法会创建一个分布式事务的子事务(XAResourceArchive),每一个子事务对应着一个从业务服务的请求调用,最后放入resourceList内。本次主业务服务的方法内,所有对从业务服务的请求调用,都会生成一个子事务,存入resourceList。

这个resourceList非常重要,并且需要注意的是,此时try请求还没有发送出去!

注意: 这里有一个坑,enlistResource()方法内会判断当前执行的分布式子事务的状态是不是Status.STATUS_ACTIVE,如果不是,则会抛出IllegalStateException。如果有人在confirm的方法内调用了一个没有在try中执行过的普通远程服务,由于没有经历try阶段,这个普通远程服务对应的分布式子事务的状态一定不是Status.STATUS_ACTIVE,这就会导致抛出IllegalStateException。值得注意的是,此时可是在ribbon的chooseServer()阶段,一旦报错后,ribbon会误以为待调用的远程服务找不到对应的服务实例,并抛出对应的异常。

所以说,不要在confirm阶段调用非tcc接口(没有经历过try阶段),最好是分开写,TCC事务相关的方法写在一起,其它的服务调用独立成另一个方法。

四. 从业务服务try请求执行报错了怎么办?

4.1 主业务服务如何处理从业务服务的响应结果?

还是要回到CompensableFeignHandler的invoke( )。这里面有两个变量: participantEnlistFlag和participantDelistFlag。虽然我没有细看Feign底层发起http请求至从业务服务,解析响应结果,并填入CompensableTransactionImpl的participantEnlistFlag和participantDelistFlag内的具体过程,但是通过打断点,可以大概猜测出含义:
1.participantEnlistFlag
这个东西就是用于标识,本次分布式子事务是否需要加入分布式事务,它是一个布尔值,第一次调用从业务服务的方法时,显然就是true了。加入的地方就是resourceList。
2. participantDelistFlag
这个东西就是用于标识,是否需要从分布式子事务的列表中,移除当前分布式子事务。

我猜测ByteTCC就是用participantDelistFlag这个变量来控制下方的代码,实现是否需要在resourceList中,移除当前分布式子事务。如果从业务服务的try执行失败了,那么participantDelistFlag就等于true,那么就执行remove方法。

这里就是利用participantDelistFlag来移除分布式子事务。比如说,本次请求从业务服务的try方法执行失败了,显然就需要把这个从业务服务对应的分布式子事务从resourceList给移除掉。(写的思路有点儿跳跃,这个地方移除子事务是有用意的,毕竟resourceList剩余的子事务肯定是try执行成功的,现在有一个try执行失败了,只需要把它给移除掉,那么剩下来的子事务就可以用来发送cancel请求了)

下方的代码来自CompensableTransactionImpl的delistResource()方法,XAResource.TMSUCCESS代码代表分布式子事务执行成功,XAResource.TMFAIL代表执行失败。

public boolean delistResource(XAResource xaRes, int flag) throws IllegalStateException, SystemException {
	省略.. 
	if (RemoteResourceDescriptor.class.isInstance(xaRes)) {
		省略.. 
		XAResourceArchive archive = this.resourceMap.get(identifier);
		if (flag == XAResource.TMFAIL) {
			this.resourceMap.remove(identifier);
			if (archive != null) {
				this.resourceList.remove(archive);
			} // end-if (archive != null)

			compensableLogger.updateTransaction(this.getTransactionArchive());
		} else {
			if (archive != null) {
				this.applicationMap.put(resource.getApplication(), archive);
			}
		}
	}
	return true;
}

4.2 如果发现从业务服务的try执行出错,主业务服务做了哪些操作?

如果发现某个从业务服务的try执行出错,主业务服务会执行CompensableTransactionImpl内的方法,无非就是两个步骤:

  1. 回滚主业务服务的本地事务 对应fireNativeParticipantCancel( )
  2. 回滚从业务服务的事务 对应fireRemoteParticipantCancel( )

代码逻辑都很简单,值得注意的就一点,那就是需要回滚哪些从业务服务的事务呢?

这里就需要借助之前一直强调的resourceList了,首先,被调用了try接口的服务都会放到resourceList中,接着会把那些执行报错的从业务服务给移除掉,那么resourceList剩下的从业务服务,一定都是try执行成功的,我们需要回滚的就是这些从业务服务执行的try逻辑。

此时,主业务服务会循环遍历resourceList,取出每一个从业务服务,借助SpringCloudCoordinator(这个东西也是ByteTCC自己写的),通过RestTemplate等组件发送cancel请求至从业务服务。

问:为什么不需要回滚try阶段执行报错的从业务服务的事务呢?

答:由于从业务服务自己的try方法上一定会加@Transactional,所以从业务服务借助DatasourceTransactionManager,自己就可以回滚本地事务了,根本就不需要主业务服务来帮忙。

4.3 从业务服务接收到cancel请求后,是如何处理的?

从业务服务接收请求的controller是ByteTCC提供的,叫做CompensableCoordinatorController,它的执行逻辑也很简单,首先解析请求头,反序列化出分布式事务的具体信息,接着根据启动时扫描获取的@Compensable注解信息,找到cancellableKey属性的值,然后在spring容器内找到对应的类,最后通过反射调用对应的方法,完成cancel逻辑。

五. 如果某个从业务服务的try执行失败,主业务服务如何发起cancel请求?

还是顺着思路捋一遍吧,从业务服务的try执行失败,意味着主业务服务在TransactionInterceptor #invoke()方法中,invocation.proceedWithInvocation();会向外抛出异常,那么程序一定会走到catch中,也就是要执行completeTransactionAfterThrowing(),而这个方法背后其实就是在调用TM的rollback()方法,这里显然调用的不是原生的TM,而是由ByteTCC提供的TM,所以,现在只需要看看这个被定制的TM到底是如何实现rollback的。

对应代码: org.bytesoft.bytejta.TransactionManagerImpl
在这里插入图片描述
看到166行,transaction.rollback(),底层走的是CompensableTransactionImpl的rollback(),这个方法有一大坨代码,然而核心的代码就两句:

this.fireNativeParticipantCancel();

this.fireRemoteParticipantCancel();

看方法名字都能轻松得知,不就是先执行本地配置的cancel,回滚本地事务,然后执行远程“所有”子业务服务的cancel,回滚子事务么。

  1. this.fireNativeParticipantCancel();
    作用:主业务服务补偿机制。首先获取主业务服务的分布式事务对象,然后获取在启动时就提前扫描好的Compensable相关注解信息,接着找到cancellable属性对应的值,最后在Spring容器中取出这个值对应的类(也就是CancelService呗),通过反射,执行它的方法。这就相当于完成了主业务服务的分布式事务回滚。
    写到这里我不禁有了一个疑惑,主业务服务自身的本地事务通过原生TransactionInterceptor+TM不就可以完成了么,为什么还要像其他从业务服务一样,特意写一套try、confirm和cancel呢?之所以要用cancel,我怀疑是为了兼容链式调用,想想看,在一个冗长的分布式调用链路上,每一个拥有后继节点的节点都能被视为刚才说的"主业务服务",自己的try提交后的内容,通过fireNativeParticipantCancel()来补偿,而在他身后紧跟着的服务就是他的"子业务服务",可以通过fireRemoteParticipantCancel()来补偿,做到统一处理。

  2. this.fireRemoteParticipantCancel();
    作用: try执行成功的所有从业务服务的补偿机制。

六. 所有从业务服务try执行成功后,主业务服务如何调用confirm的?

回到主业务服务的TransactionAspectSupport,当所有的从业务服务try阶段执行成功后,此时主业务服务即将执行commitTransactionAfterReturning,准备着手提交事务。

protected void commitTransactionAfterReturning(TransactionInfo txInfo) {
    if (txInfo != null && txInfo.hasTransaction()) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
        }
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}

这里会调用ByteTCC定制的TM,也就是TransactionManagerImpl,执行commit操作。

... 省略代码
transaction.commit();
transaction.forgetQuietly(); 
... 省略代码

TransactionManagerImpl的commit()方法内,核心代码也就以上两句话,先提交事务,事务提交成功后,再从各个业务服务中,移除当前分布式事务(底层就是删除bytetcc数据表中,对应txid的那条数据)。

先来看看transaction.commit();

if (this.transactionStatus == Status.STATUS_ACTIVE) {
    this.fireCommit();
} else if (this.transactionStatus == Status.STATUS_MARKED_ROLLBACK) {
    this.fireRollback();
    throw new HeuristicRollbackException();
} else {
	...省略
}

若当前分布式事务的状态是正常的,则说明try阶段中,所有的子业务服务全部执行成功,此时执行this.fireCommit()。这个方法的里面的代码非常简单,就是提交本地事务,以及提交远程事务。不难发现,无论是哪种事务提交,都被包裹在不同的try/catch中,也就是说,就算主业务系统的本地事务执行confirm失败,也不会影响向远程子业务系统发送confirm请求。

代码省略...
try {
	this.fireNativeParticipantConfirm();
} catch() {}

代码省略...
try {
	this.fireRemoteParticipantConfirm();
} catch() {}

接下来的代码就没什么可说的了,无外乎就是获取主业务服务启动时解析的@Compensable中confirmableKey的配置,从Spring容器中取出对应的ConfirmService类,通过反射执行confirm操作,以及循环遍历ResourceList,分别取出子业务服务,借助SpringCloudCoordinate,向子业务服务的发送confirm请求。

若当前分布式事务的状态是异常的,比如Status.STATUS_MARKED_ROLLBACK,被标记成需要回滚,则会执行this.fireRollback()。

try {
	this.fireNativeParticipantCancel();
} catch() {}

try {
	this.fireRemoteParticipantCancel();
} catch() {}

执行的操作与confirm几乎一样,把confirm替换成cancel即可。

rollback在最后会向外抛出HeuristicRollbackException异常。

七. 如果confirm或cancel执行失败,主业务服务是如何不断重试的?

起初,我在发起confirm或者cancel请求的类,也就是CompensableTransationImpl中尝试寻找这段重试逻辑,结果没找到。执行失败后,仅仅只是对分布式子事务进行了一些标记,修改了一些状态。

接着,我在CompensableCoordinatorController中,也没能找到重试逻辑,请求发送失败后,也仅仅只是打印一些日志,记录本次请求获取的响应状态,比如500之类的。

那么confirm活着cancel执行失败后,主业务幅度按不断重试的逻辑究竟在哪里呢?

其实是CompensableWork,伴随着系统启动,ByteTCC会初始化一条CompensableWork线程,它的工作主要是每隔60秒获取所有的分布式事务,找到它们内部关联的出现故障的分布式子事务,接着执行recover操作,调用你的commit或者rollback接口,尝试完成你的分布式事务

如果尝试一次后,你的commit或者rollback接口仍然调不通,或者执行失败,则出现故障的分布式事务操作仍然会停留在xidToErrTxMap中。那么等到60秒后,CompensableWork会再一次的尝试向这些从业务服务发送commit或者rollback请求。

当且仅当发送请求,并获得成功响应后,bytetcc框架才会把这些分布式子事务从xidToErrTxMap内移除。

看看TransactionRecoveryImpl的代码

private void recoverCoordinator(Transaction transaction) {
    ...省略
    switch (transaction.getTransactionStatus()) { 
        case Status.STATUS_ACTIVE:
        case Status.STATUS_MARKED_ROLLBACK:
        case Status.STATUS_PREPARING:
        case Status.STATUS_UNKNOWN:
            transaction.recoveryRollback();
        case Status.STATUS_ROLLING_BACK: 
            transaction.recoveryRollback();
        case Status.STATUS_COMMITTING:
            transaction.recoveryCommit();
    }
}

无论是STATUS_ROLLING_BACK,还是STATUS_COMMITTING,只要分布式子事务的状态处于这种中间的、正在执行的状态,一律视作上一次操作没有执行成功,因为如果执行成功了,状态应该发生变化啊。

如果之前分布式子事务处于"回滚中"的状态,那就说明上一次回滚没有执行成功,所以bytetcc框架就会重新发送rollback请求至从业务服务。

如果之前的分布式子事务处于"提交中"的状态,那就说明上一次提交没有执行成功,所以bytetcc框架就会重新发送commit请求至从业务服务。

Tips: cancel对于bytetcc框架底层之间传递请求时,使用的就是rollback。

总结:

bytetcc搞了一个Map来存confirm或者cancel执行失败的分布式子事务,在系统启动时,又搞了一个线程,每隔60秒扫描一次Map,遍历这些分布式子事务,按照之前失败时保存的状态,有选择性的重新发送confirm或者cancel请求。

八. 如果tcc分布式事务执行到一半,系统宕机了,重启后是如何恢复事务的?

当执行了分布式事务后,bytetcc框架创建一个日志文件,记录事务的活动日志。只要这份文件没有丢失,那么当系统宕机并重启后,bytetcc框架就能读取这份日志文件,重新执行上一次没有执行完成的分布式事务。

如果在执行分布式事务的过程中,主业务服务宕机了,重启后,主业务服务会读取本地的活动日志,解析出各个分布式子事务的执行情况,根据它们当前的状态来判断,到底是做confirm请求,还是做cancel请求。

所以宕机后,恢复事务的关键就在于那份记录了分布式事务的活动日志。

九. 链式调用是如何实现的?

比如服务链条是这样的:

服务A -> 服务B -> 服务C -> 服务D

首先是try阶段,服务A调用服务B的try,服务B调用服务C的try,每个服务只需要管好自己的下家。

假设服务C执行报错,并且没有调用服务D。

只要有任何一个服务的try执行报错,这个错误就会向前传导,直到传给服务A。所以,位于头部的服务A就会向他的下家(服务B)发送cancel,服务B会向服务C发送cancel请求,注意,这里服务C就不能盲目的调用服务D的cancel了,还记得resourceList吧,由于服务C自己执行都报错了,没有调用服务D的try接口,所以不会向服务D发送cancel。

当且仅当链条中所有服务的try全部执行成功,位于头部的服务A就会向他的下家发送confirm,现在问题来了,服务B如何知道要向服务C发送confirm呢?服务C又是如何知道要向服务D发送confirm呢?

很简单,try阶段时,每个服务都会将自己调用了的远程服务放到resourceList中,所以在confirm阶段,各自都非常清楚,需要向哪个服务(自己的下家)发送confirm请求。

cancel阶段同理。

我认为,bytetcc链式调用的思想在于去中心化,只要能做到,每个服务管好自己的下家,把执行情况返回给上家,这样就够了。调用链路的头结点不需要控制所有的节点,它只需要控制自己的下家,由下家来传导请求就OK了。

强调: 无论是try阶段、confirm阶段还是cancel阶段,一定要把方法内产生的异常往外抛,不要自己给压制了,不然上游服务无法掌握你真正的执行情况。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值