跳到主要内容

· 阅读需 20 分钟

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在今年的开源之夏活动中,我加入了 Apache Seata (Incubator) 社区,完成了开源之夏的课题,并从此一直积极参与社区。我有幸在云栖大会-开发者秀场上分享了我的开发者经验。在本文中,我将与大家分享我在 Seata 社区中的开发者之旅,以及在这个旅程中积累的经验和见解。希望通过我的故事,能够激励更多人踏上这充满挑战和激励的开源之路,为开源社区的繁荣做出自己的贡献。

相关背景

在正式介绍我的经历之前,我想先提供一些相关的背景信息,以解释为什么我要参与开源以及如何参与开源。关于参与开源的原因,我相信每个人都有不同的动机。以下是我认为一些主要的原因:

  • 学习:参与开源使我们有机会为不同组织开发的开源项目做出贡献,与行业专家互动,提供了学习的机会。
  • 技能提升:以我为例,我通常使用 Java 和 Python 进行后端开发。但在参与Seata项目时,我有机会学习Go语言,拓宽了我的后端技术栈。此外作为学生,我很难接触到生产级框架或应用,而开源社区为我提供了这个机会。
  • 兴趣:我身边的朋友都是热衷于开源的,他们享受编程,对开源充满热情。
  • 求职:参与开源可以丰富我们的作品集,为简历增加分量。
  • 工作需求:有时参与开源是为了解决工作中遇到的问题或满足工作需求。

这些都是参与开源的原因,对我来说,学习、技能提升和兴趣是我参与开源的主要动机。无论你是在校学生还是在职人员,如果你有参与开源的意愿,不要犹豫,任何人都可以为开源项目做出贡献。年龄、性别、工作和所在地都不重要,关键是你的热情和对开源项目的好奇心。

我参与开源的契机是参加了中科院软件所举办的开源之夏活动。

开源之夏是一个面向高校开发者的开源活动,社区发布开源项目,学生开发者在导师的指导下完成项目的开发,结项成果贡献给社区,合入社区仓库,获得项目奖金和证书。开源之夏是踏入开源社区的一个绝佳契机,也是我第一次比较正式地接触开源项目,而这个经历为我打开了一扇全新的大门。自此我深刻地认识到参与开源项目的建设,分享自己的技术成果,让更多的开发者能够使用你所贡献的东西,是一件极富乐趣和意义的事情。

下面我分享的这张图片是开源之夏官方公开的数据,从 2020 年开始参与的社区数量还有学生数量都在逐年增加,活动也是越办越好。可以看到今年的参与的社区项目共有 133 个,每个社区又提供了若干个课题,而每位学生只能选择一个课题。想要在这么多个社区中找到想要参与的社区和适合自己的课题是一个相对复杂的任务。

img

综合考虑社区的活跃程度、技术栈契合度、新人引导情况等,最终我选择加入 Seata 社区。

Seata 是一款开源的分布式事务框架,提供了完整的分布式事务解决方案,包括 AT、TCC、Saga 和 XA 事务模式,可支持多种编程语言和数据存储方案。从 19 年开源起到今年已经走过了 5 个年头,社区中有超过 300 多位贡献者,项目收获了 24k+ 星标,是一个非常成熟的社区。同时 Seata 兼容 10 余种主流 RPC 框架和 RDBMS,与 20 多个社区存在集成和被集成的关系,被几千家客户应用到业务系统中,可以说是分布式事务解决方案的事实标准。

img

2023 年 10 月 29 日,Seata 正式捐赠给了 Apache 软件基金会,成为孵化项目。经过孵化之后,Seata将有望成为首个 Apache 软件基金会的分布式事务框架顶级项目。这次捐赠也将推动 Seata 更广泛地发展,对生态系统的建设产生深远的影响,从而使更多的开发者受益。这个重要的里程碑也为 Seata 带来更广阔的发展空间。

开发之旅

介绍完了一些基本情况,后文中我将分享我在 Seata 社区的开发之旅。

在正式开始开发之前,我进行了许多准备工作。因为 Seata 已经经历了五年的发展,积累了数十万行代码,因此直接参与开发需要一定的上手成本。我分享了一些准备经验,希望能够为大家提供一些启发。

  1. 文档和博客是第一手材料 文档和博客这类的文本材料可以帮助社区新人迅速了解项目背景和代码结构。 首先,官方文档是最主要的参考资料,从这里可以了解到一切官方认为你需要了解的东西。 img 博客,仅次于官方文档的材料,一般是开发者或者是深度用户编写的,和文档不同的点在于博客可能会更深入到某个专项上去介绍,比如一些项目的理论模型、项目结构、某个模块的源码分析等等。 img 公众号,和博客类似,一般是偏技术性的文章,公众号还有个优点是可以订阅推送,利用碎片时间阅读一些技术。 img 此外,开源社区的一些在线分享或线下 Meetup 公开的幻灯片也是非常有意义的文本资料。 img 除了官方资料之外,还有许多第三方资料可供学习,比如可以通过用户分享的 use cases 了解项目的具体实施和实践;通过第三方社区的集成文档了解项目的生态;还有就是通过第三方的视频教程来学习。但在所有这些资料中,我认为官方文档和博客是最有帮助的。
  2. 熟悉使用框架 当然刚才说的这些文本资料肯定不需要面面俱到的看完,纸上得来终觉浅,看到感觉差不多明白了就可以去实践了。可以按照官方文档的"Get Started"章节逐步了解项目的基本流程。另一种方法是查找官方提供的示例或演示,构建并运行它们,理解代码和配置的含义,并通过使用项目了解项目的需求、目标以及现有功能和架构。 例如,Seata有一个名为 seata-samples 的仓库,其中包含20多种用例,比如 Seata 和 Dubbo 集成,和 SCA, Nacos 集成的案例,基本可以覆盖到支持的所有场景。
  3. 粗略阅读源代码把握主要逻辑 在准备阶段,粗略地阅读源代码以把握项目的主要逻辑也很重要。了解如何高效地把握项目的主要内容是一个需要长期积累的技能。首先,通过前述的准备步骤,了解项目的概念、交互和流程模型是很有帮助的。 以Seata为例,通过官方文档和实际操作,可以了解Seata事务领域的三个角色:TC(Transaction Coordinator)、TM(Transaction Manager)和 RM(Resource Manager)。TC 作为独立部署的 Server 用于维护全局和分支事务的状态,是 Seata 实现高可用的关键;TM 用于与 TC 交互,定义全局事务的开始、提交或回滚;RM 用于管理分支事务处理的资源,与 TC 交互以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。粗略地了解这些角色之间的交互后,可以更轻松地把握项目的主要逻辑。 img 脑海里刻下了这些模型的印象,对源码的主干提取就相对得心应手了一些。比如 Seata TC 事务协调者,作为 Server 端,是一个独立于业务部署的单独应用。那为了分析源码,就可以直接在本地把 server 起起来,通过启动类开始追踪。可以分析到一些初始化的逻辑比如服务注册、全局锁的初始化等等。还有可以通过 RPC 的调用来追踪到交互逻辑的代码,比如 TC 是如何对全局事务和分支事务进行持久化,如何驱动全局事务提交或者回滚的。 然而内嵌客户端的框架代码,没有一个启动类入口可以入手分析。那其实可以从一个 sample 入手,找到其对框架代码的引用从而进行阅读。比如 Seata 一个很重要的注解是 GlobalTransaction,用于标识一个全局事务。想要知道 TM 是如何对这个注解分析的,那我们通过 IDE 的搜索功能,找到 GlobalTransaction 的拦截器即可分析其中的逻辑。 还有一个小 tips 分享给大家,往往来说单测注重于单一模块的职能,可以通过阅读单测可以了解一个模块的输入输出、逻辑边界,也可以顺着单测的调用链去阅读代码,也是理解源码一个很重要的手段。

万事俱备只欠东风,做完充足的准备,下一步就是区积极参与到社区之中。

参与的方式也有很多种,最常见的参与方式是查看项目的 Issues 列表,社区通常会为新贡献者标记一些带有特殊标签的 Issue,如“good-first-issue”、“contributions-welcome”和“help-wanted”等。可以通过这些标签筛选感兴趣的任务。

img

除了 Issues,GitHub 还提供了讨论的功能,可以参与一些公开的讨论并获取新的想法。

img

此外,社区通常会定期举行会议,比如周会或双周会,可以通过参加这些会议来了解社区的最新进展,提出问题以及与其他社区成员交流。

总结与心得

我加入 Seata 社区最初是通过开源之夏活动。我完成了我的课题,为 Seata Saga 实现了一些新的功能,也做了一系列的优化。但我不止于此,因为在 Seata 的开源经历中我获得了学生生涯中最宝贵的一次开发者体验,在之后的时间我也持续通过上述参与方式持续活跃在社区中。这主要得益于以下几个方面:

  1. 沟通与社交:导师制度为我提供了重要的支持。在开发过程中,我与我的导师亦夏之间的密切合作对我适应社区文化和工作流程起到了关键作用。他不仅帮助我适应了社区,还为我提供了程序设计的思路,也与我分享了一些在工作中的经验和见解,这些都对我的发展非常有帮助。此外,Seata 社区创始人清铭也提供了很多帮助,包括建立了与其他同学的联系,帮助我进行 Code Review,也为我提供了许多机会。
  2. 正反馈:在 Seata 的开发过程中,我经历了一个良性的循环。许多细节为我提供了许多正反馈,例如我的贡献能被用户广泛使用和受益,比如开发得到了社区的认可。这些正反馈加强了我继续在 Seata 社区贡献的意愿。
  3. 技能提升:再就是参与 Seata 开发,对我能力的提升也是巨大的。在这里,我能学习到生产级别的代码,包括性能优化,接口设计,边界判断的技巧。可以直接参与一个开源项目的运作,包括项目计划,安排,沟通等。当然还了解一个分布式事务框架是如何设计并实现的。

除了这些宝贵的开发者体验,我也从这次经历中体悟到了一些关于参与开源的个人心得,为激励其他有兴趣参与开源社区的同学,我做了简单的总结:

  1. 了解和学习社区文化和价值观:每个开源社区都有不同的文化和价值观。了解社区的文化和价值观对于成功参与社区至关重要。观察和了解社区其他成员的日常开发和交流方式是学习社区文化的好方法。在社区中要尊重他人的意见和包容不同的观点。
  2. 敢于迈出第一步:不要害怕面对困难,迈出第一步是参与开源社区的关键。可以通过领取标有"good-first-issue"等标签的 Issue,编写文档、单元测试等方式来开始。重要的是要克服畏难情绪,积极尝试并学习。
  3. 对自己的工作要充满信心:不要怀疑自己的能力。每个人都是从零开始的,没有人天生就是专家。参与开源社区是一个学习和成长的过程,需要不断的实践和积累经验。
  4. 积极参与讨论,持续学习不同技术:不要害怕提出问题,无论是关于项目的具体技术还是开发过程中的挑战。同时也不要局限于一个领域。尝试学习和掌握不同编程语言、框架和工具,这可以拓宽技术视野,为项目提供有价值的洞见。

通过我的开源之旅,我积累了宝贵的经验和技能,这些不仅帮助我成长为一个更有价值的开发者,也让我深刻地了解了开源社区的力量。然而,我不仅仅是个别的参与者,我代表着 Seata 社区的一部分。Seata 作为一个正在不断成长和演变的开源项目,有着巨大的潜力,同时也面临着新的挑战。因此我要强调 Seata 社区的重要性和未来的潜力,它已经进入 Apache 软件基金会的孵化阶段,这个重要的里程碑将为 Seata 带来更广阔的发展空间。Seata 欢迎更多的开发者和贡献者的加入,让我们共同推动这个开源项目的发展,为分布式事务领域的进步贡献一份力量。

