作者:刘月财
本文主要介绍 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 TCC 实现原理
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 的服务。一般情况推荐使用第一种继承接口的方式来做,比较简单。
在实际使用的时候,用户只需要做以下几件事情即可:
- 定义好自己的 TCC 服务,可以参考上面介绍的这两种方式之一都可以;
- 调用 TCC 的代理方法 NewTCCServiceProxy ,将 TCC 服务的封装成代理;
- 编排好自己的子事务,传入到分布式事务的入口方法 WithGlobalTx 方法即可。
这里截图给大家看个例子,更详细的 samples 请参考 seata-go-samples 项目,地址为:https://github.com/seata/seata-go-samples
Seata-go TCC 异常处理
在实际使用 TCC 的时候,由于网络或是业务代码逻辑执行时间等因素,可能会出现以下的问题:
- 幂等: 在事务的一、二阶段,由于网络延迟或是其他原因,RM 没有及时给 TC 或 TM 响应,导致 RM 被重复触发执行一、二阶段的逻辑,这个时候,需要考虑业务的幂等;
- 空回滚: 由于网络延迟或是其他原因,RM 在未收到 Try 请求的情况下,却收到了 Rollback 请求,造成空回滚的问题;
- 悬挂: 由于网络延迟或是其他原因,RM 在未收到 Try 请求的情况下,收到了 Rollback 请求,处理完 Rollback 请求后,又收到了 Try 请求。这时全局事务已结束,会导致事务预留的资源一直无法释放。
在 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 会出现主键冲突的问题),这样就避免了防悬挂的问题。
要实现这种方式,需要使用 Seata-go 提供的代理数据源,这些操作都会由代理数据源来完成,用户只需要开启开关,关注自己的业务 SQL 即可,这个功能已经实现,会在后续进行发版。
第二种方式,是通过用户手动的方式来实现的。原理和上面类似,但是 tcc_fence_log 的操作逻辑需要由用户自己实现,下面的截图描述了大致的使用方式,详情可以参考这个 samples 代码:
https://github.com/seata/seata-go-samples/tree/main/tcc/fence
Seata-go 展望
Seata-go 社区近期与不少国内 go 语言微服务框架以及 ORM 框架背后的开发社区达成合作,比如 GORM 框架,已经集成到了 Sample 中,后续会将更多的 ORM 框架集成在 Seata-go-Samples 项目中。与 MOSN 社区的合作也在推进中,可实现真正的基于 Seata 的 Transaction Mesh。
Seata-go 的 XA 模式会在5月份进行发版,届时 Seata-go 将支持 TCC、XA 和 AT 三种事务模式。Seata-go 后续的中心将会在 Saga 模式功能的开发上。
当前的 Saga 模式仅实现了服务编排的正向推进与反向 Rollback 能力,更进一步的服务编排则可以实现 DAG、定时任务、任务批量调度,覆盖工作流的所有流程,提升用户在 Seata 这个平台上的使用体验。目前 Seata-go 依赖于 Seata Java 的 TC,按照这个工作计划,可能需要在未来的 Seata-go 版本中实现一个功能更强大的 TC 调度。
Seata-go 社区目前正在快速生长中,希望有更多对开源感兴趣的小伙伴,加入到我们社区来,一起助力 Seata-go 的成长!