TCC分布式事务----以Hmily框架为例

插曲:RocketMQ的Half Message

先引入一个插曲,RocketMQ为什么要有Half Message
在这里插入图片描述
为什么不在本地事务提交之后,直接发一个commit消息不就行了,为什么还要先发一个可以撤回的、不能被消费的half message,再执行本地事务呢?这其实是一种状态转移:Producer把事务开始执行这个状态转移到了RocketMQ的Server,这样一来,即使Producer再执行完本地事务之后进行重启,Server由于已经根据halfMessage知道了这个事务执行的状态,所以会去主动轮询Producer。因此HalfMessage的使用需要配合一个可以提供事务状态检查的接口。

TCC业界实现

tcc-transaction
https://github.com/changmingxie/tcc-transaction
tx-lcn
https://github.com/codingapi/tx-lcn
hmily
https://github.com/dromara/hmily

Hmily

这里小马哥讲的有明显两个问题

  1. 小马哥说每个服务的confirm是在try之后立马执行的,这其实是有问题的。真正的confirm是在所有的try都成功之后,发起者的try整个结束之后,由TxManager异步调用的
  2. undolog在TCC模式下根本就没用。小马哥一直在说什么undolog,但其实TCC模式下的补偿是由业务来实现的,而不是undolog。

除此之外,我还有额外的一个困惑

  1. 如果某个confirm/cancel执行失败了会怎么办,会重复调用吗?但是为什么示例给的confirm并不是一个幂等操作?在这里插入图片描述

源码分析

下面对Hmily的源码进行分析
在这里插入图片描述
makePayment方法执行的updateOrderStatus、accountService.payment、inventoryService.decrease其实是三个try操作,其中updateOrderStatus是本地服务调用,而剩下两个是RPC。本地的更新订单状态的try,对应的confirm和cancel通过@HmilyTCC这个注解进行指定,对应RPC调用的服务,服务提供者的方法上也有指定相应的Confirm和Cancel

accountService
在这里插入图片描述
inventoryService
在这里插入图片描述
ok,那到这里其实应该明朗了一些:多个分布式事务的Try被挨个调用,这些事务的Confirm和Cancel操作则通过注解被指定,我们很容易知道,框架一定会通过@HmilyTCC这个注解进行AOP,这样一来,在try操作的执行前后,就有相当大的发挥空间。

Hmily会怎么发挥呢?不妨先设想一下TCC面临的问题

问题一:Try失败

如果某个Try失败了,比如说,我accountService调用失败了,那此时会怎么样?按照TCC的思想,此时应该对orderService调用Cancel操作,因为orderService在accountService的Try之前已经Try过了。那么问题来了,accountService作为一个远程服务,应该如何通知orderService进行Cancel呢?因此,在accountService的切面中,afterThrowing一定要做的一件事情就是,通知已经Try过的服务进行Cancel。怎么通知呢?通知给谁呢?执行Cancel的线程和执行Try的是一个线程吗?从Hmily的官网可以看到,Hmily的Cancel和Confirm是由TxManager异步调用的,也就是说,TxManager是一个独立于这三个服务之外的一个线程或是进程,专门管理整个TCC的全局事务。所以,afterThrowing会通知TxManager,TxManager会调用Cancel
在这里插入图片描述

所以,下面的思路是,顺着刚刚的思路,找到AOP,分析TxManager
在这里插入图片描述
AOP的主要逻辑集中在 HmilyGlobalInterceptor#invoke
在这里插入图片描述
invoke先加载了事务的上下文,顾名思义,上下文指的是这几个分布式事务之间共享的一些信息,其通过RpcParameterLoader#load获得。从RpcParameterLoader可以看出,上下文应该是通过RPC框架,比如Dubbo,进行传递的。加载完上下文之后,会继续往下执行

在这里插入图片描述
从这里可以看出,先通过getRegistry获得一个注册表,然后从注册表中选出一个Handler,调用handleTransaction。