· 阅读需 24 分钟

Seata 是一款开源的分布式事务解决方案,star高达24000+,社区活跃度极高,致力于在微服务架构下提供高性能和简单易用的分布式事务服务.

目前Seata的分布式事务数据存储模式有file,db,redis,而本篇文章将Seata-Server Raft模式的架构,部署使用,压测对比,及为什么Seata需要Raft,并领略从调研对比,设计,到具体实现,再到知识沉淀的过程.

分享人:陈健斌(funkye) github id: funky-eyes

2. 架构介绍

2.1 Raft 模式是什么?

首先需要明白什么是raft分布式一致性算法,这里直接摘抄sofa-jraft官网的相关介绍:

RAFT 是一种新型易于理解的分布式一致性复制协议,由斯坦福大学的 Diego Ongaro 和 John Ousterhout 提出,作为 RAMCloud 项目中的中心协调组件。Raft 是一种 Leader-Based 的 Multi-Paxos 变种,相比 Paxos、Zab、View Stamped Replication 等协议提供了更完整更清晰的协议描述,并提供了清晰的节点增删描述。 Raft 作为复制状态机,是分布式系统中最核心最基础的组件,提供命令在多个节点之间有序复制和执行,当多个节点初始状态一致的时候,保证节点之间状态一致。

简而言之Seata的Raft模式就是基于Sofa-Jraft组件实现可保证Seata-Server自身的数据一致性和服务高可用.

2.2 为什么需要raft模式

看完上述的Seata-Raft模式是什么的定义后,是否就有疑问,难道现在Seata-Server就无法保证一致性和高可用了吗?那么下面从一致性和高可用来看看目前Seata-Server是如何做的.

2.2.1 现有存储模式

在当前的 Seata 设计中,Server 端的作用是保证事务的二阶段被正确执行。然而,这取决于事务记录的正确存储。为确保事务记录不丢失,需要在保持状态正确的前提下,驱动所有的 Seata-RM 执行正确的二阶段行为。那么,Seata 目前是如何存储事务状态和记录的呢?

首先介绍一下 Seata 支持的三种事务存储模式:file、db 和 redis。根据一致性的排名,db 模式下的事务记录可以得到最好的保证,其次是 file 模式的异步刷盘,最后是 redis 模式下的 aof 和 rdb

顾名思义:

  • file 模式是 Seata 自实现的事务存储方式,它以顺序写的形式将事务信息存储到本地磁盘上。为了兼顾性能,默认采用异步方式,并将事务信息存储在内存中,确保内存和磁盘上的数据一致性。当 Seata-Server(TC)意外宕机时,在重新启动时会从磁盘读取事务信息并恢复到内存中,以便继续运行事务上下文。
  • db 是 Seata 的抽象事务存储管理器(AbstractTransactionStoreManager)的另一种实现方式。它依赖于数据库,如 PostgreSQL、MySQL、Oracle 等,在数据库中进行事务信息的增删改查操作。一致性由数据库的本地事务保证,数据也由数据库负责持久化到磁盘。
  • redis 和 db 类似,也是一种事务存储方式。它利用 Jedis 和 Lua 脚本来进行事务的增删改查操作,部分操作(如竞争锁)在 Seata 2.x 版本中全部采用了 Lua 脚本。数据的存储与 db 类似,依赖于存储方(Redis)来保证数据的一致性。与 db 类似,redis 在 Seata 中采用了计算和存储分离的架构设计.

2.2.2 高可用

高可用简单理解就是集群能够在主节点宕机后继续正常运行,常见的方式是通过部署多个提供相同服务的节点,并通过注册中心实时感知主节点的上下线情况,以便及时切换到可用的节点。

看起来似乎只需要加几台机器进行部署,但实际上背后存在一个问题,即如何确保多个节点像一个整体一样运作。如果其中一个节点宕机,另一个节点能够完美接替宕机节点的工作,包括处理宕机节点的数据。解决这个问题的答案其实很简单,在计算与存储分离的架构下,只需将数据存储在共享的存储中间件中,任何一个节点都可以通过访问该公共存储区域获取所有节点操作的事务信息,从而实现高可用的能力。

然而,前提条件是计算与存储必须分离。为什么计算与存储一体化设计不可行呢?这就要说到 File 模式的实现了。如之前描述的,File 模式将数据存储在本地磁盘和节点内存中,数据写操作没有任何同步,这意味着目前的 File 模式无法实现高可用,仅支持单机部署。作为初级的快速入门和简单使用而言,File 模式适用性较低,高性能的基于内存的 File 模式也基本上不再被生产环境使用。

2.3 Seata-Raft是如何设计的呢?

2.3.1 设计原理

Seata-Raft模式的设计思路是通过封装无法高可用的file模式,利用Raft算法实现多个TC之间数据的同步。该模式保证了使用file模式时多个TC的数据一致性,同时将异步刷盘操作改为使用Raft日志和快照进行数据恢复。 流程图

在Seata-Raft模式中,client端在启动时会从配置中心获取当前client的事务分组(例如default)以及相关Raft集群节点的IP地址。通过向Seata-Server的控制端口发送请求,client可以获取到default分组对应的Raft集群的元数据,包括leader、follower和learner成员节点。然后,client会监视(watch)非leader节点的任意成员节点。

假设TM开始一个事务,并且本地的metadata中的leader节点指向了TC1的地址,那么TM只会与TC1进行交互。当TC1添加一个全局事务信息时,通过Raft协议,即图中标注为步骤1的日志发送,TC1会将日志发送给其他节点,步骤2是follower节点响应日志接收情况。当超过半数的节点(如TC2)接受并响应成功时,TC1上的状态机(FSM)将执行添加全局事务的动作。

watch watch2

如果TC1宕机或发生重选举,会发生什么呢?由于首次启动时已经获取到了元数据,client会执行watch follower节点的接口来更新本地的metadata信息。因此,后续的事务请求将发送到新的leader(例如TC2)。同时,TC1的数据已经被同步到了TC2和TC3,因此数据一致性不会受到影响。只在选举发生的瞬间,如果某个事务正好发送给了旧的leader,该事务会被主动回滚,以确保数据的正确性。

需要注意的是,在该模式下,如果事务处于决议发送请求或一阶段流程还未走完的时刻,并且恰好在选举时发生,这些事务会被主动回滚。因为RPC节点已经宕机或发生了重选举,当前没有实现RPC重试。TM侧默认有5次重试机制,但由于选举需要大约1s-2s的时间,这些处于begin状态的事务可能无法成功决议,因此会优先回滚,释放锁,以避免影响其他业务的正确性。

2.3.2 故障恢复

在Seata中,当TC发生故障时,数据恢复的过程如下:

故障恢复

如上图所示

  • 检查是否存在最新的数据快照:首先,系统会检查是否存在最新的数据快照文件。数据快照是基于内存的数据状态的一次全量拷贝,如果有最新的数据快照,则系统将直接加载该快照到内存中。

  • 根据快照后的Raft日志进行回放:如果存在最新的快照或者没有快照文件,系统将根据之前记录的Raft日志进行数据回放。每个Seata-Server中的请求最终会经过ServerOnRequestProcessor进行处理,然后转移到具体的协调者类(DefaultCoordinator或RaftCoordinator)中,再转向具体的业务代码(DefaultCore)进行相应的事务处理(如begin、commit、rollback等)。

  • 当日志回放完成后,便会由leader发起日志的同步,并继续执行相关事务的增删改动作。

通过以上步骤,Seata能够实现在故障发生后的数据恢复。首先尝试加载最新的快照,如果有的话可以减少回放的时间;然后根据Raft日志进行回放,保证数据操作的一致性;最后通过日志同步机制,确保数据在多节点之间的一致性。

2.3.3 业务处理同步过程

流程 对于client侧获取最新metadata时恰好有业务线程在执行begin、commit或registry等操作的情况,Seata采取了以下处理方式:

  • client侧:

    • 如果客户端正在执行begin、commit或registry等操作,并且此时需要获取最新metadata,由于此时的leader可能已经不存在或不是当前leader,因此客户端的RPC请求可能会失败。
    • 如果请求失败,客户端会收到异常响应,此时客户端需要根据请求的结果进行回滚操作。
  • TC侧对旧leader的检测:

    • 在TC侧,如果此时客户端的请求到达旧的leader节点,TC会进行当前是否是leader的检测,如果不是leader,则会拒绝该请求。
    • 如果是leader但在中途失败,比如在提交任务到状态机的过程中失败,由于当前已经不是leader,创建任务(createTask)的动作会失败。这样,客户端也会接收到响应异常。
    • 旧leader的提交任务也会失败,确保了事务信息的一致性。 通过上述处理方式,当客户端获取最新metadata时恰好遇到业务操作的情况,Seata能够保证数据的一致性和事务的正确性。如果客户端的RPC请求失败,将触发回滚操作;而在TC侧,对旧leader的检测和任务提交的失败可以防止事务信息不一致的问题。这样,客户端的数据也能保持一致性。

3.使用部署

在使用和部署上,社区秉持着最小侵入,最小改动的原则,所以整体的部署上手应该是非常简单的,接下来分开client与server两端的部署改动点进行介绍

3.1 client

首先,使用注册配置中心较多的同学应该知道Seata的配置项中有一个seata.registry.type的配置项,支持了nacos,zk,etcd,redis等等,而在2.0以后增加了一个raft的配置项

   registry:
type: raft
raft:
server-addr: 192.168.0.111:7091, 192.168.0.112:7091, 192.168.0.113:7091

registry.type 改为raft,并配置raft相关元数据的获取地址,该地址统一为seata-server的ip+http端口 然后必不可少的就是传统的事务分组的配置

seata:
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default

如现在使用的事务分组为default_tx_group,那么对应的seata集群/分组就是default,这个是有对应关系的,后续再server部署环节上会介绍 至此client的改动已经完成了

3.2 server

对于server的改动可能会多一些,要熟悉一些调优参数和配置,当然也可以选择默认值不做任何修改

seata:
server:
raft:
group: default #此值代表该raft集群的group,client的事务分组对应的值要与之对应
server-addr: 192.168.0.111:9091,192.168.0.112:9091,192.168.0.113:9091 # 3台节点的ip和端口,端口为该节点的netty端口+1000,默认netty端口为8091
snapshot-interval: 600 # 600秒做一次数据的快照,以便raftlog的快速滚动,但是每次做快照如果内存中事务数据过多会导致每600秒产生一次业务rt的抖动,但是对于故障恢复比较友好,重启节点较快,可以调整为30分钟,1小时都行,具体按业务来,可以自行压测看看是否有抖动,在rt抖动和故障恢复中自行找个平衡点
apply-batch: 32 # 最多批量32次动作做一次提交raftlog
max-append-bufferSize: 262144 #日志存储缓冲区最大大小,默认256K
max-replicator-inflight-msgs: 256 #在启用 pipeline 请求情况下,最大 in-flight 请求数,默认256
disruptor-buffer-size: 16384 #内部 disruptor buffer 大小,如果是写入吞吐量较高场景,需要适当调高该值,默认 16384
election-timeout-ms: 1000 #超过多久没有leader的心跳开始重选举
reporter-enabled: false # raft自身的监控是否开启
reporter-initial-delay: 60 # 监控的区间间隔
serialization: jackson # 序列化方式,不要改动
compressor: none # raftlog的压缩方式,如gzip,zstd等
sync: true # raft日志的刷盘方式,默认是同步刷盘
config:
# support: nacos, consul, apollo, zk, etcd3
type: file # 该配置可以选择不同的配置中心
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: file # raft模式下不允许使用非file的其他注册中心
store:
# support: file 、 db 、 redis 、 raft
mode: raft # 使用raft存储模式
file:
dir: sessionStore # 该路径为raftlog及事务相关日志的存储位置,默认是相对路径,最好设置一个固定的位置

