深度剖析 Seata TCC 模式图解 + 源码分析

Posted 后端进阶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度剖析 Seata TCC 模式图解 + 源码分析相关的知识,希望对你有一定的参考价值。

TCC 是分布式事务中的二阶段提交协议,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel),他们的具体含义如下:

  1. Try:对业务资源的检查并预留;
  2. Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功;
  3. Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。

TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。

Seata TCC 模式跟通用型 TCC 模式原理一致,我们先来使用 Seata TCC 模式实现一个分布式事务:

假设现有一个业务需要同时使用服务 A 和服务 B 完成一个事务操作,我们在服务 A 定义该服务的一个 TCC 接口:

注解开启全局事务,而服务 A 和服务 B 的 TCC 接口为事务参与者,Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。

TCC 接口可以是 RPC,也可以是 JVM 内部调用,意味着一个 TCC 接口,会有发起方和调用方两个身份,以上例子,TCC 接口在服务 A 和服务 B 中是发起方,在业务所在系统中是调用方。如果该 TCC 接口为 Dubbo RPC,那么调用方就是一个 dubbo:reference,发起方则是一个 dubbo:service。

Seata 启动时会对 TCC 接口进行扫描并解析,如果 TCC 接口是一个发布方,则在 Seata 启动时会向 TC 注册 TCC Resource,每个 TCC Resource 都有一个资源 ID;如果 TCC 接口时一个调用方,Seata 代理调用方,与 AT 模式一样,代理会拦截 TCC 接口的调用,即每次调用 Try 方法,会向 TC 注册一个分支事务,接着才执行原来的 RPC 调用。

当全局事务决议提交/回滚时,TC 会通过分支注册的的资源 ID 回调到对应参与者服务中执行 TCC Resource 的 Confirm/Cancel 方法。

从上面的 Seata TCC 模型可以看出,TCC 模式在 Seata 中也是遵循 TC、TM、RM 三种角色模型的,如何在这三种角色模型中实现 TCC 模式呢?我将其主要实现归纳为资源解析、资源管理、事务处理。

注解的 TCC 接口资源:

RemotingParser 接口主要有 isRemotingisReferenceisServicegetServiceDesc 等方法,默认的实现为 DefaultRemotingParser,其余各自的 RPC 协议解析类都在 DefaultRemotingParser 中执行,Seata 目前已经实现了对 Dubbo、HSF、SofaRpc、LocalTCC 的 RPC 协议的解析,同时具备 SPI 可扩展性,未来欢迎大家为 Seata 提供更多的 RPC 协议解析类。

在 Seata 启动过程中,有个 GlobalTransactionScanner 注解进行扫描,会执行以下方法:

io.seata.spring.util.TCCBeanParserUtils#isTccAutoProxy

该方法目的是判断 bean 是否已被 TCC 代理,在过程中会先判断 bean 是否是一个 Remoting bean,如果是则调用 getServiceDesc 方法对 remoting bean 进行解析,同时判断如果是一个发起方,则对其进行资源注册:

io.seata.rm.tcc.remoting.parser.DefaultRemotingParser#parserRemotingServiceInfo

方法对 remoting bean 进行解析,并将解析后的 remotingBeanDesc 放入 本地缓存 remotingServiceMap 中,同时调用解析类 isService 方法判断是否为发起方,如果是发起方,则解析 TwoPhaseBusinessAction 注解内容生成一个 TCCResource,并对其进行资源注册。

包含了 TCC 接口的相关信息,同时会在本地进行缓存。继续调用父类 registerResource 方法(封装了通信方法)向 TC 注册,TCC 资源的 resourceId 是 actionName,actionName 就是 @TwoParseBusinessAction 注解中的 name。

2、资源提交/回滚

io.seata.rm.tcc.TCCResourceManager#branchCommit

上下文,执行的参数就在上下文中。最后,执行 TCCResource 中获取 commit 的方法进行二阶段提交。

二阶段回滚同理类似。

扫描到 TCC 接口调用方(Reference)时,会使 TccActionInterceptor 对其进行代理拦截处理,TccActionInterceptor 实现 MethodInterceptor

TccActionInterceptor 中还会调用 ActionInterceptorHandler 类型执行拦截处理逻辑,事务相关处理就在 ActionInterceptorHandler#proceed 方法中:

