跳到主要内容

· 阅读需 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 和 XA 三种事务模式。 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/)博主,GitHub ID:objcoding。

· 阅读需 10 分钟

Seata AT 模式是一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。

为什么要检查全局锁呢,这是由于 Seata AT 模式的事务隔离是建立在支事务的本地隔离级别基础之上的,在数据库本地隔离级别读已提交或以上的前提下,Seata 设计了由事务协调器维护的全局写排他锁,来保证事务间的写隔离,同时,将全局事务默认定义在读未提交的隔离级别上。

Seata 事务隔离级别解读

在讲 Seata 事务隔离级之前,我们先来回顾一下数据库事务的隔离级别,目前数据库事务的隔离级别一共有 4 种,由低到高分别为:

  1. Read uncommitted:读未提交
  2. Read committed:读已提交
  3. Repeatable read:可重复读
  4. Serializable:序列化

数据库一般默认的隔离级别为读已提交,比如 Oracle,也有一些数据的默认隔离级别为可重复读,比如 Mysql,一般而言,数据库的读已提交能够满足业务绝大部分场景了。

我们知道 Seata 的事务是一个全局事务,它包含了若干个分支本地事务,在全局事务执行过程中(全局事务还没执行完),某个本地事务提交了,如果 Seata 没有采取任务措施,则会导致已提交的本地事务被读取,造成脏读,如果数据在全局事务提交前已提交的本地事务被修改,则会造成脏写。

由此可以看出,传统意义的脏读是读到了未提交的数据,Seata 脏读是读到了全局事务下未提交的数据,全局事务可能包含多个本地事务,某个本地事务提交了不代表全局事务提交了。

在绝大部分应用在读已提交的隔离级别下工作是没有问题的,而实际上,这当中又有绝大多数的应用场景,实际上工作在读未提交的隔离级别下同样没有问题。

在极端场景下,应用如果需要达到全局的读已提交,Seata 也提供了全局锁机制实现全局事务读已提交。但是默认情况下,Seata 的全局事务是工作在读未提交隔离级别的,保证绝大多数场景的高效性。

全局锁实现

AT 模式下,会使用 Seata 内部数据源代理 DataSourceProxy,全局锁的实现就是隐藏在这个代理中。我们分别在执行、提交的过程都做了什么。

1、执行过程

执行过程在 StatementProxy 类,在执行过程中,如果执行 SQL 是 select for update,则会使用 SelectForUpdateExecutor 类,如果执行方法中带有 @GlobalTransactional or @GlobalLock注解,则会检查是否有全局锁,如果当前存在全局锁,则会回滚本地事务,通过 while 循环不断地重新竞争获取本地锁和全局锁。

io.seata.rm.datasource.exec.SelectForUpdateExecutor#doExecute

public T doExecute(Object... args) throws Throwable {
Connection conn = statementProxy.getConnection();
// ... ...
try {
// ... ...
while (true) {
try {
// ... ...
if (RootContext.inGlobalTransaction() || RootContext.requireGlobalLock()) {
// Do the same thing under either @GlobalTransactional or @GlobalLock,
// that only check the global lock here.
statementProxy.getConnectionProxy().checkLock(lockKeys);
} else {
throw new RuntimeException("Unknown situation!");
}
break;
} catch (LockConflictException lce) {
if (sp != null) {
conn.rollback(sp);
} else {
conn.rollback();
}
// trigger retry
lockRetryController.sleep(lce);
}
}
} finally {
// ...
}

2、提交过程

提交过程在 ConnectionProxy#doCommit方法中。

1)如果执行方法中带有@GlobalTransactional注解,则会在注册分支时候获取全局锁:

  • 请求 TC 注册分支

io.seata.rm.datasource.ConnectionProxy#register

private void register() throws TransactionException {
if (!context.hasUndoLog() || !context.hasLockKey()) {
return;
}
Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
null, context.getXid(), null, context.buildLockKeys());
context.setBranchId(branchId);
}
  • TC 注册分支的时候,获取全局锁

io.seata.server.transaction.at.ATCore#branchSessionLock

protected void branchSessionLock(GlobalSession globalSession, BranchSession branchSession) throws TransactionException {
if (!branchSession.lock()) {
throw new BranchTransactionException(LockKeyConflict, String
.format("Global lock acquire failed xid = %s branchId = %s", globalSession.getXid(),
branchSession.getBranchId()));
}
}

2)如果执行方法中带有@GlobalLock注解,在提交前会查询全局锁是否存在,如果存在则抛异常:

io.seata.rm.datasource.ConnectionProxy#processLocalCommitWithGlobalLocks