在3个或者大于3个节点的seata-server中配置完以上参数后,直接启动便可以看到类似以下的日志输出,就代表集群已经正常启动了

2023-10-13 17:20:06.392  WARN --- [Rpc-netty-server-worker-10-thread-1] [com.alipay.sofa.jraft.rpc.impl.BoltRaftRpcFactory] [ensurePipeline] []: JRaft SET bolt.rpc.dispatch-msg-list-in-default-executor to be false for replicator pipeline optimistic.
2023-10-13 17:20:06.439 INFO --- [default/PeerPair[10.58.16.231:9091 -> 10.58.12.217:9091]-AppendEntriesThread0] [com.alipay.sofa.jraft.storage.impl.LocalRaftMetaStorage] [save] []: Save raft meta, path=sessionStore/raft/9091/default/raft_meta, term=4, votedFor=0.0.0.0:0, cost time=25 ms
2023-10-13 17:20:06.441 WARN --- [default/PeerPair[10.58.16.231:9091 -> 10.58.12.217:9091]-AppendEntriesThread0] [com.alipay.sofa.jraft.core.NodeImpl] [handleAppendEntriesRequest] []: Node <default/10.58.16.231:9091> reject term_unmatched AppendEntriesRequest from 10.58.12.217:9091, term=4, prevLogIndex=4, prevLogTerm=4, localPrevLogTerm=0, lastLogIndex=0, entriesSize=0.
2023-10-13 17:20:06.442 INFO --- [JRaft-FSMCaller-Disruptor-0] [io.seata.server.cluster.raft.RaftStateMachine] [onStartFollowing] []: groupId: default, onStartFollowing: LeaderChangeContext [leaderId=10.58.12.217:9091, term=4, status=Status[ENEWLEADER<10011>: Raft node receives message from new leader with higher term.]].
2023-10-13 17:20:06.449 WARN --- [default/PeerPair[10.58.16.231:9091 -> 10.58.12.217:9091]-AppendEntriesThread0] [com.alipay.sofa.jraft.core.NodeImpl] [handleAppendEntriesRequest] []: Node <default/10.58.16.231:9091> reject term_unmatched AppendEntriesRequest from 10.58.12.217:9091, term=4, prevLogIndex=4, prevLogTerm=4, localPrevLogTerm=0, lastLogIndex=0, entriesSize=0.
2023-10-13 17:20:06.459 INFO --- [Bolt-default-executor-4-thread-1] [com.alipay.sofa.jraft.core.NodeImpl] [handleInstallSnapshot] []: Node <default/10.58.16.231:9091> received InstallSnapshotRequest from 10.58.12.217:9091, lastIncludedLogIndex=4, lastIncludedLogTerm=4, lastLogId=LogId [index=0, term=0].
2023-10-13 17:20:06.489 INFO --- [Bolt-conn-event-executor-13-thread-1] [com.alipay.sofa.jraft.rpc.impl.core.ClientServiceConnectionEventProcessor] [onEvent] []: Peer 10.58.12.217:9091 is connected
2023-10-13 17:20:06.519 INFO --- [JRaft-Group-Default-Executor-0] [com.alipay.sofa.jraft.util.Recyclers] [<clinit>] []: -Djraft.recyclers.maxCapacityPerThread: 4096.
2023-10-13 17:20:06.574 INFO --- [JRaft-Group-Default-Executor-0] [com.alipay.sofa.jraft.storage.snapshot.local.LocalSnapshotStorage] [destroySnapshot] []: Deleting snapshot sessionStore/raft/9091/default/snapshot/snapshot_4.
2023-10-13 17:20:06.574 INFO --- [JRaft-Group-Default-Executor-0] [com.alipay.sofa.jraft.storage.snapshot.local.LocalSnapshotStorage] [close] []: Renaming sessionStore/raft/9091/default/snapshot/temp to sessionStore/raft/9091/default/snapshot/snapshot_4.
2023-10-13 17:20:06.689 INFO --- [JRaft-FSMCaller-Disruptor-0] [io.seata.server.cluster.raft.snapshot.session.SessionSnapshotFile] [load] []: on snapshot load start index: 4
2023-10-13 17:20:06.694 INFO --- [JRaft-FSMCaller-Disruptor-0] [io.seata.server.cluster.raft.snapshot.session.SessionSnapshotFile] [load] []: on snapshot load end index: 4
2023-10-13 17:20:06.694 INFO --- [JRaft-FSMCaller-Disruptor-0] [io.seata.server.cluster.raft.RaftStateMachine] [onSnapshotLoad] []: groupId: default, onSnapshotLoad cost: 110 ms.
2023-10-13 17:20:06.694 INFO --- [JRaft-FSMCaller-Disruptor-0] [io.seata.server.cluster.raft.RaftStateMachine] [onConfigurationCommitted] []: groupId: default, onConfigurationCommitted: 10.58.12.165:9091,10.58.12.217:9091,10.58.16.231:9091.
2023-10-13 17:20:06.705 INFO --- [JRaft-FSMCaller-Disruptor-0] [com.alipay.sofa.jraft.storage.snapshot.SnapshotExecutorImpl] [onSnapshotLoadDone] []: Node <default/10.58.16.231:9091> onSnapshotLoadDone, last_included_index: 4
last_included_term: 4
peers: "10.58.12.165:9091"
peers: "10.58.12.217:9091"
peers: "10.58.16.231:9091"

2023-10-13 17:20:06.722 INFO --- [JRaft-Group-Default-Executor-1] [com.alipay.sofa.jraft.storage.impl.RocksDBLogStorage] [lambda$truncatePrefixInBackground$2] []: Truncated prefix logs in data path: sessionStore/raft/9091/default/log from log index 1 to 5, cost 0 ms.

3.3 faq

  • seata.raft.server-addr配置好后,必须通过server的openapi进行集群的扩缩容,直接改动该配置进行重启是不会生效的 接口为/metadata/v1/changeCluster?raftClusterStr=新的集群列表
  • 如果server-addr:中的地址都为本机,那么需要根据本机上不同的server的netty端口增加1000的偏移量,如server.port: 7092那么netty端口为8092,raft选举和通信端口便为9092,需要增加启动参数-Dserver.raftPort=9092. Linux下可以通过export JAVA_OPT="-Dserver.raftPort=9092"等方式指定。

4.压测对比

压测对比分为两种场景,并且为了避免数据热点冲突与线程调优等情况,将Client侧的数据初始化300W条商品,并直接使用jdk21虚拟线程+spring boot3+seata AT来测试,在gc方面全部采用分代ZGC进行,压测工具为阿里云PTS,Server侧统一使用jdk21(目前还未适配虚拟线程) 服务器配置如下 TC: 4c8g3 Client: 4c8G*1 数据库为阿里云rds 4c16g

  • 64并发压测只增加@Globaltransactional注解接口空提交的性能
  • 随机300W数据进行32并发10分钟的扣库存

4.1 1.7.1 db模式

raft压测模型

空提交 64C

db64-2

随机扣库存 32C

db32-2

4.2 2.0 raft模式

raft压测模型

空提交 64C

raft64-2

随机扣库存 32C

raft32c-2

4.3 压测结果对比

32并发对300W商品随机扣库存场景

tps avgtps maxcountrterror存储类型
1709(42%↑)2019(21%↑)1228803(42%↑)13.86ms(30%↓)0Raft
1201166886410519.86ms0DB

64并发空压@Globaltransactional接口(压测峰值上限为8000)

tps avgtps maxcountrterror存储类型
5704(20%↑)8062(30%↑)4101236(20%↑)7.79ms(19%↓)0Raft
4743617234102409.65ms0DB

除了以上数据上的直观对比,通过对压测的曲线图来观察,raft模式下tps与rt更加平稳,抖动更少,性能与吞吐量上更佳.

5.总结

在Seata未来的发展中,性能、入门门槛、部署运维成本,都是我们需要关注和不断优化的方向,在raft模式推出后有以下几个特点:

  1. 如存储方面,存算分离后Seata对其优化的上限被拔高,自主可控
  2. 部署成本更低,无需额外的注册中心,存储中间件
  3. 入门的门槛更低,无需学习其他的一些如注册中心的知识,一站式直接使用Seata Raft 即可上手

针对业界发展趋势,一些开源项目如ClickHouse和Kafka已经开始放弃使用ZooKeeper,并转而采用自研的解决方案,比如ClickKeeper和KRaft。这些方案将元数据等信息交由自身保证存储,以减少对第三方依赖的需求,从而降低运维成本和学习成本。这些特性是非常成熟和可借鉴的。

当然,目前来看,基于Raft模式的解决方案可能还不够成熟,可能无法完全达到上述描述的那样美好。然而,正是因为存在这样的理论基础,社区更应该朝着这个方向努力,让实践逐步接近理论的要求。在这里,欢迎所有对Seata感兴趣的同学加入社区,共同为Seata添砖加瓦!

· 阅读需 23 分钟

本文主要介绍分布式事务从内部到商业化和开源的演进历程,Seata社区当前进展和未来规划。

Seata是一款开源的分布式事务解决方案,旨在为现代化微服务架构下的分布式事务提供解决方案。Seata提供了完整的分布式事务解决方案,包括AT、TCC、Saga和XA事务模式,可支持多种编程语言和数据存储方案。Seata还提供了简便易用的API,以及丰富的文档和示例,方便企业在应用Seata时进行快速开发和部署。

Seata的优势在于具有高可用性、高性能、高扩展性等特点,同时在进行横向扩展时也无需做额外的复杂操作。 目前Seata已在阿里云上几千家客户业务系统中使用,其可靠性得到了业内各大厂商的认可和应用。

作为一个开源项目,Seata的社区也在不断扩大,现已成为开发者交流、分享和学习的重要平台,也得到了越来越多企业的支持和关注。

今天我主要针对以下三个小议题对Seata进行分享:

  • 从TXC/GTS 到 Seata
  • Seata 社区最新进展
  • Seata 社区未来规划

从TXC/GTS 到Seata

分布式事务的缘起

产品矩阵 Seata 在阿里内部的产品代号叫TXC(taobao transaction constructor),这个名字有非常浓厚的组织架构色彩。TXC 起源于阿里五彩石项目,五彩石是上古神话中女娲补天所用的石子,项目名喻意为打破关键技术壁垒,象征着阿里在从单体架构向分布式架构的演进过程中的重要里程碑。在这个项目的过程中演进出一批划时代的互联网中间件,包括我们常说的三大件:

  • HSF 服务调用框架
    解决单体应用到服务化后的服务通信调用问题。
  • TDDL 分库分表框架
    解决规模化后单库存储容量和连接数问题。
  • MetaQ 消息框架
    解决异步调用问题。