方法分支注册,同时还会将 TCC 相关信息比如参数放置在上下文,上面讲的资源提交/回滚就会用到这个上下文。

在 TCC 模型执行的过程中,还可能会出现各种异常,其中最为常见的有空回滚、幂等、悬挂等。下面我讲下 Seata 是如何处理这三种异常的。

如何处理空回滚

什么是空回滚?

空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。

那么空回滚是如何产生的呢?

如上图所示,全局事务开启后,参与者 A 分支注册完成之后会执行参与者一阶段 RPC 方法,如果此时参与者 A 所在的机器发生宕机,网络异常,都会造成 RPC 调用失败,即参与者 A 一阶段方法未成功执行,但是此时全局事务已经开启,Seata 必须要推进到终态,在全局事务回滚时会调用参与者 A 的 Cancel 方法,从而造成空回滚。

要想防止空回滚,那么必须在 Cancel 方法中识别这是一个空回滚,Seata 是如何做的呢?

Seata 的做法是新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。

如何处理幂等

幂等问题指的是 TC 重复进行二阶段提交,因此 Confirm/Cancel 接口需要支持幂等处理,即不会产生资源重复提交或者重复释放。

那么幂等问题是如何产生的呢?

如上图所示,参与者 A 执行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到参与者 A 执行二阶段的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。

Seata 是如何处理幂等问题的呢?

同样的也是在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有有 3 个值,分别为:

  1. tried:1
  2. committed:2
  3. rollbacked:3

二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。

如何处理悬挂

悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。

那么悬挂是如何产生的呢?

如上图所示,在执行参与者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。

Seata 是怎么处理悬挂的呢?

在 TCC 事务控制表记录状态的字段 status 中增加一个状态:

  1. suspended:4

当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行成功。

最后,看到这里的读者,请安排下一键三连(点赞、在看、转发),这次一定好吧,原创不易,你的支持是我最大的动力!

关注公众号,后台回复关键字「后端可免费领取一份后端相关技术栈电子书!

再次感谢你的阅读,相遇是缘分,欢迎加我微信:295502545,围观朋友圈,做个点赞之交!

近期 Seata 相关文章

详解 Seata AT 模式事务隔离级别与全局锁设计

Seata RPC 模块的重构之路

我参与 Seata 开源项目的一些感悟

分布式事务中间件 Seata 的设计原理

Seata 分布式事务 XA 与 AT 全面解析

Seata 动态配置订阅与降级实现原理

Seata 配置中心实现原理

Seata 客户端需要同时启动 RM 和 TM 吗?

Seata AT 模式启动源码分析

分布式事务:Seata框架AT模式及TCC模式执行流程剖析

Seata角色术语

TC - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚,即Seata服务端。

TM - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务,在事务发起的客户端。

RM - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚,在分支事务执行的客户端。

Seata执行流程

Seata AT模式

流程图解

第一阶段

通过代理数据源DataSourceProxy对业务SQL进行解析,转换成undolog,并与业务SQL在一个事务内入库,然后注册分支事务、提交、上报状态。

技术图片

 

第二阶段

分布式事务操作成功,则TC通知RM异步删除undolog。

技术图片

 

 

 

分布式事务操作失败,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

技术图片

 

工作机制

以一个示例来说明整个 AT 分支的工作过程。

业务表:product

Field

Type

Key

id

bigint(20)

PRI

name

varchar(100)

 

since

varchar(100)

 

AT 分支事务的业务逻辑:

update product set name = GTS where name = TXC;
一阶段

过程:

  1. 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC‘)等相关的信息。
  2. 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = TXC;

得到前镜像:

id

name

since

1

TXC

2014

 

  1. 执行业务 SQL:更新这条记录的 name 为 ‘GTS‘。
  2. 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, since from product where id = 1;

得到后镜像:

id

name

since

1

GTS

2014

  1. 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
{
    "branchId": 641789253,
    "undoItems": [{
        "afterImage": {
            "rows": [{
                "fields": [{
                    "name": "id",
                    "type": 4,
                    "value": 1
                }, {
                    "name": "name",
                    "type": 12,
                    "value": "GTS"
                }, {
                    "name": "since",
                    "type": 12,
                    "value": "2014"
                }]
            }],
            "tableName": "product"
        },
        "beforeImage": {
            "rows": [{
                "fields": [{
                    "name": "id",
                    "type": 4,
                    "value": 1
                }, {
                    "name": "name",
                    "type": 12,
                    "value": "TXC"
                }, {
                    "name": "since",
                    "type": 12,
                    "value": "2014"
                }]
            }],
            "tableName": "product"
        },
        "sqlType": "UPDATE"
    }],
    "xid": "xid:xxx"
}
  1. 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
  2. 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
  3. 将本地事务提交的结果上报给 TC。