private void processLocalCommitWithGlobalLocks() throws SQLException {
checkLock(context.buildLockKeys());
try {
targetConnection.commit();
} catch (Throwable ex) {
throw new SQLException(ex);
}
context.reset();
}

GlobalLock 注解说明

从执行过程和提交过程可以看出,既然开启全局事务 @GlobalTransactional注解可以在事务提交前,查询全局锁是否存在,那为什么 Seata 还要设计多处一个 @GlobalLock注解呢?

因为并不是所有的数据库操作都需要开启全局事务,而开启全局事务是一个比较重的操作,需要向 TC 发起开启全局事务等 RPC 过程,而@GlobalLock注解只会在执行过程中查询全局锁是否存在,不会去开启全局事务,因此在不需要全局事务,而又需要检查全局锁避免脏读脏写时,使用@GlobalLock注解是一个更加轻量的操作。

如何防止脏写

先来看一下使用 Seata AT 模式是怎么产生脏写的:

注:分支事务执行过程省略其它过程。

业务一开启全局事务,其中包含分支事务A(修改 A)和分支事务 B(修改 B),业务二修改 A,其中业务一执行分支事务 A 先获取本地锁,业务二则等待业务一执行完分支事务 A 之后,获得本地锁修改 A 并入库,业务一在执行分支事务时发生异常了,由于分支事务 A 的数据被业务二修改,导致业务一的全局事务无法回滚。

如何防止脏写?

1、业务二执行时加 @GlobalTransactional注解:

注:分支事务执行过程省略其它过程。

业务二在执行全局事务过程中,分支事务 A 提交前注册分支事务获取全局锁时,发现业务业务一全局锁还没执行完,因此业务二提交不了,抛异常回滚,所以不会发生脏写。

2、业务二执行时加 @GlobalLock注解:

注:分支事务执行过程省略其它过程。

@GlobalTransactional注解效果类似,只不过不需要开启全局事务,只在本地事务提交前,检查全局锁是否存在。

2、业务二执行时加 @GlobalLock 注解 + select for update语句:

如果加了select for update语句,则会在 update 前检查全局锁是否存在,只有当全局锁释放之后,业务二才能开始执行 updateA 操作。

如果单单是 transactional,那么就有可能会出现脏写,根本原因是没有 Globallock 注解时,不会检查全局锁,这可能会导致另外一个全局事务回滚时,发现某个分支事务被脏写了。所以加 select for update 也有个好处,就是可以重试。

如何防止脏读

Seata AT 模式的脏读是指在全局事务未提交前,被其它业务读到已提交的分支事务的数据,本质上是Seata默认的全局事务是读未提交。

那么怎么避免脏读现象呢?

业务二查询 A 时加 @GlobalLock 注解 + select for update语句:

select for update语句会在执行 SQL 前检查全局锁是否存在,只有当全局锁完成之后,才能继续执行 SQL,这样就防止了脏读。

作者简介:

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

· 阅读需 11 分钟

在上一篇关于新版雪花算法的解析中,我们提到新版算法所做出的2点改变:

  1. 时间戳不再时刻追随系统时钟。
  2. 节点ID和时间戳互换位置。由原版的: 原版位分配策略 改成: 改进版位分配策略

有细心的同学提出了一个问题:新版算法在单节点内部确实是单调递增的,但是在多实例部署时,它就不再是全局单调递增了啊!因为显而易见,节点ID排在高位,那么节点ID大的,生成的ID一定大于节点ID小的,不管时间上谁先谁后。而原版算法,时间戳在高位,并且始终追随系统时钟,可以保证早生成的ID小于晚生成的ID,只有当2个节点恰好在同一时间戳生成ID时,2个ID的大小才由节点ID决定。这样看来,新版算法是不是错的?

这是一个很好的问题!能提出这个问题的同学,说明已经深入思考了标准版雪花算法和新版雪花算法的本质区别,这点值得鼓励!在这里,我们先说结论:新版算法的确不具备全局的单调递增性,但这不影响我们的初衷(减少数据库的页分裂)。这个结论看起来有点违反直觉,但可以被证明。

在证明之前,我们先简单回顾一下数据库关于页分裂的知识。以经典的mysql innodb为例,innodb使用B+树索引,其中,主键索引的叶子节点还保存了数据行的完整记录,叶子节点之间以双向链表的形式串联起来。叶子节点的物理存储形式为数据页,一个数据页内最多可以存储N条行记录(N与行的大小成反比)。如图所示: 数据页
B+树的特性要求,左边的节点应小于右边的节点。如果此时要插入一条ID为25的记录,会怎样呢(假设每个数据页只够存放4条记录)?答案是会引起页分裂,如图: 页分裂
页分裂是IO不友好的,需要新建数据页,拷贝转移旧数据页的部分记录等,我们应尽量避免。