三大件的诞生满足了微服务化业务开发的基本需求,但是微服务化后的数据一致性问题并未得到妥善解决,缺少统一的解决方案。应用微服务化后出现数据一致性问题概率远大于单体应用,从进程内调用到网络调用这种复杂的环境加剧了异常场景的产生,服务跳数的增多使得在出现业务处理异常时无法协同上下游服务同时进行数据回滚。TXC的诞生正是为了解决应用架构层数据一致性的痛点问题,TXC 核心要解决的数据一致性场景包括:

  • 跨服务的一致性。 应对系统异常如调用超时和业务异常时协调上下游服务节点回滚。
  • 分库分表的数据一致性。 应对业务层逻辑SQL操作的数据在不同数据分片上,保证其分库分表操作的内部事务。
  • 消息发送的数据一致性。 应对数据操作和消息发送成功的不一致性问题。

为了克服以上通用场景遇到的问题,TXC与三大件做了无缝集成。业务使用三大件开发时,完全感知不到背后TXC的存在,业务不需要考虑数据一致性的设计问题,数据一致性保证交给了框架托管,业务更加聚焦于业务本身的开发,极大的提升了开发的效率。


GTS架构

TXC已在阿里集团内部广泛应用多年,经过双11等大型活动的洪荒流量洗礼,TXC极大提高了业务的开发效率,保证了数据的正确性,消除了数据不一致导致的资损和商誉问题。随着架构的不断演进,标准的三节点集群已可以承载接近10W TPS的峰值和毫秒级事务处理。在可用性和性能方面都达到了4个9的SLA保证,即使在无值守状态下也能保证全年无故障。


分布式事务的演进

新事物的诞生总是会伴随着质疑的声音。中间件层来保证数据一致性到底可靠吗?TXC最初的诞生只是一种模糊的理论,缺乏理论模型和工程实践。在我们进行MVP(最小可行产品)模型测试并推广业务上线后,经常出现故障,常常需要在深夜起床处理问题,睡觉时要佩戴手环来应对紧急响应,这也是我接管这个团队在技术上过的最痛苦的几年。

分布式事务演进

随后,我们进行了广泛的讨论和系统梳理。我们首先需要定义一致性问题,我们是要像RAFT一样实现多数共识一致性,还是要像Google Spanner一样解决数据库一致性问题,还是其他方式?从应用节点自上而下的分层结构来看,主要包括开发框架、服务调用框架、数据中间件、数据库Driver和数据库。我们需要决定在哪一层解决数据一致性问题。我们比较了解决不同层次数据一致性问题所面临的一致性要求、通用性、实现复杂度和业务接入成本。最后,我们权衡利弊,把实现复杂度留给我们,作为一个一致性组件,我们需要确保较高的一致性,但又不能锁定到具体数据库的实现上,确保场景的通用性和业务接入成本足够低以便更容易实现业务,这也是TXC最初采用AT模式的原因。

分布式事务它不仅仅是一个框架,它是一个体系。 我们在理论上定义了一致性问题,概念上抽象出了模式、角色、动作和隔离性等。从工程实践的角度,我们定义了编程模型,包括低侵入的注解、简单的方法模板和灵活的API ,定义了事务的基础能力和增强能力(例如如何以低成本支持大量活动),以及运维、安全、性能、可观测性和高可用等方面的能力。

事务逻辑模型 分布式事务解决了哪些问题呢?一个经典且具有体感的例子就是转账场景。转账过程包括减去余额和增加余额两个步骤,我们如何保证操作的原子性?在没有任何干预的情况下,这两个步骤可能会遇到各种问题,例如B账户已销户或出现服务调用超时等情况。

超时问题一直是分布式应用中比较难解决的问题,我们无法准确知晓B服务是否执行以及其执行顺序。从数据的角度来看,这意味着B 账户的钱未必会被成功加起来。在服务化改造之后,每个节点仅获知部分信息,而事务本身需要全局协调所有节点,因此需要一个拥有上帝视角、能够获取全部信息的中心化角色,这个角色就是TC(transaction coordinator),它用于全局协调事务的状态。TM(Transaction Manager) 则是驱动事务生成提议的角色。但是,即使上帝也有打瞌睡的时候,他的判断也并不总是正确的,因此需要一个RM(resource manager) 角色作为灵魂的代表来验证事务的真实性。这就是TXC 最基本的哲学模型。我们从方法论上验证了它的数据一致性是非常完备的,当然,我们的认知是有边界的。也许未来会证明我们是火鸡工程师,但在当前情况下,它的模型已经足以解决大部分现有问题。

分布式事务性能 经过多年的架构演进,从事务的单链路耗时角度来看,TXC在事务开始时的处理平均时间约为0.2毫秒,分支注册的平均时间约为0.4毫秒,整个事务额外的耗时在毫秒级别之内。这也是我们推算出的极限理论值。在吞吐量方面,单节点的TPS达到3万次/秒,标准集群的TPS接近10万次/秒。


Seata 开源

为什么要做开源?这是很多人问过我的问题。2017年我们做了商业化的 GTS(Global Transaction Service )产品产品在阿里云上售卖,有公有云和专有云两种形态。此时集团内发展的顺利,但是在我们商业化的过程中并不顺利,我们遇到了各种各样的问题,问题总结起来主要包括两类:一是开发者对于分布式事务的理论相当匮乏, 大多数人连本地事务都没搞明白是怎么回事更何况是分布式事务。 二是产品成熟度上存在问题, 经常遇到稀奇古怪的场景问题,导致了支持交付成本的急剧上升,研发变成了售后客服。

我们反思为什么遇到如此多的问题,这里主要的问题是在阿里集团内部是统一语言栈和统一技术栈的,我们对特定场景的打磨是非常成熟的,服务阿里一家公司和服务云上成千上万家企业有本质的区,这也启示我们产品的场景生态做的不够好。在GitHub 80%以上的开源软件是基础软件,基础软件首要解决的是场景通用性问题,因此它不能被有一家企业Lock In,比如像Linux,它有非常多的社区分发版本。因此,为了让我们的产品变得更好,我们选择了开源,与开发者们共建,普及更多的企业用户。

阿里开源 阿里的开源经历了三个主要阶段。第一个阶段是Dubbo所处的阶段,开发者用爱发电, Dubbo开源了有10几年的时间,时间充分证明了Dubbo是非常优秀的开源软件,它的微内核插件化的扩展性设计也是我最初开源Seata 的重要参考。做软件设计的时候我们要思考扩展性和性能权衡起来哪个会更重要一些,我们到底是要做一个三年的设计,五年的设计亦或是满足业务发展的十年设计。我们在做0-1服务调用问题的解决方案的同时,能否预测到1-100规模化后的治理问题。

第二个阶段是开源和商业化的闭环,商业化反哺于开源社区,促进了开源社区的发展。 我认为云厂商更容易做好开源的原因如下:

  • 首先,云是一个规模化的经济,必然要建立在稳定成熟的内核基础上,在上面去包装其产品化能力包括高可用、免运维和弹性能力。不稳定的内核必然导致过高的交付支持成本,研发团队的支持答疑穿透过高,过高的交付成本无法实现大规模的复制,穿透率过高无法使产品快速的演进迭代。
  • 其次,商业产品是更懂业务需求的。我们内部团队做技术的经常是站在研发的视角YY 需求,做出来的东西没有人使用,也就不会形成价值的转换。商业化收集到的都是真实的业务需求,因此,它的开源内核也必须会朝着这个方向演进。如果不朝着这个方向去演进必然导致两边架构上的分裂,增加团队的维护成本。
  • 最后,开源和商业化闭环,能促进双方更好的发展。如果开源内核经常出现各种问题,你是否愿意相信的它的商业化产品是足够优秀的。

第三个阶段是体系化和标准化。 首先,体系化是开源解决方案的基础。阿里的开源项目大多是基于内部电商场景的实践而诞生的。例如Higress,它用于打通蚂蚁集团的网关;Nacos承载着服务的百万实例和千万连接;Sentinel 提供大促时的降级和限流等高可用性能力;而Seata负责保障交易数据的一致性。这套体系化的开源解决方案是基于阿里电商生态的最佳实践而设计的。其次,标准化是另一个重要的特点。以OpenSergo为例,它既是一个标准,又是一个实现。在过去几年里,国内开源项目数量呈爆发式增长。然而,各个开源产品的能力差异很大,彼此集成时会遇到许多兼容性问题。因此,像OpenSergo这样的开源项目能够定义一些标准化的能力和接口,并提供一些实现,这将为整个开源生态系统的发展提供极大的帮助。


Seata 社区最新进展

Seata 社区简介

社区简介 目前,Seata已经开源了4种事务模式,包括AT、TCC、Saga和XA,并在积极探索其他可行的事务解决方案。 Seata已经与10多个主流的RPC框架和关系数据库进行了集成,同时与20 多个社区存在集成和被集成的关系。此外,我们还在多语言体系上探索除Java之外的语言,如Golang、PHP、Python和JS。

Seata已经被几千家客户应用到业务系统中。Seata的应用已经变得越来越成熟,在金融业务场景中信银行和光大银行与社区做了很好的合作,并成功将其纳入到核心账务系统中。在金融场景对微服务体系的落地是非常严苛的,这也标志着Seata的内核成熟度迈上了一个新台阶。


Seata 扩展生态

扩展生态 Seata采用了微内核和插件化的设计,它在API、注册配置中心、存储模式、锁控制、SQL解析器、负载均衡、传输、协议编解码、可观察性等方面暴露了丰富的扩展点。 这使得业务可以方便地进行灵活的扩展和技术组件的选择。


Seata 应用案例

应用案例 案例1:中航信航旅纵横项目
中航信航旅纵横项目在Seata 0.2版本中引入Seata解决机票和优惠券业务的数据一致性问题,大大提高了开发效率、减少了数据不一致造成的资损并提升了用户交互体验。

案例2:滴滴出行二轮车事业部
滴滴出行二轮车事业部在Seata 0.6.1版本中引入Seata,解决了小蓝单车、电动车、资产等业务流程的数据一致性问题,优化了用户使用体验并减少了资产的损失。

案例3:美团基础架构
美团基础架构团队基于开源的Seata项目开发了内部分布式事务解决方案Swan,被用于解决美团内部各业务的分布式事务问题。

场景4:盒马小镇
盒马小镇在游戏互动中使用Seata控制偷花的流程,开发周期大大缩短,从20天缩短到了5天,有效降低了开发成本。


Seata 事务模式的演进

模式演进


Seata 当前进展

  • 支持 Oracle和 Postgresql 多主键。
  • 支持 Dubbo3
  • 支持 Spring Boot3
  • 支持 JDK 17
  • 支持 ARM64 镜像
  • 支持多注册模型
  • 扩展了多种SQL语法
  • 支持 GraalVM Native Image
  • 支持 Redis lua 存储模式

Seata 2.x 发展规划

发展规划

主要包括下面几个方面:

  • 存储/协议/特性
    存储模式上探索存算不分离的Raft集群模式;更好的体验,统一当前4种事务模式的API;兼容GTS协议;支持Saga注解;支持分布式锁的控制;支持以数据视角的洞察和治理。
  • 生态
    融合支持更多的数据库,更多的服务框架,同时探索国产化信创生态的支持;支持MQ生态;进一步完善APM的支持。
  • 解决方案
    解决方案上除了支持微服务生态探索多云方案;更贴近云原生的解决方案;增加安全和流量防护能力;实现架构上核心组件的自闭环收敛。
  • 多语言生态
    多语言生态中Java最成熟,其他已支持的编程语言继续完善,同时探索与语言无关的Transaction Mesh方案。
  • 研发效能/体验
    提升测试的覆盖率,优先保证质量、兼容性和稳定性;重构官网文档结构,提升文档搜索的命中率;在体验上简化运维部署,实现一键安装和配置元数据简化;控制台支持事务控制和在线分析能力。