二阶段-回滚
  1. 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
  2. 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
  3. 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
  4. 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = TXC where id = 1;
  1. 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
二阶段-提交
  1. 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
  2. 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

事务使用

我们只需要使用一个 @GlobalTransactional 注解在业务方法上:

 @GlobalTransactional
    public void purchase(String userId, String commodityCode, int orderCount) {
        ......
    }

源码分析

相关配置

SeataAutoConfiguration

Spring自动配置类中配置了全局事务扫描器GlobalTransactionScanner。

技术图片

 

GlobalTransactionScanner

GlobalTransactionScanner内容如下。

技术图片

 

 

 

可以看到分别实现了Spring的3个接口InitializingBean,ApplicationContextAware,DisposableBean。

在afterPropertiesSet()中,调用了initClient方法:

技术图片

 

 

 

initClient方法里面对TmClient,RmClient进行了初始化(参数就是配置文件bean里配置的applicationId和txServiceGroup),并注册了一个Spring的ShutdownHook。

技术图片

 

 

 

TmClient.init()

TmClient的初始化方法。

技术图片

 

其最终调用到的是AbstractNettyRemotingClient的init()方法,启动了一个定时器不断进行重连操作。

技术图片

 

 

 

NettyClientChannelManager的reconnect方法内容如下:

技术图片

 

 

 

方法getAvailServerList内从注册中心获取服务器列表。

技术图片

 

 

 

RegistryFactory.getInstance().lookup(transactionServiceGroup)是针对不同注册中心做了适配的,默认看下File形式的实现。

技术图片

 

 

 

进到FileRegistryServiceImpl#lookup方法,这里结合File.conf配置来说明。

 技术图片

 

FileRegistryServiceImpl的服务器查找方法如下:

 技术图片

 

 

 

1、现根据事务分组(key=vgroup_mapping.事务分组名称)找到分组所属的server集群名称,这里是default

2、然后根据集群名称(key=集群名称.grouplist)找到server对应ip端口地址

梳理下TmClient的初始化流程:

  1. 启动定时执行器,每10秒尝试进行一次重连seata-server
  2. 重连时,先从file.conf中根据分组名称(service_group)找到集群名称(cluster_name)
  3. 再根据集群名称找到seata-serverr集群ip端口列表
  4. 从ip列表中选择一个用netty进行连接
RmClient.init()

RmClient的初始化方法。

技术图片

 

 

 

1、设置了资源管理器resourceManager

2、设置了消息回调监听器,rmHandler用于接收seata-server在二阶段发出的提交或者回滚请求

ResourceManager管理资源的注册和注销。

技术图片

 

RMHandlerAT在收到TC二阶段回滚消息时执行回滚。

技术图片

 

 

 

第一阶段

拦截器中开启事务

在需要加全局事务的方法中,会加上GlobalTransactional注解,注解往往对应着拦截器,Seata中拦截全局事务的拦截器是GlobalTransactionalInterceptor,看下其拦截方法。

技术图片

 

 

 

判断:

l  如果方法上有全局事务注解,调用handleGlobalTransaction开启全局事务

l  如果没有,按普通方法执行,避免性能下降

看下handleGlobalTransaction()方法:

技术图片

 

 

 

可以看到最终调用的是TransactionalTemplate的execute方法,execute方法如下:

技术图片

 

 

 

分为几步:

l  开启全局事务beginTransaction

l  执行业务方法

l  提交事务commitTransaction(若没抛异常)

l  执行completeTransactionAfterThrowing回滚操作(抛异常)

beginTransaction最终调用到了io.seata.tm.api.DefaultGlobalTransaction#begin(int, java.lang.String)方法,代码如下:

技术图片

 

 

 

  1. 调用transactionManager.begin()方法通过TmNettyRemotingClient与server通信并生成一个xid
  2. 将xid绑定到Root上下文中

看到这里,也就明确了一点,全局事务开启时,是由TM来发起的。

commitTransaction和rollbackTransaction方法类似,由TM发送事务commit或rollback信息给seata-server。