getRegistry的逻辑如下
在这里插入图片描述
也就是,通过注解从REGSTRY中选择一个注册表,REGISTRY是一个静态变量,已经被初始化过
在这里插入图片描述
我们是TCC注解,所以选择TCC的实现,这里的设计思路很像Dubbo 框架的SPI Loader
在这里插入图片描述
注册表中被放入了很多Handler,这些Handler通过角色来获取,事务发起者和事务参与者的Handler是不一样的。在我们的例子中,orderService就是一个事务发起者START,而accountService和inventoryService就是事务参与者PARTICIPANT。让我们回到invokeWithinTransaction方法,通过方法签名上的分布式事务注解、当前角色,选定Handler之后,调用具体的handleTransaction实现。那么选择的逻辑,也就是select是什么呢?
在这里插入图片描述
如果当前上下文为空,也就是RPC的源头,那么就是发起者,则返回发起者的Handler。如果上下文不为空,则从上下文中找到角色,返回对应角色的Handler。我们当前的角色是Start,那么就找Start的Handler
在这里插入图片描述
下面去分析 StarterHmilyTccTransactionHandler
在这里插入图片描述
这里有两个非常重要的角色:executor和disruptor
point.proceed()就是执行原本的方法逻辑,即调用3个try。一旦抛出异常,则会调用executor.globalCancel(currentTransaction),而顺利执行完毕的话,则会调用executor.globalConfirm(currentTransaction)。那么disruptor.getProvider().onData() 我理解是将cancel和confirm进行了异步化处理。
在这里插入图片描述
所以disruptor只是一个异步化手段,暂时不做深入分析,这里重点关注的还是executor

首先是preTry
在这里插入图片描述
preTry构建了上下文对象,因为现在是START,所以还没有上下文 。上下文中设置了当前的角色START,动作TRYING,类型TCC等信息,随后将上下文放入了HmilyContextHolder中了,这个HmilyContextHolder是一个ThreadLocal,方便后面RPC调用时随时获取。

preTry之后就是调用切点方法的proceed了, 为了符合时序,我们的分析思路最好不要从executor.globalCancel(currentTransaction)或者executor.globalConfirm(currentTransaction)开始。这是因为在执行这两步操作之前的proceed,其实是调用了三个try操作的,本地的try,即更新订单,是本地服务,而剩下的两个try都是rpc,也都被标注了@HmilyTCC注解,因此分析他们的Handler也是很有必要的。所以这就需要我们分析ParticipantHmilyTccTransactionHandler

在这里插入图片描述
这个还是挺有趣的。如果是TRYING阶段,则会执行具体的proceed,而如果是CONFIRM或者是CANCELING阶段,则不会去执行proceed了,而是调用participantConfirm/participantCancel。我们目前只是进行了Try操作,是TRYING阶段,第62行和69行可以看到,如果当前服务的Try执行成功了,则万事大吉,记录下日志之后就返回。而如果Try抛出异常,则会删除当前参与者的日志记录,并且将异常往外抛。这个往外抛异常的操作,毫无疑问会引起本次RPC调用的失败,最终会进入到StarterHmilyTccTransactionHandler的catch中。而这个日志记录我觉得也很关键,因为它可以用来判定,当前参与者是否完成了Try操作,这决定了一旦出错,是否要对它执行Cancel。

所以,下面分析的重点,就来到了事务发起者的globalCancel和globalConfirm
在这里插入图片描述
globalCancel会设置当前全局事务的状态为CANCELING,然后遍历事务的参与者,挨个执行cancel操作。这里有个问题,我作为事务的发起者,如何知道有哪些参与者呢?hmilyParticipants来自currentTransaction,而currentTransaction 在外层来自ThreadLocal,也就是说,事务参与者执行完逻辑之后,会更新全局事务currentTransaction,然后通过RPC返回给START方。具体操作在ParticipantHmilyTccTransactionHandler调用的executor.preTryParticipant中
在这里插入图片描述
那么回到cancel,这个cancel是怎么执行到远程服务的cancel方法的呢?
在这里插入图片描述
HmilyParticipant的cancelHmilyInvocation应该是指定了cancel方法的信息
在这里插入图片描述
在这里插入图片描述
executeRPC应该就是执行具体调用cancel的RPC逻辑了。
在这里插入图片描述
这里我有个疑问,为什么cancel的RPC需要我从START方去调用,难道不是远程自己调吗?如果是从START来调,那远程岂不还要导出cancel方法?
但其实我看到accountService的cancel方法并没有被作为接口导出
在这里插入图片描述
前面提到过,HmilyParticipant会在RPC调用链时拦截调用并被构建出来。
在这里插入图片描述
第83行,调用之前,在第77行就被构建出来了。调用之后,在 89行,被注册了,这个时候,START才得以感知到Participant的存在。

而在ParticipantHmilyTccTransactionHandler在TRYING阶段执行 executor.preTryParticipant 的时候,会对HmilyParticipant进行构建,此时指定了自己的cancel和confirm。不过这个是怎么传递给调用方的呢?
在这里插入图片描述

难绷。。终于知道了,这个根根不会传递给调用方,而是自己解析出来cancel和confirm的方法之后放到本地缓存里了
在这里插入图片描述