理想的情况下,主键ID最好是顺序递增的(例如把主键设置为auto_increment),这样就只会在当前数据页放满了的时候,才需要新建下一页,双向链表永远是顺序尾部增长的,不会有中间的节点发生分裂的情况。

最糟糕的情况下,主键ID是随机无序生成的(例如java中一个UUID字符串),这种情况下,新插入的记录会随机分配到任何一个数据页,如果该页已满,就会触发页分裂。

如果主键ID由标准版雪花算法生成,最好的情况下,是每个时间戳内只有一个节点在生成ID,这时候算法的效果等同于理想情况的顺序递增,即跟auto_increment无差。最坏的情况下,是每个时间戳内所有节点都在生成ID,这时候算法的效果接近于无序(但仍比UUID的完全无序要好得多,因为workerId只有10位决定了最多只有1024个节点)。实际生产中,算法的效果取决于业务流量,并发度越低,算法越接近理想情况。

那么,换成新版算法又会如何呢?
新版算法从全局角度来看,ID是无序的,但对于每一个workerId,它生成的ID都是严格单调递增的,又因为workerId是有限的,所以最多可划分出1024个子序列,每个子序列都是单调递增的。
对于数据库而言,也许它初期接收的ID都是无序的,来自各个子序列的ID都混在一起,就像这样: 初期
如果这时候来了个worker1-seq2,显然会造成页分裂: 首次分裂
但分裂之后,有趣的事情发生了,对于worker1而言,后续的seq3,seq4不会再造成页分裂(因为还装得下),seq5也只需要像顺序增长那样新建页进行链接(区别是这个新页不是在双向链表的尾部)。注意,worker1的后续ID,不会排到worker2及之后的任意节点(因而不会造成后边节点的页分裂),因为它们总比worker2的ID小;也不会排到worker1当前节点的前边(因而不会造成前边节点的页分裂),因为worker1的子序列总是单调递增的。在这里,我们称worker1这样的子序列达到了稳态,意为这条子序列已经"稳定"了,它的后续增长只会出现在子序列的尾部,而不会造成其它节点的页分裂。

同样的事情,可以推广到各个子序列上。无论前期数据库接收到的ID有多乱,经过有限次的页分裂后,双向链表总能达到这样一个稳定的终态: 终态
到达终态后,后续的ID只会在该ID所属的子序列上进行顺序增长,而不会造成页分裂。该状态下的顺序增长与auto_increment的顺序增长的区别是,前者有1024个增长位点(各个子序列的尾部),后者只有尾部一个。

到这里,我们可以回答开头所提出的问题了:新算法从全局来看的确不是全局递增的,但该算法是收敛的,达到稳态后,新算法同样能达成像全局顺序递增一样的效果。


扩展思考

以上只提到了序列不停增长的情况,而实践生产中,不光有新数据的插入,也有旧数据的删除。而数据的删除有可能会导致页合并(innodb若发现相邻2个数据页的空间利用率都不到50%,就会把它俩合并),这对新算法的影响如何呢?

经过上面的流程,我们可以发现,新算法的本质是利用前期的页分裂,把不同的子序列逐渐分离开来,让算法不断收敛到稳态。而页合并则恰好相反,它有可能会把不同的子序列又合并回同一个数据页里,妨碍算法的收敛。尤其是在收敛的前期,频繁的页合并甚至可以让算法永远无法收敛(你刚分离出来我就又把它们合并回去,一夜回到解放前~)!但在收敛之后,只有在各个子序列的尾节点进行的页合并,才有可能破坏稳态(一个子序列的尾节点和下一个子序列的头节点进行合并)。而在子序列其余节点上的页合并,不影响稳态,因为子序列仍然是有序的,只不过长度变短了而已。

以seata的服务端为例,服务端那3张表的数据的生命周期都是比较短的,一个全局事务结束之后,它们就会被清除了,这对于新算法是不友好的,没有给时间它进行收敛。不过已经有延迟删除的PR在review中,搭配这个PR,效果会好很多。比如定期每周清理一次,前期就有足够的时间给算法进行收敛,其余的大部分时间,数据库就能从中受益了。到期清理时,最坏的结果也不过是表被清空,算法从头再来。

如果您希望把新算法应用到业务系统当中,请务必确保算法有时间进行收敛。比如用户表之类的,数据本就打算长期保存的,算法可以自然收敛。或者也做了延迟删除的机制,给算法足够的时间进行收敛。

如果您有更好的意见和建议,也欢迎跟seata社区联系!