技术图片

 

SQL解析与undolog生成

由于Seata对数据源做了代理,所以sql解析与undolog入库操作是在数据源代理中执行的。数据源代理类创建器的配置。

技术图片

 

 

 

DataSourceProxy,ConnectionProxy,StatementProxy是Seata提供的代理封装类。

最终对Sql进行解析操作,发生在StatementProxy类中:

技术图片

 

 

 

然后交给了ExecuteTemplate执行,跟到ExecuteTemplate中查看:

技术图片

 

 

 

  1. 先判断是否开启了全局事务,如果没有,不走代理,不解析sql,避免性能下降
  2. 调用SQLVisitorFactory对目标sql进行解析
  3. 针对特定类型sql操作(INSERT,UPDATE,DELETE,SELECT_FOR_UPDATE)等进行特殊解析
  4. 执行sql并返回结果

关键点在于特定类型执行器中的execute方法,挑选InsertExecutor为例说明,其execute方法调用的是父类BaseTransactionalExecutor中的execute方法,看下源码。

技术图片

 

 

 

将ROOT上下文中的xid绑定到了connectionProxy中,并调用了doExecute方法,看下AbstractDMLBaseExecutor中的doExecute方法。

技术图片

 

 

 

查看代码,生成undolog在executeAutoCommitFalse方法中:

技术图片

 

 

 

executeAutoCommitTrue中先将autoCommit设置为false(因为要对sql进行解析,生成undolog在一个事务中入库,避免提前入库)。

再执行到executeAutoCommitFalse中,分为4步:

  1. 获取sql执行前镜像beforeImage
  2. 执行sql
  3. 获取sql执行后afterimage
  4. 根据beforeImage,afterImage生成undolog记录并添加到connectionProxy的上下文中

到此为止,红色框中几步已经完成。

技术图片

 

 

 

分支事务注册与事务提交

业务sql执行以及undolog执行完后会在ConnectionProxy中执行commit操作,

看下代码。

技术图片

 

 

 

1、如果处于全局事务中,则调用processGlobalTransactionCommit()处理全局事务提交

2、如果加了全局锁注解,加全局锁并提交

3、如果没有对应注释,按直接进行事务提交

主要看processGlobalTransactionCommit()方法,也是核心代码:

技术图片

 

 

 

流程分为如下几步:

  1. 注册分支事务register(),并将branchId分支id绑定到上下文中。
  2. UndoLogManager.flushUndoLogs(this) 如果包含undolog,则将之前绑定到上下文中的undolog进行入库。
  3. 提交本地事务。
  4. 如果操作失败,report()中通过RM提交第一阶段失败消息,如果成功,report()提交第一阶段成功消息。

技术图片

 

 

 

undolog入库和普通业务sql的执行用的一个connection,处于一个本地事务中,保证了业务数据变更时,一定会有对应undolog存在。

至此,第一阶段中undolog提交与本地事务提交,分支事务注册与汇报也已完成。

技术图片

 

 

 

第二阶段

在前面分析RmClient.init()方法时,提到了Seata会使用SPI拓展机制找到RmClient的回调处理器RMHandlerAT,该类是负责接送二阶段seata-server发给RmClient的提交、回滚消息,并作出提交,回滚操作。

RMHandlerAT继承自AbstractRMHandler,AbstractRMHandler中两个handle方法对应,事务提交、回滚操作。

技术图片

 

 

 

全局事务提交

全局事务提交对应了doBranchCommit(request, response)方法。

技术图片

 

 

 

调用的是getResourceManager(),上面提到SPI拓展提到的DataSourceManager类。

技术图片

 

 

 

DataSourceManager中调用了asyncWorker来异步提交,看下AsyncWorker中branchCommit方法。

技术图片

 

 

 

这边只是往一个ASYNC_COMMIT_BUFFER缓冲List中新增了一个二阶段提交的context。

但真正提交在哪呢?答案在AsyncWorker的init()方法中,其init()方法会在DataSourceManager中被调用,内部启动一个定时器不断进行全局事务提交操作。

技术图片

 

 

 

真正的分支事务提交就是在doBranchCommits中完成的,主要工作是删除回滚日志。

技术图片

 