一句话总结2.x 的规划:更大的场景,更大的生态,从可用到好用。


Seata 社区联系方式

联系方式

· 阅读需 12 分钟

Seata简介

Seata的前身是阿里巴巴集团内大规模使用保证分布式事务一致性的中间件,Seata是其开源产品,由社区维护。在介绍Seata前,先与大家讨论下我们业务发展过程中经常遇到的一些问题场景。

业务场景

我们业务在发展的过程中,基本上都是从一个简单的应用,逐渐过渡到规模庞大、业务复杂的应用。这些复杂的场景难免遇到分布式事务管理问题,Seata的出现正是解决这些分布式场景下的事务管理问题。介绍下其中几个经典的场景:

场景一:分库分表场景下的分布式事务

image.png 起初我们的业务规模小、轻量化,单一数据库就能保障我们的数据链路。但随着业务规模不断扩大、业务不断复杂化,通常单一数据库在容量、性能上会遭遇瓶颈。通常的解决方案是向分库、分表的架构演进。此时,即引入了分库分表场景下的分布式事务场景。

场景二:跨服务场景下的分布式事务

image.png 降低单体应用复杂度的方案:应用微服务化拆分。拆分后,我们的产品由多个功能各异的微服务组件构成,每个微服务都使用独立的数据库资源。在涉及到跨服务调用的数据一致性场景时,就引入了跨服务场景下的分布式事务。

Seata架构

image.png 其核心组件主要如下:

  • Transaction Coordinator(TC)

事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。

  • Transaction Manager(TM)

控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议,TM定义全局事务的边界。

  • Resource Manager(RM)

控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。RM负责定义分支事务的边界和行为。

Seata的可观测实践

为什么需要可观测?

  • 分布式事务消息链路较复杂

Seata在解决了用户易用性和分布式事务一致性这些问题的同时,需要多次TC与TM、RM之间的交互,尤其当微服务的链路变复杂时,Seata的交互链路也会呈正相关性增加。这种情况下,其实我们就需要引入可观测的能力来观察、分析事物链路。

  • 异常链路、故障排查难定位,性能优化无从下手

在排查Seata的异常事务链路时,传统的方法需要看日志,这样检索起来比较麻烦。在引入可观测能力后,帮助我们直观的分析链路,快速定位问题;为优化耗时的事务链路提供依据。

  • 可视化、数据可量化

可视化能力可让用户对事务执行情况有直观的感受;借助可量化的数据,可帮助用户评估资源消耗、规划预算。

可观测能力概览

可观测维度seata期望的能力技术选型参考
Metrics功能层面:可按业务分组隔离,采集事务总量、耗时等重要指标
性能层面:高度量性能,插件按需加载
架构层面:减少第三方依赖,服务端、客户端能够采用统一的架构,减少技术复杂度
兼容性层面:至少兼容Prometheus生态Prometheus:指标存储和查询等领域有着业界领先的地位
OpenTelemetry:可观测数据采集和规范的事实标准。但自身并不负责数据的存储,展示和分析
Tracing功能层面:全链路追踪分布式事务生命周期,反应分布式事务执行性能消耗
易用性方面:对使用seata的用户而言简单易接入SkyWalking:利用Java的Agent探针技术,效率高,简单易用。
Logging功能层面:记录服务端、客户端全部生命周期信息
易用性层面:能根据XID快速匹配全局事务对应链路日志Alibaba Cloud Service
ELK

Metrics维度

设计思路

  1. Seata作为一个被集成的数据一致性框架,Metrics模块将尽可能少的使用第三方依赖以降低发生冲突的风险
  2. Metrics模块将竭力争取更高的度量性能和更低的资源开销,尽可能降低开启后带来的副作用
  3. 配置时,Metrics是否激活、数据如何发布,取决于对应的配置;开启配置则自动启用,并默认将度量数据通过prometheusexporter的形式发布
  4. 不使用Spring,使用SPI(Service Provider Interface)加载扩展

模块设计

图片 1.png

  • seata-metrics-core:Metrics核心模块,根据配置组织(加载)1个Registry和N个Exporter;
  • seata-metrics-api:定义了Meter指标接口,Registry指标注册中心接口;
  • seata-metrics-exporter-prometheus:内置的prometheus-exporter实现;
  • seata-metrics-registry-compact:内置的Registry实现,并轻量级实现了Gauge、Counter、Summay、Timer指标;

metrics模块工作流

图片 1.png 上图是metrics模块的工作流,其工作流程如下:

  1. 利用SPI机制,根据配置加载Exporter和Registry的实现类;
  2. 基于消息订阅与通知机制,监听所有全局事务的状态变更事件,并publish到EventBus;
  3. 事件订阅者消费事件,并将生成的metrics写入Registry;
  4. 监控系统(如prometheus)从Exporter中拉取数据。

TC核心指标

image.png

TM核心指标

image.png

RM核心指标

image.png

大盘展示

lQLPJxZhZlqESU3NBpjNBp6w8zYK6VbMgzYCoKVrWEDWAA_1694_1688.png

Tracing维度

Seata为什么需要tracing?

  1. 对业务侧而言,引入Seata后,对业务性能会带来多大损耗?主要时间消耗在什么地方?如何针对性的优化业务逻辑?这些都是未知的。
  2. Seata的所有消息记录都通过日志持久化落盘,但对不了解Seata的用户而言,日志非常不友好。能否通过接入Tracing,提升事务链路排查效率?
  3. 对于新手用户,可通过Tracing记录,快速了解seata的工作原理,降低seata使用门槛。

Seata的tracing解决方案

  • Seata在自定义的RPC消息协议中定义了Header信息;
  • SkyWalking拦截指定的RPC消息,并注入tracing相关的span信息;
  • 以RPC消息的发出&接收为临界点,定义了span的生命周期范围。

基于上述的方式,Seata实现了事务全链路的tracing,具体接入可参考为[Seata应用 | Seata-server]接入Skywalking

tracing效果

  • 基于的demo场景:
  1. 用户请求交易服务
  2. 交易服务锁定库存
  3. 交易服务创建账单
  4. 账单服务进行扣款

image.png

  • GlobalCommit成功的事务链路(事例)

image.png image.png image.png

Logging维度

设计思路

image.png Logging这一块其实承担的是可观测这几个维度当中的兜底角色。放在最底层的,其实就是我们日志格式的设计,只有好日志格式,我们才能对它进行更好的采集、模块化的存储和展示。在其之上,是日志的采集、存储、监控、告警、数据可视化,这些模块更多的是有现成的工具,比如阿里的SLS日志服务、还有ELK的一套技术栈,我们更多是将开销成本、接入复杂度、生态繁荣度等作为考量。

日志格式设计

这里拿Seata-Server的一个日志格式作为案例: image.png

  • 线程池规范命名:当线程池、线程比较多时,规范的线程命名能将无序执行的线程执行次序清晰展示。
  • 方法全类名可追溯:快速定位到具体的代码块。
  • 重点运行时信息透出:重点突出关键日志,不关键的日志不打印,减少日志冗余。
  • 消息格式可扩展:通过扩展消息类的输出格式,减少日志的代码修改量。

总结&展望

Metrics

总结:基本实现分布式事务的可量化、可观测。 展望:更细粒度的指标、更广阔的生态兼容。

Tracing

总结:分布式事务全链路的可追溯。 展望:根据xid追溯事务链路,异常链路根因快速定位。

Logging

总结:结构化的日志格式。 展望:日志可观测体系演进。

· 阅读需 3 分钟

生产环境可用的 seata-go 1.2.0 来了

Seata 是一款开源的分布式事务解决方案,提供高性能和简单易用的分布式事务服务。

发布概览

Seata-go 1.2.0 版本支持 XA 模式。XA 协议是由 X/Open 组织提出的分布式事务处理规范,其优点是对业务代码无侵入。当前 Seata-go 的 XA 模式支持 MySQL 数据库。至此,seata-go 已经集齐 AT、TCC、Saga 和 XA 四种事务模式,完成了与 Seata Java 的功能对齐。 XA 模式的主要功能:

