本文主要介绍 seata-go 中 TCC 的设计思路、异常处理以及在实战中的使用。
Seata 是一款开源的分布式事务解决方案,致力于为现代化微服务架构下的分布式事务提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 等多种事务模式,帮助用户解决不同场景下的业务问题。同时,Seata 还支持多语言编程,并且提供了简易的 API 接口、丰富的文档以及快速上手的 samples 示例项目,也能快速帮助开发者入门并上手 Seata 的使用。
Seata-go 是 Seata 多语言生态中 golang 语言的实现方案,它致力于帮助 golang 开发者也能使用 Seata 的能力来解决分布式事务场景的问题。
Seata-go 复用了 Seata TC 的能力,client 的功能和 Seata 保持一致。目前 Seata-go 已经支持了 TCC 和 AT 模式,XA 模式正在测试中,预计会在 5 月份发版。Saga 模式正在设计和规划中,后面也会和 Seata 的 Saga 功能保持一致。
本文主要从以下几个角度,介绍 Seata-go 中的 TCC 模式的设计与使用:
-
Seata-go TCC 实现原理
-
Sata-go TCC 异常处理
-
Seata-go 的展望
Seata-go 采用了 getty 做 TCP 网络通信,完全实现了 Seata 的通信协议。下层实现了配置中心和注册中心,也支持了很多的第三方框架的接入,比如 dubbo、grpc、gorm 等等,目前也正在积极和各个社区沟通,以支持更多框架的接入。Seata-go 简易的系统架构图如下:
先来简单回顾下 TCC 模式的含义。TCC 是分布式事务方案的一种实现,它采用了二阶段提交协议,TCC 的全称是 Try-Confirm-Cancel,Try 是预留资源操作,Confirm 是提交操作,Cancel 是回滚操作。在 TCC 的一阶段中,先触发所有的子事务执行 Try 操作,如果所有的子事务的一阶段都执行成功,那么会触发所有子事务二阶段执行 Confirm 操作,否则二阶段执行 Cancel 操作,以此来保证各个子事务状态的一致性。
TCC 是一种侵入式的分布式事务方案,Try、Confirm 和 Cancel 三个阶段的逻辑,都需要用户自己去实现。这样做意味着更多的代码量,以及对业务很大的入侵性;而优点是则比较灵活,能由用户随意发挥以解决更复杂的分布式事务场景的问题。
在介绍 Seata-go 的 TCC 模式之前,先来回顾下
Seata 中的三个核心角色,即 TC、TM 和 RM
。TC 是事务协调者,负责维护全局事务的状态,以及触发分支事务的提交和回滚动作;TM 是事务管理器,负责子事务的编排,以及全局事务的提交和回滚动作;RM 是资源管理器,管理分支事务处理的资源,比如 MySQL 数据库的操作等。
了解了这三个核心角色,就可以大致的理解下 TCC 的事务流程,大致分为以下几个步骤:
-
TM 向 TC 发送请求,开启全局事务,TC 侧记录下全局事务的状态信息;
-
TM 分别向所有的 RM 发送请求,RM 会向 TC 注册分支事务,然后执行 Try 阶段的逻辑;
-
如果当中某个 RM 给 TM 返回 Try 阶段执行失败,那 TM 就向 TC 发送“回滚全局事务” 的请求。TC 收到后,就会向所有已执行 Try 的 RM 发送 Rollback 指令,触发 RM 执行 Cancel 逻辑;
-
如果所有的 RM 都给 TM 返回 Try 阶段执行成功,那 TM 就向 TC 发送“提交全局事务” 的请求。TC 收到后,就会向所有已执行 Try 的 RM 发送 Commit 指令,触发 RM 执行 Commit 逻辑。
至此,一个完整的分布式事务就执行完了,以下是这个过程的流程图:
在 Seata-go 中,为了方便用户使用,提供了两种定义 TCC 服务方法,一种是实现 TwoPhaseInterface 接口,具体如下:
另一种是通过 tag 的方式来定义 TCC 服务,这种方式会相对复杂点,但是也更加的灵活:
第二种 tag 的方案,主要是为了满足一些特殊的场景,比如说,dubbo-go 的 server 和 client 是使用 tag 的方式来定义的,这个时候就需要使用 tag 的方式来定义 TCC 的服务。
一般情况推荐使用第一种继承接口的方式来做,比较简单。
这里截图给大家看个例子,更详细的 samples 请参考 seata-go-samples 项目,地址为:
https://github.com/seata/seata-go-samples
在实际使用 TCC 的时候,由于网络或是业务代码逻辑执行时间等因素,可能会出现以下的问题:
在 Seata-go 中,提供了两种解决方案,来帮助用户解决这个问题。
第一种方式的原理和 Seata Java 的处理逻辑是一样的,都是借助 tcc_fence_log 事务状态表来做的:
用户需要在自己的业务数据库中,创建这个表,RM 在提交业务 SQL 的时候,同时会在这个表里面插入一条记录,这俩 SQL 是在一个本地事务中完成的。由于这个表中,“全局事务ID+分支事务ID”是一个联合主键,导致重复执行时会失败,这样就解决了 Try 阶段的幂等问题。在 Commit 和 Cancel 阶段时,会先查询这个表中分支事务的状态,然后才进行实际的逻辑,最后再更新状态。这样也能保证 Commit 和 Cancel 阶段的幂等性。
再来看看 Seata-go 是如何解决事务悬挂和空回滚的问题。假如一个 Rollbback 请求过来,RM 去查询 tcc_fence_log 表,发现没有记录(因为 RM 尚未收到 Try 请求),此时会往 tcc_fence_log 表插入一条记录,并标记状态为 suspend,然后直接退出,而不会去执行 Rollback 的逻辑,这样就避免了空回滚的问题。如果 RM 后面再收到 Try 请求,由于 tcc_fence_log 表已经有一条记录,就会导致事务 SQL 无法提交而失败(tcc_fence_log 会出现主键冲突的问题),这样就避免了防悬挂的问题。