主要分为几步:

  1. 先按resourceId(也就是数据连接)对提交操作进行分组,一个数据库的可以一起操作,提升效率
  2. 根据resourceId找到对应DataSourceProxy,并获取一个普通的数据库连接getPlainConnection(),估计这本身不需要做代理操作,故用了普通的数据库连接
  3. 调用UndoLogManager.deleteUndoLog(commitContext.xid, commitContext.branchId, conn)删除undolog

回过头来看下设计原理图:

技术图片

 

 

 

全局事务回滚

接下来我们看看全局事务回滚的方法,AbstractRMHandler#doBranchRollback 。

技术图片

 

 

 

该方法调用了DataSourceManager的branchRollback方法。

技术图片

 

 

 

最终回滚方法调用的是UndoLogManager.undo(dataSourceProxy, xid, branchId),大体是根据Undolog进行反解析并执行回滚操作。

技术图片

 

 

 

然后进行回滚日志的清理和提交。

技术图片

 

 

 

具体的Undolog反解析操作实现在AbstractUndoExecutor的子类中。

技术图片

 

 

 

再回头看下回滚设计原理图:

技术图片

 

 

 

Seata TCC模式

流程图解

TCC执行流程如下图所示:

技术图片

 

大致流程如下:

1.全局事务拦截器拦截到@GlobalTransational注解,调用TM开启全局事务

2.执行TCC参与者的prepare方法时,被TCC拦截器拦截,在prepare方法执

行前注册分支事务到TC,在prepare方法执行后向TC报告分支事务的状态

3.如果执行发生异常则TM通知TC回滚事务,否则TM通知TC执行提交事务

4.TC收到TM的提交或回滚通知,遍历各TCC分支事务,逐个进行提交或回滚

 

 

事务使用

跟AT模式一样,TCC模式也通过@GlobalTransactional注解开启全局事务,然后调用各个两阶段参与者的prepare方法即可。

技术图片

 

 

 

两阶段的参与者格式如下:

l  TwoPhaseBusinessAction注解标记这是个TCC接口,同时指定commitMethod,rollbackMethod的名称

l  BusinessActionContext是TCC事务中的上下文对象

l  BusinessActionContextParameter注解标记的参数会在上下文中传播,即能通过BusinessActionContext对象在commit方法及cancle方法中取到该参数值

技术图片

 

 

 

源码分析

TCC资源注册

GlobalTransactionScanner继承了AbstractAutoProxyCreator抽象类,并重新实现了wrapIfNecessary接口,该接口用来在spring启动时,生成代理类。

技术图片

 

 

 

看下重写的wrapIfNecessary方法。

技术图片

 

 

 

可以看到这段逻辑中,判断了bean如果是个TCC的接口实现,则将拦截器初始化为TccActionInterceptor,TccActionInterceptor是TCC方法的核心拦截器,后面会具体介绍,先跟到TCCBeanParserUtils.isTccAutoProxy()中看下源码。

技术图片

 

 

 

isTccAutoProxy()中又会调用DefaultRemotingParser#parserRemotingServiceInfo来进行TCC资源注册。

技术图片

 

 

 技术图片

 

 

 

可以看到,通过反射拿到了TwoPhaseBusinessAction注解中声明的Commit方法和Rollback方法并封装成TCCResource对象,最终调用ResourceManager的registerResource方法。

TCC模式下ResourceManger的实现为TCCResourceManager,AbstractRMHandler的实现为RMHandlerTCC。

跟到TCCResourceManager中查看registerResource方法。

技术图片

 

 

 

看到将TCCResource对象存储在本地Map中,方便后续通过ResourceId找到对应Resource来进行提交,回滚操作。super.registerResource代码如下,通过RmNettyRemotingClient发送rpc请求给Seata-server进行资源注册。

技术图片

至此,本地内存中会有个TCCResourceCache,注册完成后,seata-server端也会有个TCC的资源列表。

 

服务端接收RM注册信息的接口在DefaultServerMessageListenerImpl 的onRegRmMessage中,看下代码。

技术图片

 

最终调用了ChannelManager.registerRMChannel方法。

技术图片

 

 

 

服务端也会对RpcContext进行缓存,缓存Map嵌套层次较多,最外层key为resourceId,往内一次是applicationId,clientIIp,port。

至此,TCC资源管理器RM已完成注册,本地及服务端均有以resourceId为key的缓存Map。

开启TCC全局事务

TCC模式业务调用方和AT模式一样,需要使用GlobalTransactional注解来开启全局事务。