feature

  • [#467] 实现 XA 模式支持 MySQL
  • [#534] 支持 session 的负载均衡

bugfix

  • [#540] 修复初始化 xa 模式的 bug
  • [#545] 修复 xa 模式获取 db 版本号的 bug
  • [#548] 修复启动 xa 会失败的 bug
  • [#556] 修复 xa 数据源的 bug
  • [#562] 修复提交 xa 全局事务的 bug
  • [#564] 修复提交 xa 分支事务的 bug
  • [#566] 修复使用 xa 数据源执行本地事务的 bug

optimize

  • [#523] 优化 CI 流程
  • [#525] 将 jackson 序列化重命名为 json
  • [#532] 移除重复的代码
  • [#536] 优化 go import 代码格式
  • [#554] 优化 xa 模式的性能
  • [#561] 优化 xa 模式的日志输出

test

  • [#535] 添加集成测试

doc

  • [#550] 添加 1.2.0 版本的改动日志

contributors

Thanks to these contributors for their code commits. Please report an unintended omission.

· 阅读需 11 分钟

欢迎大家报名Seata 开源之夏2023课题

开源之夏 2023 学生报名期为 4 月 29 日-6月4日,欢迎报名Seata 2023 课题!在这里,您将有机会深入探讨分布式事务的理论和应用,并与来自不同背景的同学一起合作完成实践项目。我们期待着您的积极参与和贡献,共同推动分布式事务领域的发展。

summer2023-1

开源之夏2023

开源之夏是由中科院软件所“开源软件供应链点亮计划”发起并长期支持的一项暑期开源活动,旨在鼓励在校学生积极参与开源软件的开发维护,培养和发掘更多优秀的开发者,促进优秀开源软件社区的蓬勃发展,助力开源软件供应链建设。

参与学生通过远程线上协作方式,配有资深导师指导,参与到开源社区各组织项目开发中并收获奖金、礼品与证书。这些收获,不仅仅是未来毕业简历上浓墨重彩的一笔,更是迈向顶尖开发者的闪亮起点,可以说非常值得一试。 每个项目难度分为基础和进阶两档,对应学生结项奖金分别为税前人民币 8000 元和税前人民币 12000 元。

Seata社区介绍

Seata 是一款开源的分布式事务解决方案,GitHub获得超过23K+ Starts致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 在阿里内部一直扮演着分布式数据一致性的中间件角色,几乎每笔交易都要使用Seata,历经双11洪荒流量的洗礼,对业务进行了有力的技术支撑。

Seata社区开源之夏2023项目课题汇总

Seata社区为开源之夏2023组委会推荐6项精选项目课题,您可以访问以下链接进行选报:
https://summer-ospp.ac.cn/org/orgdetail/064c15df-705c-483a-8fc8-02831370db14?lang=zh
请及时与各导师沟通并准备项目申请材料,并登录官方注册申报(以下课题顺序不分先后): seata2023-2

项目一: 实现用于服务发现和注册的NamingServer

难度: 进阶/Advanced

项目社区导师: 陈健斌

导师联系邮箱: 364176773@qq.com

项目简述:
目前seata的服务暴露及发现主要依赖于第三方注册中心,随着项目的演进发展,带来了额外的学习使用成本,而业内主流具有服务端的中间件大多都开始演进自身的服务自闭环和控制及提供于服务端更高契合度和可靠性的组件或功能如kafka 的KRaft,rocketmq的NameServer,clickhouse的ClickHouse Keeper等.故为了解决如上问题和架构演进要求,seata需要构建自身的nameserver来保证更加稳定可靠。

项目链接: https://summer-ospp.ac.cn/org/prodetail/230640380?list=org&navpage=org



项目二: 在seata-go中实现saga事务模式

难度: 进阶/Advanced

项目社区导师: 刘月财

导师联系邮箱: luky116@apache.org

项目简述: Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。Seata-go 中当前没有支持saga模式,希望后面参考Java版本的实现能将该功能支持上。

项目链接: https://summer-ospp.ac.cn/org/prodetail/230640382?list=org&navpage=org



项目三: seata saga模式产品化能力提升

难度: 进阶/Advanced

项目社区导师: 李宗杰

导师联系邮箱: leezongjie@163.com

项目简述: saga作为分布式事务的解决方案之一,在长事务上应用尤其广泛,seata也提供了对应的状态机实现。随着业务的不断发展和接入,对seata提出了更高的要求,我们需要支持流式saga 编排,对当前状态机的实现进行优化和扩展,进一步服务业务。

项目链接: https://summer-ospp.ac.cn/org/prodetail/230640415?list=org&navpage=org



项目四: 增加控制台事务控制能力

难度: 进阶/Advanced

项目社区导师: 王良

导师联系邮箱: 841369634@qq.com

项目简述: 在分布式事务中,经常会存在非常多的异常情况,这些异常情况往往会导致系统无法继续运行。而这些异常往往需要人工介入排查原因并排除故障,才能够使系统继续正常运行。虽然seata 提供了控制台查询事务数据,但还未提供任何事务控制能力,帮助排除故障。所以,本课题主要是在seata服务端控制台上,添加事务控制能力。

项目链接: https://summer-ospp.ac.cn/org/prodetail/230640423?list=org&navpage=org



项目五: 提高单测覆盖率和建立集成测试

难度: 基础/Basic

项目社区导师: 张嘉伟

导师联系邮箱: 349071347@qq.com

项目简述: 为了进一步提高项目的质量以及稳定性, 需要进一步提升项目单元测试覆盖率以及加入集成测试来模拟生产中不同的场景. 集成测试的目的是为了模拟client与server的交互过程, 而非单一的对某个接口进行测试。

项目链接: https://summer-ospp.ac.cn/org/prodetail/230640424?list=org&navpage=org



项目六: 实现Seata运维ctl工具

难度: 进阶/Advanced

项目社区导师: 季敏

导师联系邮箱: jimin.jm@alibaba-inc.com

项目简述: 运维ctl命令在Seata中非常重要,它是Seata的命令行工具,可以帮助我们管理和操作Seata的各种组件。运维ctl命令可以让我们快速地启动、停止和管理Seata服务,定位和解决问题。此外,运维ctl 命令还提供了丰富的指令,可以让我们方便地检查Seata的健康状态、模拟事务和打印导出配置信息等,大大提高了我们的工作效率和运维体验。

以下是对实现定制ctl运维命令行的一些建议:

  • 借鉴其他开源项目的实现方式,比如kubectl,helm等,并根据Seata的特点和需求进行定制。
  • 将常用的运维操作直接封装进命令行,减少用户的手动操作。
  • 考虑使用友好的命令和参数名称,将命令行设计得易于理解和记忆。
  • 提供详细的帮助文档和示例,帮助用户快速上手和了解如何使用各种参数和选项。
  • 考虑命令行的跨平台支持,例如支持Windows、Linux和MacOS等操作系统。 一款好的ctl命令行应该是易用、灵活、可定制、健壮和易维护的。

项目链接:https://summer-ospp.ac.cn/org/prodetail/230640431?list=org&navpage=org



如何参与开源之夏2023并快速选定项目?

欢迎通过上方联系方式,与各导师沟通并准备项目申请材料。

课题参与期间,学生可以在世界任何地方线上工作,Seata相关项目结项需要在9月30日前以 PR 的形式提交到Seata社区仓库中并完成合并,请务必尽早准备。 seata2023-3

需要在课题期间第一时间获取导师及其他信息,可扫码进入钉钉群交流 ——了解Seata社区各领域项目、结识Seata社区开源导师,以助力后续申请。



参考资料:

Seata网站 : https://seata.apache.org/

Seata GitHub : https://github.com/seata

开源之夏官网: https://summer-ospp.ac.cn/org/orgdetail/ab188e59-fab8-468f-bc89-bdc2bd8b5e64?lang=zh

如果同学们对微服务其他领域项目感兴趣,也可以尝试申请,例如:

  • 对于微服务配置注册中心有兴趣的同学,可以尝试填报Nacos 开源之夏
  • 对于微服务框架和RPC框架有兴趣的同学,可以尝试填报Spring Cloud Alibaba 开源之夏Dubbo 开源之夏
  • 对于云原生网关有兴趣的同学,可以尝试填报Higress 开源之夏
  • 对于分布式高可用防护有兴趣的同学,可以尝试填报 [Sentinel 开源之夏](https://summer-ospp.ac. cn/org/orgdetail/5e879522-bd90-4a8b-bf8b-b11aea48626b?lang=zh) ;
  • 对于微服务治理有兴趣的同学,可以尝试填报 [OpenSergo 开源之夏](https://summer-ospp.ac. cn/org/orgdetail/aaff4eec-11b1-4375-997d-5eea8f51762b?lang=zh)。

· 阅读需 7 分钟

Seata 1.6.0 重磅发布,大幅提升性能

Seata 是一款开源的分布式事务解决方案,提供高性能和简单易用的分布式事务服务。

seata-server 下载链接:

source | binary

此版本更新如下:

feature:

  • [#4863] 支持 oracle 和 postgresql 多主键
  • [#4649] seata-server支持多注册中心
  • [#4779] 支持 Apache Dubbo3
  • [#4479] TCC注解支持添加在接口和实现类上
  • [#4877] client sdk 支持jdk17
  • [#4914] 支持 mysql 的update join联表更新语法
  • [#4542] 支持 oracle timestamp 类型
  • [#5111] 支持Nacos contextPath 配置
  • [#4802] dockerfile 支持 arm64

bugfix:

  • [#4780] 修复超时回滚成功后无法发送TimeoutRollbacked事件
  • [#4954] 修复output表达式错误时,保存执行结果空指针异常
  • [#4817] 修复高版本springboot配置不标准的问题
  • [#4838] 修复使用 Statement.executeBatch() 时无法生成undo log 的问题
  • [#4533] 修复handleRetryRollbacking的event重复导致的指标数据不准确
  • [#4912] 修复mysql InsertOnDuplicateUpdate 列名大小写不一致无法正确匹配
  • [#4543] 修复对 Oracle 数据类型nclob的支持
  • [#4915] 修复获取不到ServerRecoveryProperties属性的问题
  • [#4919] 修复XID的port和address出现null:0的情况
  • [#4928] 修复 rpcContext.getClientRMHolderMap NPE 问题
  • [#4953] 修复InsertOnDuplicateUpdate可绕过修改主键的问题
  • [#4978] 修复 kryo 支持循环依赖
  • [#4985] 修复 undo_log id重复的问题
  • [#4874] 修复OpenJDK 11 启动失败
  • [#5018] 修复启动脚本中 loader path 使用相对路径导致 server 启动失败问题
  • [#5004] 修复mysql update join行数据重复的问题
  • [#5032] 修复mysql InsertOnDuplicateUpdate中条件参数填充位置计算错误导致的镜像查询SQL语句异常问题
  • [#5033] 修复InsertOnDuplicateUpdate的SQL语句中无插入列字段导致的空指针问题
  • [#5038] 修复SagaAsyncThreadPoolProperties冲突问题
  • [#5050] 修复Saga模式下全局状态未正确更改成Committed问题
  • [#5052] 修复update join条件中占位符参数问题
  • [#5031] 修复InsertOnDuplicateUpdate中不应该使用null值索引作为查询条件
  • [#5075] 修复InsertOnDuplicateUpdate无法拦截无主键和唯一索引的SQL
  • [#5093] 修复seata server重启后accessKey丢失问题
  • [#5092] 修复当seata and jpa共同使用时, AutoConfiguration的顺序不正确的问题
  • [#5109] 修复当RM侧没有加@GlobalTransactional报NPE的问题
  • [#5098] Druid 禁用 oracle implicit cache
  • [#4860] 修复metrics tag覆盖问题
  • [#5028] 修复 insert on duplicate SQL中 null 值问题
  • [#5078] 修复SQL语句中无主键和唯一键拦截问题
  • [#5097] 修复当Server重启时 accessKey 丢失问题
  • [#5131] 修复XAConn处于active状态时无法回滚的问题
  • [#5134] 修复hikariDataSource 自动代理在某些情况下失效的问题
  • [#5163] 修复高版本JDK编译失败的问题

optimize:

  • [#4681] 优化竞争锁过程
  • [#4774] 优化 seataio/seata-server 镜像中的 mysql8 依赖
  • [#4750] 优化AT分支释放全局锁不使用xid
  • [#4790] 添加自动发布 OSSRH github action
  • [#4765] mysql8.0.29版本及以上XA模式不持connection至二阶段
  • [#4797] 优化所有github actions脚本
  • [#4800] 添加 NOTICE 文件
  • [#4761] 使用 hget 代替 RedisLocker 中的 hmget
  • [#4414] 移除log4j依赖
  • [#4836] 优化 BaseTransactionalExecutor#buildLockKey(TableRecords rowsIncludingPK) 方法可读性
  • [#4865] 修复 Saga 可视化设计器 GGEditor 安全漏洞
  • [#4590] 自动降级支持开关支持动态配置
  • [#4490] tccfence 记录表优化成按索引删除
  • [#4911] 添加 header 和license 检测
  • [#4917] 升级 package-lock.json 修复漏洞
  • [#4924] 优化 pom 依赖
  • [#4932] 抽取部分配置的默认值
  • [#4925] 优化 javadoc 注释
  • [#4921] 修复控制台模块安全漏洞和升级 skywalking-eyes 版本
  • [#4936] 优化存储配置的读取
  • [#4946] 将获取锁时遇到的sql异常传递给客户端
  • [#4962] 优化构建配置,并修正docker镜像的基础镜像
  • [#4974] 取消redis模式下,查询globalStatus数量的限制
  • [#4981] 优化当tcc fence记录查不到时的错误提示
  • [#4995] 修复mysql InsertOnDuplicateUpdate后置镜像查询SQL中重复的主键查询条件
  • [#5047] 移除无用代码
  • [#5051] 回滚时undolog产生脏写需要抛出不再重试(BranchRollbackFailed_Unretriable)的异常
  • [#5075] 拦截没有主键及唯一索引值的insert on duplicate update语句
  • [#5104] ConnectionProxy脱离对druid的依赖
  • [#5124] 支持oracle删除TCC fence记录表
  • [#4468] 支持kryo 5.3.0
  • [#4807] 优化镜像和OSS仓库发布流水线
  • [#4445] 优化事务超时判断
  • [#4958] 优化超时事务 triggerAfterCommit() 的执行
  • [#4582] 优化redis存储模式的事务排序
  • [#4963] 增加 ARM64 流水线 CI 测试
  • [#4434] 移除 seata-server CMS GC 参数

test:

  • [#4411] 测试Oracle数据库AT模式下类型支持
  • [#4794] 重构代码,尝试修复单元测试 DataSourceProxyTest.getResourceIdTest()
  • [#5101] 修复zk注册和配置中心报ClassNotFoundException的问题 DataSourceProxyTest.getResourceIdTest()

非常感谢以下 contributors 的代码贡献。若有无意遗漏,请报告。

同时,我们收到了社区反馈的很多有价值的issue和建议,非常感谢大家。

· 阅读需 3 分钟

Seata 1.5.2 重磅发布,支持xid负载均衡

Seata 是一款开源的分布式事务解决方案,提供高性能和简单易用的分布式事务服务。

seata-server 下载链接:

source | binary

此版本更新如下:

feature:

  • [#4661] 支持根据xid负载均衡算法
  • [#4676] 支持Nacos作为注册中心时,server通过挂载SLB暴露服务
  • [#4642] 支持client批量请求并行处理
  • [#4567] 支持where条件中find_in_set函数

bugfix:

  • [#4515] 修复develop分支SeataTCCFenceAutoConfiguration在客户端未使用DB时,启动抛出ClassNotFoundException的问题。
  • [#4661] 修复控制台中使用PostgreSQL出现的SQL异常
  • [#4667] 修复develop分支RedisTransactionStoreManager迭代时更新map的异常
  • [#4678] 修复属性transport.enableRmClientBatchSendRequest没有配置的情况下缓存穿透的问题
  • [#4701] 修复命令行参数丢失问题
  • [#4607] 修复跳过全局锁校验的缺陷
  • [#4696] 修复 oracle 存储模式时的插入问题
  • [#4726] 修复批量发送消息时可能的NPE问题
  • [#4729] 修复AspectTransactional.rollbackForClassName设置错误
  • [#4653] 修复 INSERT_ON_DUPLICATE 主键为非数值异常

optimize:

  • [#4650] 修复安全漏洞
  • [#4670] 优化branchResultMessageExecutor线程池的线程数
  • [#4662] 优化回滚事务监控指标
  • [#4693] 优化控制台导航栏
  • [#4700] 修复 maven-compiler-plugin 和 maven-resources-plugin 执行失败
  • [#4711] 分离部署时 lib 依赖
  • [#4720] 优化pom描述
  • [#4728] 将logback版本依赖升级至1.2.9
  • [#4745] 发行包中支持 mysql8 driver
  • [#4626] 使用 easyj-maven-plugin 插件代替 flatten-maven-plugin插件,以修复shade 插件与 flatten 插件不兼容的问题
  • [#4629] 更新globalSession状态时检查更改前后的约束关系
  • [#4662] 优化 EnhancedServiceLoader 可读性

test:

  • [#4544] 优化TransactionContextFilterTest中jackson包依赖问题
  • [#4731] 修复 AsyncWorkerTest 和 LockManagerTest 的单测问题。

非常感谢以下 contributors 的代码贡献。若有无意遗漏,请报告。

同时,我们收到了社区反馈的很多有价值的issue和建议,非常感谢大家。

· 阅读需 14 分钟

今天来聊一聊阿里巴巴 Seata 新版本(1.5.1)是怎么解决 TCC 模式下的幂等、悬挂和空回滚问题的。

1 TCC 回顾

TCC 模式是最经典的分布式事务解决方案,它将分布式事务分为两个阶段来执行,try 阶段对每个分支事务进行预留资源,如果所有分支事务都预留资源成功,则进入 commit 阶段提交全局事务,如果有一个节点预留资源失败则进入 cancel 阶段回滚全局事务。

以传统的订单、库存、账户服务为例,在 try 阶段尝试预留资源,插入订单、扣减库存、扣减金额,这三个服务都是要提交本地事务的,这里可以把资源转入中间表。在 commit 阶段,再把 try 阶段预留的资源转入最终表。而在 cancel 阶段,把 try 阶段预留的资源进行释放,比如把账户金额返回给客户的账户。

注意:try 阶段必须是要提交本地事务的,比如扣减订单金额,必须把钱从客户账户扣掉,如果不扣掉,在 commit 阶段客户账户钱不够了,就会出问题。

1.1 try-commit

try 阶段首先进行预留资源,然后在 commit 阶段扣除资源。如下图:

fence-try-commit

1.2 try-cancel

try 阶段首先进行预留资源,预留资源时扣减库存失败导致全局事务回滚,在 cancel 阶段释放资源。如下图:

fence-try-cancel

2 TCC 优势

TCC 模式最大的优势是效率高。TCC 模式在 try 阶段的锁定资源并不是真正意义上的锁定,而是真实提交了本地事务,将资源预留到中间态,并不需要阻塞等待,因此效率比其他模式要高。

同时 TCC 模式还可以进行如下优化:

2.1 异步提交

try 阶段成功后,不立即进入 confirm/cancel 阶段,而是认为全局事务已经结束了,启动定时任务来异步执行 confirm/cancel,扣减或释放资源,这样会有很大的性能提升。

2.2 同库模式

TCC 模式中有三个角色:

  • TM:管理全局事务,包括开启全局事务,提交/回滚全局事务;
  • RM:管理分支事务;
  • TC: 管理全局事务和分支事务的状态。

下图来自 Seata 官网:

fence-fiffrent-db

TM 开启全局事务时,RM 需要向 TC 发送注册消息,TC 保存分支事务的状态。TM 请求提交或回滚时,TC 需要向 RM 发送提交或回滚消息。这样包含两个个分支事务的分布式事务中,TC 和 RM 之间有四次 RPC。

优化后的流程如下图:

fence-same-db

TC 保存全局事务的状态。TM 开启全局事务时,RM 不再需要向 TC 发送注册消息,而是把分支事务状态保存在了本地。TM 向 TC 发送提交或回滚消息后,RM 异步线程首先查出本地保存的未提交分支事务,然后向 TC 发送消息获取(本地分支事务)所在的全局事务状态,以决定是提交还是回滚本地事务。

这样优化后,RPC 次数减少了 50%,性能大幅提升。

3 RM 代码示例

以库存服务为例,RM 库存服务接口代码如下:

@LocalTCC
public interface StorageService {

/**
* 扣减库存
* @param xid 全局xid
* @param productId 产品id
* @param count 数量
* @return
*/
@TwoPhaseBusinessAction(name = "storageApi", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
boolean decrease(String xid, Long productId, Integer count);

/**
* 提交事务
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);

/**
* 回滚事务
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}

通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。在 try 阶段的方法(decrease方法)上有一个 @TwoPhaseBusinessAction 注解,这里定义了分支事务的 resourceId,commit 方法和 cancel 方法,useTCCFence 这个属性下一节再讲。

4 TCC 存在问题

TCC 模式中存在的三大问题是幂等、悬挂和空回滚。在 Seata1.5.1 版本中,增加了一张事务控制表,表名是 tcc_fence_log 来解决这个问题。而在上一节 @TwoPhaseBusinessAction 注解中提到的属性 useTCCFence 就是来指定是否开启这个机制,这个属性值默认是 false。

tcc_fence_log 建表语句如下(MySQL 语法):

CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`action_name` VARCHAR(64) NOT NULL COMMENT 'action name',
`status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
`gmt_modified` DATETIME(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

4.1 幂等

在 commit/cancel 阶段,因为 TC 没有收到分支事务的响应,需要进行重试,这就要分支事务支持幂等。

我们看一下新版本是怎么解决的。下面的代码在 TCCResourceManager 类:

@Override
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
String applicationData) throws TransactionException {
TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId);
//省略判断
Object targetTCCBean = tccResource.getTargetBean();
Method commitMethod = tccResource.getCommitMethod();
//省略判断
try {
//BusinessActionContext
BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId,
applicationData);
Object[] args = this.getTwoPhaseCommitArgs(tccResource, businessActionContext);
Object ret;
boolean result;
//注解 useTCCFence 属性是否设置为 true
if (Boolean.TRUE.equals(businessActionContext.getActionContext(Constants.USE_TCC_FENCE))) {
try {
result = TCCFenceHandler.commitFence(commitMethod, targetTCCBean, xid, branchId, args);
} catch (SkipCallbackWrapperException | UndeclaredThrowableException e) {
throw e.getCause();
}
} else {
//省略逻辑
}
LOGGER.info("TCC resource commit result : {}, xid: {}, branchId: {}, resourceId: {}", result, xid, branchId, resourceId);
return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;
} catch (Throwable t) {
//省略
return BranchStatus.PhaseTwo_CommitFailed_Retryable;
}
}

上面的代码可以看到,执行分支事务提交方法时,首先判断 useTCCFence 属性是否为 true,如果为 true,则走 TCCFenceHandler 类中的 commitFence 逻辑,否则走普通提交逻辑。

TCCFenceHandler 类中的 commitFence 方法调用了 TCCFenceHandler 类的 commitFence 方法,代码如下:

public static boolean commitFence(Method commitMethod, Object targetTCCBean,
String xid, Long branchId, Object[] args) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
if (tccFenceDO == null) {
throw new TCCFenceException(String.format("TCC fence record not exists, commit fence method failed. xid= %s, branchId= %s", xid, branchId),
FrameworkErrorCode.RecordAlreadyExists);
}
if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
LOGGER.info("Branch transaction has already committed before. idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
return true;
}
if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
}
return false;
}
return updateStatusAndInvokeTargetMethod(conn, commitMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_COMMITTED, status, args);
} catch (Throwable t) {
status.setRollbackOnly();
throw new SkipCallbackWrapperException(t);
}
});
}

从代码中可以看到,提交事务时首先会判断 tcc_fence_log 表中是否已经有记录,如果有记录,则判断事务执行状态并返回。这样如果判断到事务的状态已经是 STATUS_COMMITTED,就不会再次提交,保证了幂等。如果 tcc_fence_log 表中没有记录,则插入一条记录,供后面重试时判断。

Rollback 的逻辑跟 commit 类似,逻辑在类 TCCFenceHandler 的 rollbackFence 方法。

4.2 空回滚

如下图,账户服务是两个节点的集群,在 try 阶段账户服务 1 这个节点发生了故障,try 阶段在不考虑重试的情况下,全局事务必须要走向结束状态,这样就需要在账户服务上执行一次 cancel 操作。

fence-empty-rollback

Seata 的解决方案是在 try 阶段 往 tcc_fence_log 表插入一条记录,status 字段值是 STATUS_TRIED,在 Rollback 阶段判断记录是否存在,如果不存在,则不执行回滚操作。代码如下:

//TCCFenceHandler 类
public static Object prepareFence(String xid, Long branchId, String actionName, Callback<Object> targetCallback) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
LOGGER.info("TCC fence prepare result: {}. xid: {}, branchId: {}", result, xid, branchId);
if (result) {
return targetCallback.execute();
} else {
throw new TCCFenceException(String.format("Insert tcc fence record error, prepare fence failed. xid= %s, branchId= %s", xid, branchId),
FrameworkErrorCode.InsertRecordError);
}
} catch (TCCFenceException e) {
//省略
} catch (Throwable t) {
//省略
}
});
}

在 Rollback 阶段的处理逻辑如下:

//TCCFenceHandler 类
public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,
String xid, Long branchId, Object[] args, String actionName) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
// non_rollback
if (tccFenceDO == null) {
//不执行回滚逻辑
return true;
} else {
if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
LOGGER.info("Branch transaction had already rollbacked before, idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
return true;
}
if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
}
return false;
}
}
return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);
} catch (Throwable t) {
status.setRollbackOnly();
throw new SkipCallbackWrapperException(t);
}
});
}

updateStatusAndInvokeTargetMethod 方法执行的 sql 如下:

update tcc_fence_log set status = ?, gmt_modified = ?
where xid = ? and branch_id = ? and status = ? ;

可见就是把 tcc_fence_log 表记录的 status 字段值从 STATUS_TRIED 改为 STATUS_ROLLBACKED,如果更新成功,就执行回滚逻辑。

4.3 悬挂

悬挂是指因为网络问题,RM 开始没有收到 try 指令,但是执行了 Rollback 后 RM 又收到了 try 指令并且预留资源成功,这时全局事务已经结束,最终导致预留的资源不能释放。如下图:

fence-suspend

Seata 解决这个问题的方法是执行 Rollback 方法时先判断 tcc_fence_log 是否存在当前 xid 的记录,如果没有则向 tcc_fence_log 表插入一条记录,状态是 STATUS_SUSPENDED,并且不再执行回滚操作。代码如下:

public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,
String xid, Long branchId, Object[] args, String actionName) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
// non_rollback
if (tccFenceDO == null) {
//插入防悬挂记录
boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED);
//省略逻辑
return true;
} else {
//省略逻辑
}
return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);
} catch (Throwable t) {
//省略逻辑
}
});
}

而后面执行 try 阶段方法时首先会向 tcc_fence_log 表插入一条当前 xid 的记录,这样就造成了主键冲突。代码如下:

//TCCFenceHandler 类
public static Object prepareFence(String xid, Long branchId, String actionName, Callback<Object> targetCallback) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
//省略逻辑
} catch (TCCFenceException e) {
if (e.getErrcode() == FrameworkErrorCode.DuplicateKeyException) {
LOGGER.error("Branch transaction has already rollbacked before,prepare fence failed. xid= {},branchId = {}", xid, branchId);
addToLogCleanQueue(xid, branchId);
}
status.setRollbackOnly();
throw new SkipCallbackWrapperException(e);
} catch (Throwable t) {
//省略
}
});
}

注意:queryTCCFenceDO 方法 sql 中使用了 for update,这样就不用担心 Rollback 方法中获取不到 tcc_fence_log 表记录而无法判断 try 阶段本地事务的执行结果了。

5 总结

TCC 模式是分布式事务中非常重要的事务模式,但是幂等、悬挂和空回滚一直是 TCC 模式需要考虑的问题,Seata 框架在 1.5.1 版本完美解决了这些问题。

对 tcc_fence_log 表的操作也需要考虑事务的控制,Seata 使用了代理数据源,使 tcc_fence_log 表操作和 RM 业务操作在同一个本地事务中执行,这样就能保证本地操作和对 tcc_fence_log 的操作同时成功或失败。

· 阅读需 16 分钟

Seata 目前支持 AT 模式、XA 模式、TCC 模式和 SAGA 模式,之前文章更多谈及的是非侵入式的 AT 模式,今天带大家认识一下同样是二阶段提交的 TCC 模式。

什么是 TCC

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

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

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

img

Seata TCC 模式

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

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

public interface TccActionOne {
@TwoPhaseBusinessAction(name = "DubboTccActionOne", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);

public boolean commit(BusinessActionContext actionContext);

public boolean rollback(BusinessActionContext actionContext);
}

同样,在服务 B 定义该服务的一个 TCC 接口:

public interface TccActionTwo {
@TwoPhaseBusinessAction(name = "DubboTccActionTwo", commitMethod = "commit", rollbackMethod = "rollback")
public void prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "b") String b);

public void commit(BusinessActionContext actionContext);

public void rollback(BusinessActionContext actionContext);
}

在业务所在系统中开启全局事务并执行服务 A 和服务 B 的 TCC 预留资源方法:

@GlobalTransactional
public String doTransactionCommit(){
//服务A事务参与者
tccActionOne.prepare(null,"one");
//服务B事务参与者
tccActionTwo.prepare(null,"two");
}

以上就是使用 Seata TCC 模式实现一个全局事务的例子,可以看出,TCC 模式同样使用 @GlobalTransactional 注解开启全局事务,而服务 A 和服务 B 的 TCC 接口为事务参与者,Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。

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

img

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 模式

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

资源解析

资源解析即是把 TCC 接口进行解析并注册,前面说过,TCC 接口可以是 RPC,也可以是 JVM 内部调用,在 Seata TCC 模块有中一个 remoting 模块,该模块专门用于解析具有 TwoPhaseBusinessAction 注解的 TCC 接口资源:

img

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

public RemotingDesc parserRemotingServiceInfo(Object bean,String beanName,RemotingParser remotingParser){
RemotingDesc remotingBeanDesc=remotingParser.getServiceDesc(bean,beanName);
if(remotingBeanDesc==null){
return null;
}
remotingServiceMap.put(beanName,remotingBeanDesc);

Class<?> interfaceClass=remotingBeanDesc.getInterfaceClass();
Method[]methods=interfaceClass.getMethods();
if(remotingParser.isService(bean,beanName)){
try{
//service bean, registry resource
Object targetBean=remotingBeanDesc.getTargetBean();
for(Method m:methods){
TwoPhaseBusinessAction twoPhaseBusinessAction=m.getAnnotation(TwoPhaseBusinessAction.class);
if(twoPhaseBusinessAction!=null){
TCCResource tccResource=new TCCResource();
tccResource.setActionName(twoPhaseBusinessAction.name());
tccResource.setTargetBean(targetBean);
tccResource.setPrepareMethod(m);
tccResource.setCommitMethodName(twoPhaseBusinessAction.commitMethod());
tccResource.setCommitMethod(interfaceClass.getMethod(twoPhaseBusinessAction.commitMethod(),
twoPhaseBusinessAction.commitArgsClasses()));
tccResource.setRollbackMethodName(twoPhaseBusinessAction.rollbackMethod());
tccResource.setRollbackMethod(interfaceClass.getMethod(twoPhaseBusinessAction.rollbackMethod(),
twoPhaseBusinessAction.rollbackArgsClasses()));
// set argsClasses
tccResource.setCommitArgsClasses(twoPhaseBusinessAction.commitArgsClasses());
tccResource.setRollbackArgsClasses(twoPhaseBusinessAction.rollbackArgsClasses());
// set phase two method's keys
tccResource.setPhaseTwoCommitKeys(this.getTwoPhaseArgs(tccResource.getCommitMethod(),
twoPhaseBusinessAction.commitArgsClasses()));
tccResource.setPhaseTwoRollbackKeys(this.getTwoPhaseArgs(tccResource.getRollbackMethod(),
twoPhaseBusinessAction.rollbackArgsClasses()));
//registry tcc resource
DefaultResourceManager.get().registerResource(tccResource);
}
}
}catch(Throwable t){
throw new FrameworkException(t,"parser remoting service error");
}
}
if(remotingParser.isReference(bean,beanName)){
//reference bean, TCC proxy
remotingBeanDesc.setReference(true);
}
return remotingBeanDesc;
}

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

资源管理

1、资源注册

Seata TCC 模式的资源叫 TCCResource,其资源管理器叫 TCCResourceManager,前面讲过,当解析完 TCC 接口 RPC 资源后,如果是发起方,则会对其进行资源注册:

io.seata.rm.tcc.TCCResourceManager#registerResource

public void registerResource(Resource resource){
TCCResource tccResource=(TCCResource)resource;
tccResourceCache.put(tccResource.getResourceId(),tccResource);
super.registerResource(tccResource);
}

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

2、资源提交/回滚

io.seata.rm.tcc.TCCResourceManager#branchCommit

public BranchStatus branchCommit(BranchType branchType,String xid,long branchId,String resourceId,
String applicationData)throws TransactionException{
TCCResource tccResource=(TCCResource)tccResourceCache.get(resourceId);
if(tccResource==null){
throw new ShouldNeverHappenException(String.format("TCC resource is not exist, resourceId: %s",resourceId));
}
Object targetTCCBean=tccResource.getTargetBean();
Method commitMethod=tccResource.getCommitMethod();
if(targetTCCBean==null||commitMethod==null){
throw new ShouldNeverHappenException(String.format("TCC resource is not available, resourceId: %s",resourceId));
}
try{
//BusinessActionContext
BusinessActionContext businessActionContext=getBusinessActionContext(xid,branchId,resourceId,
applicationData);
// ... ...
ret=commitMethod.invoke(targetTCCBean,args);
// ... ...
return result?BranchStatus.PhaseTwo_Committed:BranchStatus.PhaseTwo_CommitFailed_Retryable;
}catch(Throwable t){
String msg=String.format("commit TCC resource error, resourceId: %s, xid: %s.",resourceId,xid);
LOGGER.error(msg,t);
return BranchStatus.PhaseTwo_CommitFailed_Retryable;
}
}

当 TM 决议二阶段提交,TC 会通过分支注册的的资源 ID 回调到对应参与者(即 TCC 接口发起方)服务中执行 TCC Resource 的 Confirm/Cancel 方法。

资源管理器中会根据 resourceId 在本地缓存找到对应的 TCCResource,同时根据 xid、branchId、resourceId、applicationData 找到对应的 BusinessActionContext 上下文,执行的参数就在上下文中。最后,执行 TCCResource 中获取 commit 的方法进行二阶段提交。

二阶段回滚同理类似。

事务处理

前面讲过,如果 TCC 接口时一个调用方,则会使用 Seata TCC 代理对调用方进行拦截处理,并在处理调用真正的 RPC 方法前对分支进行注册。

执行方法io.seata.spring.util.TCCBeanParserUtils#isTccAutoProxy除了对 TCC 接口资源进行解析,还会判断 TCC 接口是否为调用方,如果是调用方则返回 true:

io.seata.spring.annotation.GlobalTransactionScanner#wrapIfNecessary

img

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

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

public Object proceed(Method method,Object[]arguments,String xid,TwoPhaseBusinessAction businessAction,
Callback<Object> targetCallback)throws Throwable{
//Get action context from arguments, or create a new one and then reset to arguments
BusinessActionContext actionContext=getOrCreateActionContextAndResetToArguments(method.getParameterTypes(),arguments);
//Creating Branch Record
String branchId=doTccActionLogStore(method,arguments,businessAction,actionContext);
// ... ...
try{
// ... ...
return targetCallback.execute();
}finally{
try{
//to report business action context finally if the actionContext.getUpdated() is true
BusinessActionContextUtil.reportContext(actionContext);
}finally{
// ... ...
}
}
}

以上,在执行 TCC 接口一阶段之前,会调用 doTccActionLogStore 方法分支注册,同时还会将 TCC 相关信息比如参数放置在上下文,上面讲的资源提交/回滚就会用到这个上下文。

如何控制异常

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

如何处理空回滚

什么是空回滚?

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

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

img

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

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

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

如何处理幂等

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

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

img

如上图所示,参与者 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 方法预留的资源永远无法提交和释放了。

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

img

如上图所示,在执行参与者 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 方法执行成功。

作者简介

张乘辉,目前就职于蚂蚁集团,热爱分享技术,微信公众号「后端进阶」作者,技术博客(https://objcoding.com/)博主,Seata Committer,GitHub ID:objcoding。