放完之后,在cancel和confirm逻辑中可以之间拿到并调用本地
在这里插入图片描述
而STRAT发起globalcancel时,是RPC,此时不管是cancelInvocation还是confirmInvocation,都是指向的try方法的,这点可以从RPC Filter的DubboHmilyTransactionFilter#buildParticipant中看出
在这里插入图片描述
ok,那没问题,不管是globalCancel还是globalConfrim,执行的RPC都是调用远程的Try,至于具体的Cancel和Confirm操作交给远程决定。

终于可以回答问题了:Try失败之后,遍历参与者列表(这个列表中只有已经Try成功的参与者),然后调用Cancel,调用逻辑是向参与者的try发起RPC,会被AOP拦截,不会执行try而是执行注解上的cancel。

问题二:Cancel失败或者Confirm 失败怎么办

一个很蛋疼的事情:如果Cancel 或者 Confirm 执行失败, Hmily不会对其进行重试或者补偿。
在这里插入图片描述
在这里插入图片描述

可以看到,Hmily将confirm和cancel丢入了异步任务中,并且没有对异常进行任何处理。

会这么草率吗?不是记得有日志吗?是不是应该另外启一个线程,然后重复Confirm或者Cancel,幂等性交给业务方保证。

哦哦哦,看我发现了什么

在这里插入图片描述
ok,说明还是有恢复服务的,浅找了一下
HmilyTransactionSelfRecoveryScheduled # selfTccRecovery,里面有详细的TCC异常恢复逻辑

在这里插入图片描述

恢复思路大概是:

  1. 有最大重试次数,超过次数直接设置状态为DEATH
  2. 每次恢复,需要锁住该行,然后调用confirm或者cancel

这种恢复逻辑的存在,就需要我们保证Confirm和Cancel操作的幂等性。

https://dromara.org/zh/blog/hmily_introduction.html

这里面还提到了很多,比如针对RPC集群场景下,如何保证TRY,和Confirm路由到不同节点时,仍然可以从缓存中找到HmilyParticipant对象。关键就在于
这里的CacheLoader的逻辑是,如果不存在key,则调用load进行加载,而load则是从设置的日志库中读取。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、课程简介Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。       在本套课程中,我们将全面的讲解Spring Cloud技术栈, 从环境的部署到技术的应用,再到项目实战,让我们不仅是学习框架技术的使用,而且可以学习到使用Spring Cloud如何解决实际的问题。Spring Cloud各个组件相互配合,合作支持了一套完整的微服务架构。- 注册中心负责服务的注册与发现,很好将各服务连接起来- 断路器负责监控服务之间的调用情况,连续多次失败进行熔断保护。- API网关负责转发所有对外的请求和服务- 配置中心提供了统一的配置信息管理服务,可以实时的通知各个服务获取最新的配置信息- 链路追踪技术可以将所有的请求数据记录下来,方便我们进行后续分析- 各个组件又提供了功能完善的dashboard监控平台,可以方便的监控各组件的运行状况2、适应人群有一定的Java基础,并且要有一定的web开发基础。3、课程亮点       系统的学习Spring Cloud技术栈,由浅入深的讲解微服务技术。涵盖了基础知识,原理剖析,组件使用,源码分析,优劣分析,替换方案等,以案例的形式讲解微服务中的种种问题和解决方案l  微服务的基础知识n  软件架构的发展史n  微服务的核心知识(CAP,RPC等)l  注册中心n  Eureka搭建配置服务注册n  Eureka服务端高可用集群n  Eureka的原理和源码导读n  Eureka替换方案Consuln  Consul下载安装&服务注册&高可用l  服务发现与服务调用n  Ribbon负载均衡基本使用&源码分析n  Feign的使用与源码分析n  Hystrix熔断(雪崩效应,Hystrix使用与原理分析)n  Hystrix替换方案Sentinell  微服务网关n  Zuul网关使用&原理分析&源码分析n  Zuul 1.x 版本的不足与替换方案n  SpringCloud Gateway深入剖析l  链路追踪n  链路追踪的基础知识n  Sleuth的介绍与使用n  Sleuth与Zipkin的整合开发l  配置中心n  SpringClond Config与bus 开发配置中心n  开源配置中心Apollo4、主讲内容章节一:1.     微服务基础知识2.     SpringCloud概述3.     服务注册中心Eureka4.     Eureka的替换方案Consul章节二:1.     Ribbon实现客户端负载均衡2.     基于Feign的微服务调用3.     微服务熔断技术Hystrix4.     Hystrix的替换方案Sentinel章节三:1.     微服务网关Zuul的基本使用2.     Zuul1.x 版本的不足和替换方案3.     深入SpringCloud Gateway4.     链路追踪Sleuth与Zipkin章节四:1.     SpringCloud Config的使用2.     SpringCloud Config结合SpringCloud Bus完成动态配置更新3.     开源配置中心Apollo
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值