技术图片

 

 

 

业务方法执行时,最终会被AT模式源码分析中提到过的拦截器GlobalTransactionalInterceptor拦截,开启一个全局事务,获得全局事务id,即xid。

具体代码是TransactionalTemplate的execute方法,execute方法如下:

技术图片

 

 

 

分为一下几步:

1.开启全局事务beginTransaction(TM与TC通信并获得Xid)

服务端接收全局事务开启请求的方法在DefaultCore的begin方法中,可以看到创建了一个GlabalSession。

技术图片

2.执行业务方法

3.提交事务commitTransaction(TM与TC通信,发起事务提交请求)

4.如果发生异常,执行completeTransactionAfterThrowing回滚操作(TM与TC通信,发起事务回滚请求)

 

TCC拦截器-注册分支事务

TCC注册过程分析时,如果bean是个TCC的bean(即bean中方法包含TwoPhaseBusinessAction注解),会初始出TccActionInterceptor拦截器,其实现了MethodInterceptor,这也是TCC接口的方法级别核心拦截器。

看下源码中的invoke方法:

技术图片

 

 

 

方法调用了actionInterceptorHandler.proceed方法:

技术图片

 

接着看doTccActionLogStore方法:

技术图片

 

 

 技术图片

 

 

 

服务端接收分支注册的代码也在DefaultCore(见Seata源码)中,代码如下:

技术图片

至此,TCC分支事务注册完毕。

全局事务提交

TransactionalTemplate的execute方法中,若业务执行无异常,则会调用commitTransaction方法。

技术图片

 

 

 

最终调用的DefaultGlobalTransaction的commit方法。

技术图片

 

 

 

其中调用TM的commit方法,来通知TC对全局事务进行提交。

 

TC收到commit消息的处理在DefaultCore(见Seata源码)的commit方法中,查看代码:

技术图片

 

分为几步:

  1. 根据xid取出GlabalSession
  2. 关闭Session,防止再有分支注册尽量
  3. 修改状态为提交中
  4. 如果可以异步提交,则异步提交,否则同步提交

看下同步提交代码doGlobalCommit:

技术图片

 

 

 

遍历每个branchSession,对每个分支事务进行提交,失败会无限重试。

resourceManagerInbound.branchCommit方法会调用DefaultCoordinator中branchCommit方法来与TCC资源管理器通信,发送分支事务提交消息,这里sendSyncRequest方法中就会根据resourceId去找到第一步(TCC资源管理器注册)中RpcContext的缓存,并得到对应Channel来建立Netty通信。

技术图片

 

 

 

各TCC资源管理器接收到分支事务提交请求后,会调用TCCResourceManager的branchCommit方法实际对事务进行提交。

技术图片

 

 

 

自此客户端收到seata-server提交信息后,完成了对分支事务的提交。

总结一下全局事务提交的大致流程:

  1. 业务方调用微服务无异常,通过TM发起事务提交请求
  2. TC接收到事务提交请求后,通过Xid找到全局事务,再取出所有分支事务
  3. 遍历分支事务,发出分支事务提交请求
  4. TCC资源管理器RM接收到提交请求后,从本地TCCResource缓存中根据resourceId取出对应方法bean,反射调用commit方法

全局事务回滚

全局事务回滚思路与全局事务提交过程基本一致。

全局事务回滚的大致流程:

1.    业务方调用微服务发生异常,通过TM发起事务回滚请求

2.    TC接收到事务回滚请求后,通过Xid找到全局事务,再取出所有分支事务

3.    遍历分支事务,发出分支事务回滚请求

4.    TCC资源管理器RM接收到回滚请求后,从本地TCCResource缓存中根据resourceId取出对应方法bean,反射调用rollback方法

 

到此,我们完成了对Seata框架AT模式和TCC模式完整执行流程的分析。

 


作者:朝雨忆轻尘
出处:https://www.cnblogs.com/xifengxiaoma/ 
版权所有,欢迎转载,转载请注明原文作者及出处。



以上是关于深度剖析 Seata TCC 模式图解 + 源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Seata TCC模式原理与实战

Seata基础使用-分布式事务

分布式事务--Seata

实战!阿里神器 Seata 实现 TCC模式 解决分布式事务,真香!

SpringCloud Alibaba Seata TCC 模式讲解与使用

阿里分布式中间件Seata从入门到精通