SpringCloud&分布式事务
分布式和微服务的区别
分布式和微服务的区别
分布式和微服务,虽然经常一起出现,但它们描述的维度完全不同。用一句大白话总结就是:分布式是干活的方式,微服务是组织的架构。
1)分布式
分布式就是把任务分散到多台机器或节点上,这些节点一起干活,共同完成一个或多个共享的任务。
也就是说分布式的各个节点其实目标是一致的,之所以要分布式只是为了有更好的能力,能更快、更高效地承接任务,同时提升系统的容错性和可扩展性(例如,通过数据复制避免单点故障)。
比如常见的分布式文件系统、分布式数据库。
核心目标:让多台机器协作,像一个整体系统一样高效工作,同时处理大规模数据或计算。
2)微服务
微服务其实是一种软件设计方式或者说是架构风格,它主要是为了把一个大而全的服务,拆分成多个可以独立、松耦合的服务单元,为了让这些服务单元可以独立部署、运行、管理。
比如电商服务拆分成微服务,可以分为商品服务、用户服务、订单服务、库存服务等等,每个服务还可以使用不同的技术栈(如Java、Node.js)。
核心目标:让系统更容易迭代、扩展、独立部署。
所以分布式关注的是把任务分给多台机器(焦点在基础设施和资源分配上),微服务关注的是把业务拆成多个服务(焦点在软件设计和组织结构上)。
微服务通常运行在分布式系统上,但分布式不等于微服务。
比如我把一个庞大的单体应用复制 10 份,部署在 10 台服务器上,前面加个 Nginx 做负载均衡。这是分布式系统(因为用了多台机器),但它不是微服务(因为业务逻辑没拆分)。你可以叫它分布式单体。
反之,一个微服务架构如果所有服务都部署在单台机器上(虽不常见),它仍是微服务,但不是分布式。

单体应用,SOA以及微服务的区别
单体应用、SOA 和微服务架构都是不同的架构风格,适用于不同的情况:
- 单体应用,其对外是一个整体,所有的功能都打包在同一个应用上。这种架构风格方便测试和容易部署,不过有一个点,当发生故障的时候可能会导致服务整体不可用,容易发生单点故障,所以灵活性和可维护性并不是很高。
- SOA 是一种面向服务的架构风格,将系统划分为多个独立的服务。这些服务可以通过网络调用,并且可以实现跨平台、跨语言进行交互。SOA 的优点就是提供了跨系统的服务复用以及松耦合的交互方式,但是 SOA 的开发需要投入的工作很多,引入了 ESB (企业服务总线)和标准协议,增加了架构的复杂性,其算是一种分布式架构,但是其应用的拆分相对来说比较粗。
- 微服务架构是在 SOA 的基础上演变而来的,微服务架构进一步将系统为多个小型、独立的服务,每个服务都是一个单独的应用程序,可以采用独立部署的方式运行和扩展。相比 SOA,微服务架构具有更高的灵活性以及可维护性,因为其拆分会朝着细粒化的方向发展,这样的话单个服务维护也会更加方便,但微服务越多,其服务治理、也会更加复杂,且微服务架构落地也需要投入大量的工作,包括服务的定义、通信机制的选择等,其需要考虑的方面也会更多。
对比总结
- 单体应用:适合小型应用或初创项目,架构简单,但不易扩展和维护。
- SOA:适合大型企业应用,强调服务复用和标准化,但引入了 ESB 的复杂性。他的粒度相比微服务而言更粗。呀往往是中心化,强依赖于ESB,因此ESB容易成为单点瓶颈。
- 微服务架构:适合需要高扩展性、高可靠性和快速迭代的大型分布式应用,带来了管理和运维的复杂性。微服务粒度更细,通常是去中心化的,强调业务自治,团队自治
以下是一张关于三者之间对比的图:

微服务架构是如何运作的
在知道微服务架构运行的流程之前,我们可以先看一下微服务架构以下的组成,如下图所示

- 客户端:这个主要是来自不同设备的不同用户请求
- 身份提供商:这个是验证用户身份的工具,即鉴权,一般客户端在访问 API 网关之前都需要实现这一步,通过用户提供商获取安全令牌
- API 网关:主要用于处理客户端的请求,并把请求路由转发到对应的微服务
- 静态内容:指容纳系统的所有内容
- 管理:指微服务的配置、监控、运维等服务治理
- 服务发现:准确来说是服务的注册与发现,用于服务间的通信以及查找
- CDN: 内容交付网络,可以用于加速的网络服务
- 远程服务:即驻留在 IT 设备网络上的远程访问信息
服务雪崩
服务雪崩是指在微服务架构或分布式系统中,由于某个服务不可用或性能下降,导致依赖它的其他服务也出现连锁故障,最终使整个系统或大部分服务不可用的现象。
主要原因:
服务调用链复杂
- 在微服务架构中,各个服务之间存在大量的相互调用关系。一个服务的不可用或性能下降可能会导致依赖它的多个上游服务响应变慢,甚至出现请求堆积,从而影响到整个调用链。
- 示例:服务 A 调用服务 B,服务 B 调用服务 C。如果服务 C 发生故障且请求无法及时返回,服务 B 的请求将被阻塞,进而导致服务 A 的响应变慢或超时。
重试机制的反作用:
- 当服务调用失败时,通常会有重试机制以增加成功的概率。然而,在服务故障或超时情况下,重试机制可能会产生更多的请求,进一步加剧下游服务的压力,导致故障范围扩大。
- 示例:服务 A 调用服务 B,如果服务 B 出现超时,服务 A 可能会发起多次重试,这些重试请求可能会给服务 B 带来更大的压力,最终导致服务 B 的彻底崩溃。
服务雪崩的防范措施
使用熔断器:
- 原理:熔断器(如 Hystrix、Resilience4j)能够在检测到某个服务请求的失败率达到一定阈值时,自动中断对该服务的进一步调用,从而防止服务继续被拖垮。
- 优势:通过熔断器,可以快速阻止请求进入故障服务,从而减少服务调用链中其他服务受到的影响。
服务降级:
- 原理:当某个服务不可用时,可以提供降级方案,返回默认值或简化的结果,确保系统在部分功能不可用时仍能为用户提供基本服务。
- 示例:当库存服务出现故障时,可以返回一个库存数据缓存值或提示“库存信息暂时不可用”。
限流与隔离:
- 原理:通过限流(如令牌桶、漏桶算法)和隔离(如线程池隔离、信号量隔离),可以限制单个服务的请求数量,防止服务因流量过大而被压垮。
- 优势:限流和隔离可以控制服务的最大并发量,保护系统的关键服务在高并发场景下的稳定性。
服务降级
服务降级是一种在分布式系统和微服务架构中常用的容错机制,用于在系统压力过大或部分服务出现故障时,暂时减少或关闭某些不必要的功能,从而确保核心功能的正常运行,避免系统崩溃。通过降级,可以提高系统的容错性和可用性。
服务降级的触发场景
服务调用超时或失败:
- 当某个服务的调用时间超过了设定的阈值,或者服务多次调用失败时,可以触发降级机制,返回预设的降级响应,避免长时间等待。
- 示例:当支付服务超时时,可以返回“支付服务暂时不可用,请稍后再试”的提示。
系统负载过高:
- 当系统的负载过高(如 CPU 使用率、内存占用率等)时,可以主动降级某些非核心功能,释放系统资源,确保核心业务的正常运行。
- 示例:在电商促销活动期间,订单服务可以降级不实时显示推荐商品,只展示基础订单信息,以减轻系统负担。
下游服务不可用:
- 如果下游依赖服务不可用或者响应时间过长,可以通过降级机制,返回缓存数据或默认数据,避免请求继续传播,影响用户体验。
- 示例:在查询库存服务不可用时,可以返回“库存信息暂时不可用”的缓存数据。
实现服务降级的常用方式
Fallback(降级回调方法):在调用远程服务时,设置一个降级回调方法,当服务调用失败或超时时,直接执行降级方法,返回预设的响应。
- 使用场景:适用于基于 Hystrix 或 Resilience4j 的系统,可以在
@HystrixCommand注解中指定fallbackMethod。 - 示例:在调用订单服务时,如果订单服务不可用,可以执行
getOrderFallback方法,返回“订单信息暂时不可用”的提示。
使用缓存数据:当服务不可用时,可以返回最近一次的缓存数据,保证用户体验的稳定性。
- 使用场景:适用于查询类请求,如商品详情、用户信息等。通过缓存减少对下游服务的依赖。
- 示例:当库存服务不可用时,可以返回前一次查询的库存数据,避免对用户展示“库存不可用”的错误信息。
限流降级:当系统流量超过某个阈值时,对部分非核心请求进行限流或直接拒绝,保护核心服务的可用性。
- 使用场景:适用于高并发场景,通过限流来避免系统过载,如在 API 网关层面对某些请求进行限流。
- 示例:在秒杀活动中,对某些促销请求进行限流,只允许部分用户参与活动,避免系统被瞬间流量冲垮。
服务降级的实现框架与工具
Hystrix:
- 简介:Hystrix 是 Netflix 开源的熔断器和降级框架,通过
@HystrixCommand注解可以方便地实现服务的降级逻辑。 - 使用场景:适用于微服务架构中的服务降级和熔断处理,特别是在基于 Spring Cloud 的应用中。
- 替代品:Hystrix 已经进入维护模式,推荐使用 Resilience4j 作为替代方案。
Resilience4j:
- 简介:Resilience4j 是一个轻量级的容错框架,支持熔断、限流、降级等功能,采用函数式编程风格,适合与 Spring Boot 2.x 集成。
- 优势:更高效的性能,更现代化的设计,支持异步编程,适合在新的微服务项目中使用。
- 使用场景:可以通过配置降级方法实现服务降级,在请求失败或超时时提供备用响应。
Sentinel:
- 简介:Sentinel 是阿里巴巴开源的服务限流降级框架,提供流量控制、熔断降级、系统自适应保护等功能。
- 优势:与 Spring Cloud Alibaba 生态集成良好,适用于国内市场,功能丰富且易于配置。
- 使用场景:在流量高峰期或微服务系统中,通过 Sentinel 实现流量控制和服务降级,提升系统的稳定性。
电商网站中的降级:
- 在电商平台的促销活动中,流量激增可能导致系统超负荷运行。通过降级,可以临时关闭一些非核心的功能(如推荐商品、商品评论),以保证订单、支付等核心功能的正常运行。
金融系统中的降级:
- 在金融系统中,当某个外部支付渠道不可用时,可以提供备用的支付方式或提示用户使用其他支付渠道。这样即使某个服务暂时不可用,用户也能继续使用系统的其他功能。
消息推送系统中的降级:
- 在消息推送系统中,当消息发送量激增时,可以通过降级暂时关闭某些非关键的推送类型,如营销信息推送,优先保证系统通知类消息的发送。
服务熔断
服务熔断指的是当某个服务的调用失败率持续升高时,通过中断对该服务的请求,防止系统资源被不断消耗,进而保护整个系统不受影响。熔断机制的灵感来源于电路熔断器(保险丝),在出现异常时,通过快速切断服务调用,避免故障进一步扩散。
服务熔断的流程
当一个服务在一段时间内连续出现失败(如超时、请求错误等)并且失败率超过设定的阈值时,熔断器将切换到打开状态,暂时中断对该服务的调用请求。这样可以避免进一步的资源浪费和请求堆积。
经过一段时间后,熔断器会自动进入半开状态,尝试恢复调用,确保服务在故障恢复后,熔断器切换到关闭状态,反之继续打开。。
服务熔断的常见触发条件
- 请求失败率高:当一个服务在设定的时间窗口内,连续多次请求失败(如超时、异常、HTTP 5xx 错误等),并且失败率超过设定阈值时,熔断器会自动触发,进入打开状态。
- 请求响应时间长:如果一个服务的响应时间长,导致调用超时,并且这种情况在一定时间内多次发生,熔断器也会触发熔断,阻止继续发送请求。
- 服务不可达:当服务完全不可访问(如网络故障或服务宕机),熔断器可以直接切断请求,快速返回错误,避免进一步的资源浪费。
熔断器的三种状态
Closed(关闭状态):
- 描述:熔断器在正常状态下处于关闭状态,所有请求都会正常发往目标服务。
- 状态转换:当服务调用失败次数或失败率达到阈值时,熔断器会从
Closed状态变为Open状态。
Open(打开状态):
- 描述:当熔断器处于打开状态时,Hystrix 会阻断所有对目标服务的请求,直接返回降级结果。
- 状态转换:经过一段时间后,熔断器会自动进入
Half-Open状态,尝试发送部分请求,以判断目标服务是否恢复。
Half-Open(半开状态):
- 描述:在半开状态下,部分请求可以尝试发往目标服务。如果这些请求成功率达到设定阈值,熔断器会关闭,恢复正常调用。
- 状态转换:如果半开状态下的请求失败率仍然很高,则熔断器会重新打开。
熔断器的常见实现框架
Hystrix:
- 简介:Hystrix 是 Netflix 开源的熔断器框架,能够对服务调用进行隔离、熔断和降级处理。Hystrix 使用
@HystrixCommand注解,可以方便地定义熔断逻辑。 - 使用场景:适用于基于 Spring Cloud 的微服务架构,提供了丰富的熔断策略和配置选项。
- 现状:Hystrix 已经进入维护模式,推荐使用 Resilience4j 作为替代。
Resilience4j:
- 简介:Resilience4j 是一个轻量级的容错框架,支持熔断、重试、限流等功能,采用 Java 8 的函数式编程风格,更加现代化。
- 使用场景:适用于需要高性能和异步处理的微服务系统,可以与 Spring Boot 2.x 集成使用。
- 优势:相较于 Hystrix,Resilience4j 性能更高,配置更加简洁。
Sentinel:
- 简介:Sentinel 是阿里巴巴开源的流量控制与熔断降级框架,能够实时监控服务的请求量、响应时间,并根据配置触发熔断。
- 使用场景:适用于国内市场,特别是使用 Spring Cloud Alibaba 生态的项目中。
- 优势:支持实时的监控和流量控制,提供丰富的可视化配置。
分布式事务
什么是分布式事务呢?在分布式系统中,多个不同的服务或者数据库之间需要执行一组操作,这些操作在不同的系统或者节点上执行,分布式事务要求这一组操作要么同时执行成功,要么同时执行失败,从而确保分布式系统中数据的一致性。
一般分为强一致性方案(追求严格ACID,但性能可能差)和最终一致性方案(允许中间状态不一致,但最终会收敛,性能更好,适合高并发场景)。

2PC/XA(强一致性)

2PC,Two-phase commit protocol,即两阶段提交协议。它引入了一个事务协调者角色,来管理各个参与者(就是各数据库资源)。
整体分为两个阶段,分别是准备阶段和提交/回滚阶段。
-
第一阶段:由事务协调者给每个参与者发送准备命令,每个参与者收到命令之后会执行相关事务操作,可以认为除了事务的提交啥都做了。然后每个参与者会返回响应告知协调者自己是否准备成功。
-
协调者收到每个参与者的响应之后,根据收集的响应,如果有一个参与者响应准备失败那么就向所有参与者发送回滚命令,反之发送提交命令。
事务协调者在第一阶段未收到个别参与者的响应,则等待一定时间就会认为事务失败,会发送回滚命令,所以在 2PC 中事务协调者有超时机制。
2PC的优缺点:
- 优点就是能利用数据库自身的功能进行本地事务的提交和回滚,不需要我们自己实现,不侵入业务逻辑
- 缺点就是:同步阻塞,单点故障和数据不一致
同步阻塞
在第一阶段执行了准备命令后,我们每个本地资源都处于锁定状态,因为除了事务的提交之外啥都做了。
所以这时候如果本地的其他请求要访问同一个资源,比如要修改商品表 id 等于 100 的那条数据,那么此时是被阻塞住的,必须等待前面事务的完结,收到提交/回滚命令执行完释放资源后,这个请求才能得以继续。
所以假设这个分布式事务涉及到很多参与者,然后有些参与者处理又特别复杂,特别慢,那么那些处理快的节点也得等着,所以说效率有点低。
单点故障
这个单点就是协调者,如果协调者挂了整个事务就执行不下去了。
如果协调者在发送准备命令前挂了还行,毕竟每个资源都还未执行命令,那么资源是没被锁定的。
可怕的是在发送完准备命令之后挂了,这时候每个本地资源都执行完处于锁定状态了,都杵着了,这就很僵硬了,如果是某个热点资源都阻塞了,这估计就要完蛋了。
数据不一致
因为协调者和参与者之间的交流是经过网络的,而网络有时候就会抽风的或者发生局部网络异常。
那么就有可能导致某些参与者无法收到协调者的请求,而某些收到了。比如是提交请求,然后那些收到命令的参与者就提交事务了,此时就产生了数据不一致的问题。
Mysql XA
2PC这种思想需要依赖于Mysql的XA规范来实现,只有Innodb支持
简单的说就是要先定义一个全局唯一的 XID,然后告知每个事务分支要进行的操作。
可以看到图中执行了两个操作,分别是改名字和插入日志,等于先注册下要做的事情,通过 XA START XID 和 XA END XID 来包裹要执行的 SQL。

然后需要发送准备命令,来执行第一阶段,也就是除了事务的提交啥都干了的阶段。

然后根据准备的情况来选择执行提交事务命令还是回滚事务命令。

基本上就是这么个流程,不过 MySQL XA 的性能不高这点是需要注意的。
3PC(三阶段提交,强一致性)

3PC 的引入是为了解决 2PC 同步阻塞和减少数据不一致的情况。
3PC 也就是多了一个阶段,一个询问的阶段,分别是准备、预提交和提交这三个阶段。
准备阶段单纯就是协调者去访问参与者,类似于你还好吗?能接请求不。
预提交其实就是 2PC 的准备阶段,除了事务的提交啥都干了。
提交阶段和 2PC 的提交一致。

3PC 多了一个阶段其实就是在执行事务之前来确认参与者是否正常,防止个别参与者不正常的情况下,其他参与者都执行了事务,锁定资源。
出发点是好的,但是绝大部分情况下肯定是正常的,所以每次都多了一个交互阶段就很不划算。
然后 3PC 在参与者处也引入了超时机制,这样在协调者挂了的情况下,如果已经到了提交阶段了,参与者等半天没收到协调者的情况的话就会自动提交事务。
不过万一协调者发的是回滚命令呢?你看这就出错了,数据不一致了。
还有维基百科上说 2PC 参与者准备阶段之后,如果协调者挂了,参与者是无法得知整体的情况的,因为大局是协调者掌控的,所以参与者相互之间的状况它们不清楚。
而 3PC 经过了第一阶段的确认,即使协调者挂了参与者也知道自己所处预提交阶段是因为已经得到准备阶段所有参与者的认可了。
简单的说就像加了个围栏,使得各参与者的状态得以统一。
从上面已经知晓了 2PC 是一个强一致性的同步阻塞协议,性能已经是比较差的了。
而 3PC 的出发点是为了解决 2PC 的缺点,但是多了一个阶段就多了一次通讯的开销,而且是绝大部分情况下无用的通讯。
虽说引入参与者超时来解决协调者挂了的阻塞问题,但是数据还是会不一致。
可以看到 3PC 的引入并没什么实际突破,而且性能更差了,所以实际只有 2PC 的落地实现。
再提一下,2PC 还是 3PC 都是协议,可以认为是一种指导思想,和真正的落地还是有差别的。
TCC(Try-Confirm-Cancel,柔性事务,它介于强一致性与最终一致性之间)

不管是 2PC 还是 3PC 都是依赖于数据库的事务提交和回滚。
而有时候一些业务它不仅仅涉及到数据库,可能是发送一条短信,也可能是上传一张图片。
所以说事务的提交和回滚就得提升到业务层面而不是数据库层面了,而 TCC 就是一种业务层面或者是应用层的两阶段提交。
TCC 分为指代 Try、Confirm、Cancel ,也就是业务层面需要写对应的三个方法,主要用于跨数据库、跨服务的业务操作的数据一致性问题。
TCC 分为两个阶段,第一阶段是资源检查预留阶段即 Try,第二阶段是提交或回滚,如果是提交的话就是执行真正的业务操作,如果是回滚则是执行预留资源的取消,恢复初始状态。
比如有一个扣款服务,我需要写 Try 方法,用来冻结扣款资金,还需要一个 Confirm 方法来执行真正的扣款,最后还需要提供 Cancel 来进行冻结操作的回滚,对应的一个事务的所有服务都需要提供这三个方法。
可以看到本来就一个方法,现在需要膨胀成三个方法,所以说 TCC 对业务有很大的侵入,像如果没有冻结的那个字段,还需要改表结构。
我们来看下流程。

虽说对业务有侵入,但是 TCC 没有资源的阻塞,每一个方法都是直接提交事务的,如果出错是通过业务层面的 Cancel 来进行补偿,所以也称补偿性事务方法。
这里有人说那要是所有人 Try 都成功了,都执行 Comfirm 了,但是个别 Confirm 失败了怎么办?
这时候只能是不停地重试调失败了的 Confirm 直到成功为止,如果真的不行只能记录下来,到时候人工介入了。
TCC 的注意点
这几个点很关键,在实现的时候一定得注意了。

幂等问题,因为网络调用无法保证请求一定能到达,所以都会有重调机制,因此对于 Try、Confirm、Cancel 三个方法都需要幂等实现,避免重复执行产生错误。
空回滚问题,指的是 Try 方法由于网络问题没收到超时了,此时事务管理器就会发出 Cancel 命令,那么需要支持 Cancel 在未执行 Try 的情况下能正常的 Cancel。
悬挂问题,这个问题也是指 Try 方法由于网络阻塞超时触发了事务管理器发出了 Cancel 命令,但是执行了 Cancel 命令之后 Try 请求到了,你说气不气。
这都 Cancel 了你来个 Try,对于事务管理器来说这时候事务已经是结束了的,这冻结操作就被“悬挂”了,所以空回滚之后还得记录一下,防止 Try 的再调用。
TCC 变体
上面我们说的是通用型的 TCC,它需要改造以前的实现,但是有一种情况是无法改造的,就是调用的是别的公司的接口。
没有 Try 的 TCC
比如坐飞机需要换乘,换乘的又是不同的航空公司,比如从 A 飞到 B,再从 B 飞到 C,只有 A - B 和 B - C 都买到票了才有意义。
这时候的选择就没得 Try 了,直接调用航空公司的买票操作,当两个航空公司都买成功了那就直接成功了,如果某个公司买失败了,那就需要调用取消订票接口。
也就是在第一阶段直接就执行完整个业务操作了,所以要重点关注回滚操作,如果回滚失败得有提醒,要人工介入等。
这其实就是 TCC 的思想。

异步 TCC
这 TCC 还能异步?其实也是一种折中,比如某些服务很难改造,并且它又不会影响主业务决策,也就是它不那么重要,不需要及时的执行。
这时候可以引入可靠消息服务,通过消息服务来替代个别服务来进行 Try、Confirm、Cancel 。
Try 的时候只是写入消息,消息还不能被消费,Confirm 就是真正发消息的操作,Cancel 就是取消消息的发送。
这可靠消息服务其实就类似于等下要提到的事务消息,这个方案等于糅合了事务消息和 TCC。
TCC 小结
可以看到 TCC 是通过业务代码来实现事务的提交和回滚,对业务的侵入较大,它是业务层面的两阶段提交,。
它的性能比 2PC 要高,因为不会有资源的阻塞,并且适用范围也大于 2PC,在实现上要注意上面提到的几个注意点。
它是业界比较常用的分布式事务实现方式,而且从变体也可以得知,还是得看业务变通的,不是说你要用 TCC 一定就得死板的让所有的服务都改造成那三个方法。
Saga

事务消息(最终一致性)

像 RocketMQ 这类消息中间件是提供事务消息的,简单而言在事务开始时会发个半消息给 broker,此时消息对消费者不可见,如果本地事务提交了,半消息也会被提交,此时消费者可见,也就可以消费了。
broker 如果很长时间没等到半消息提交或者回滚的请求,则会反查生产者,这样就能保证事务的一致性了。
Seata

Seata(Simple Extensible Autonomous Transaction Architecture) 是阿里巴巴开源的一款分布式事务解决方案。
其主要是为了解决分布式系统中全局事务的一致性问题。Seata 提供了多种事务模式,包括 AT、TCC、Saga 以及 XA 模式。
在 Seata 中有三个很重要的角色:事务协调者(TC)、事务管理者(TM)以及事务的作业管理器(RM)。
事务协调器(Transaction Coordinator,TC):TC 负责管理全局事务的生命周期,记录全局事务和分支事务的状态,并协调全局事务的提交和回滚。TC 是 Seata 的中心控制器,所有的分布式事务请求都会通过 TC 进行管理。
- 作用:TC 保证各个分支事务在全局事务中的状态一致性。它记录每个事务分支的状态信息,并在发生异常时向相关分支发送回滚命令。
事务管理器(Transaction Manager,TM):TM 负责定义全局事务的边界,即启动、提交、回滚全局事务。TM 通常嵌入在业务服务中,用于向 TC 发起全局事务的创建和提交请求。
- 作用:通过
@GlobalTransactional注解或 API,TM 标记某个业务操作为全局事务。TM 在业务逻辑执行过程中负责与 TC 通信,以确定事务的最终状态。
资源管理器(Resource Manager,RM):RM 负责管理本地资源(如数据库),以及分支事务的注册、提交、回滚。RM 的核心作用是对本地数据库进行事务操作,并将分支事务的状态通知给 TC。
- 作用:在分支事务执行时,RM 通过数据库代理层来记录数据快照,以便在事务回滚时能够还原数据。同时,RM 负责向 TC 注册分支事务,并在接收到 TC 的命令时执行数据的提交或回滚操作。
Seata 的事务执行流程
大致流程简述如下:
- TM 向 TC 发起全局事务创建,TC 返回 XID。
- TM 调用业务方法,业务方法执行过程中,RM 向 TC 注册分支事务。
- RM 执行数据库操作并生成回滚日志,同时提交本地事务。
- 业务方法执行结束后,TM 向 TC 发送全局提交或回滚请求。
- TC 收到请求后,通知 RM 执行提交或回滚操作。
- RM 执行提交或回滚,并将结果通知 TC。
- TC 记录事务状态为提交完成或回滚完成。
Seata 的事务执行流程主要包括 全局事务的创建、分支事务的注册、事务的提交或回滚 等步骤。具体流程如下:
全局事务的创建(Transaction Start)
事务管理器(TM) 通过在业务方法上使用 @GlobalTransactional 注解,标识该方法为全局事务。业务方法调用时,TM 会向 事务协调器(TC) 发起全局事务的创建请求。
TC 负责为该全局事务生成一个唯一的 全局事务 ID(XID),并返回给 TM。XID 是全局事务的唯一标识,用于后续跟踪和管理这个事务的所有操作。
分支事务的注册(Branch Register)
分支事务 是全局事务的一部分,通常对应业务方法中具体的数据库操作(如新增、更新、删除)。
业务方法 执行时,会调用数据库操作(如 INSERT、UPDATE)。Seata 的 资源管理器(RM) 通过代理数据库操作来记录数据的前后状态,并生成回滚日志。
在执行数据库操作之前,RM 会将该数据库操作注册为分支事务,并向 TC 发起分支注册请求。
注册信息包括:
- 全局事务 ID(XID)
- 资源 ID(如数据库表名)
- 分支事务的类型(如 AT 模式的 SQL 操作)
- 数据的锁定信息(用于行锁管理)
TC 接收到分支事务注册请求后,会记录该分支事务的信息,并将注册成功的结果返回给 RM。
业务方法的执行与本地事务提交(Local Transaction Commit)
分支事务注册成功后,RM 执行数据库操作,更新数据并将修改的状态保存到数据库中。
在完成操作后,本地事务提交,此时本地事务的数据已经写入数据库,但全局事务尚未提交。为了支持回滚,回滚日志(undo_log) 也会存储在数据库中,以记录数据修改前的状态。
全局事务的提交或回滚(Global Commit/Rollback)
在业务逻辑执行完成后,全局事务会进入提交或回滚的阶段,具体流程如下:
全局提交流程:
- 如果业务逻辑正常执行,TM 向 TC 发起全局提交请求。
- TC 根据 XID 确认所有分支事务状态正常后,向所有参与的 RM 发送分支提交请求。
- RM 接收到分支提交请求后,执行数据库操作的最终提交(实际上,AT 模式下的数据已经在本地事务阶段提交,分支提交请求主要是确认无误)。
- TC 收到所有分支提交成功的响应后,标记全局事务为提交完成,并释放全局锁。
全局回滚流程:
- 如果业务逻辑执行过程中发生异常,TM 向 TC 发起全局回滚请求。
- TC 向所有参与的 RM 发送分支回滚请求。
- RM 在接收到分支回滚请求后,根据
undo_log中记录的数据快照,将数据库恢复到执行操作之前的状态,确保数据一致性。 - TC 收到所有分支回滚完成的响应后,标记全局事务为回滚完成,并释放相关的全局锁。
全局事务的结束(Transaction End)
无论是全局事务的提交还是回滚,当 TC 确认所有分支事务的操作完成后,会标记该全局事务的状态为 COMMITTED 或 ROLLED BACK,并在事务日志中记录事务的结束状态。
事务结束后,Seata 会清理 undo_log 表中的数据,防止日志表占用过多的数据库存储空间。
Seata 的数据一致性保证机制
- 回滚日志机制(Undo Log):在 AT 模式下,Seata 会在本地事务执行前后记录
undo_log,即数据操作前的快照信息。回滚时,Seata 读取undo_log并恢复数据,确保事务回滚操作的正确性。 - 全局锁机制:Seata 使用全局锁来管理分支事务对数据库行的并发访问,确保同一行数据在同一时刻只能被一个全局事务修改。通过 TC 管理全局锁,可以防止多事务并发修改同一数据行时的冲突。
- 两阶段提交(2PC):Seata 的事务提交过程遵循两阶段提交协议:
- 第一阶段:RM 提交本地事务,同时记录
undo_log,注册分支事务到 TC。 - 第二阶段:TC 根据全局事务的状态决定提交或回滚,并通知所有 RM 执行相应操作。
- 第一阶段:RM 提交本地事务,同时记录
AT模式
AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。
简单来说:
- 第一阶段
- TM向TC开启全局事务
- TM调用RM执行业务
- RM执行前向TC注册分支事务
- RM执行数据库操作,RM会拦截具体的执行操作,并在记录操作前后的数据快照。RM执行完后会立刻提交本地事务,本地事务提交前需要向TC获取全局锁,全局锁记录了全局事务ID,操作的表,以及操作的数据。同一个表的数据只能由持有全局锁的全局事务提交
- RM向TC报告事务提交状态
- 第二阶段
- TM向TC发起全局事务提交
- TC检查该全局事务下RM的执行状态
- 若都执行成功,则全局提交,删除undolog,释放各RM持有的全局锁
- 若有一个执行失败,则各RM会根据各自的undolog做事务回滚
AT模式解决了XA模式下锁持有时间过长,导致其他业务被阻塞的场景。性能要好很多。
同时AT模式通过全局锁,确保同一个表的数据只能被一个全局全局事务提交,避免了出现脏读或者是回滚时覆盖其他事务已提交的数据。
但一些极端情况下AT模式仍有问题,因为只有seata管理的事务才会获取全局锁,当项目中只有分布式事务才会被seata管理,对于一些服务内部的操作可能不会交给seata管理,那他提交数据就无须获取全局锁。就可能会导致双方在操作同一个数据时,可能会出现一些问题。
针对这一点,seata的解决方案是,undolog会记录操作前后的数据快照,操作前的数据用于回滚,操作后的数据主要用于判断是否还有其他事务对该数据进行了修改,如果有的话此时可能会记录错误,或者交由人工处理
使用很简单,代码中通过 @GlobalTransactional 注解即可自动管理事务。
@GlobalTransactional
public void purchase(String userId, String commodityCode, int count, int money) {
jdbcTemplateA.update("update stock_tbl set count = count - ? where commodity_code = ?", new Object[] {count, commodityCode});
jdbcTemplateB.update("update account_tbl set money = money - ? where user_id = ?", new Object[] {money, userId});
}
AT 模式的全局事务是由以下三部分协同完成的:
- 事务管理器(TM):管理全局事务的生命周期,负责标识事务的开始和结束。
- 事务协调器(TC):负责管理全局事务的状态和分支事务的提交、回滚。
- 资源管理器(RM):管理本地事务资源和数据回滚,负责执行具体的数据库操作,并向 TC 注册分支事务。
写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
本地锁是Mysql的锁,比如行锁。而全局锁是由Seata在TC里维护的逻辑锁
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。
tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。

tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
AT 工作机制示例
以一个示例来说明整个 AT 分支的工作过程。
业务表:product
| Field | Type | Key |
|---|---|---|
| id | bigint(20) | PRI |
| name | varchar(100) | |
| since | varchar(100) |
AT 分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
一阶段
过程:
1)解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息。
2)查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';
得到前镜像:
| id | name | since |
|---|---|---|
| 1 | TXC | 2014 |
3)执行业务 SQL:更新这条记录的 name 为 'GTS'。
4)查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, since from product where id = 1;
得到后镜像:
| id | name | since |
|---|---|---|
| 1 | GTS | 2014 |
5)插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
6)提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
7)本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
8)将本地事务提交的结果上报给 TC。
二阶段-回滚
1)收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
2)通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
3)数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
4)根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
5)提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
二阶段-提交
1)收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
2)异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
TCC

TCC 是一种侵入式的分布式事务解决方案,需要业务系统自行实现 Try,Confirm,Cancel 三个操作,对业务系统有着非常大的入侵性,设计相对复杂。整体是 两阶段提交 的模型。
全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
-
一阶段 prepare 行为
- TM向TC开启全局事务
- TM调用RM执行业务
- RM在执行前向TC完成注册
- RM执行try操作,并向TC汇报执行结果
-
二阶段 commit 或 rollback 行为
- TM向TC申请提交事务
- 如果RM的try都执行成功,则会执行confirm操作
- 若有一个失败,则执行cancel
在两阶段提交协议中,资源管理器(RM, Resource Manager)需要提供“准备”、“提交”和“回滚” 3 个操作;而事务管理器(TM, Transaction Manager)分 2 阶段协调所有资源管理器,在第一阶段询问所有资源管理器“准备”是否成功,如果所有资源均“准备”成功则在第二阶段执行所有资源的“提交”操作,否则在第二阶段执行所有资源的“回滚”操作,保证所有资源的最终状态是一致的,要么全部提交要么全部回滚。
资源管理器有很多实现方式,其中 TCC(Try-Confirm-Cancel)是资源管理器的一种服务化的实现;TCC 是一种比较成熟的分布式事务解决方案,可用于解决跨数据库、跨服务业务操作的数据一致性问题;TCC 其 Try、Confirm、Cancel 3 个方法均由业务编码实现,故 TCC 可以被称为是服务化的资源管理器。
TCC 的 Try 操作作为一阶段,负责资源的检查和预留;Confirm 操作作为二阶段提交操作,执行真正的业务;Cancel 是二阶段回滚操作,执行预留资源的取消,使资源回到初始状态。
基本使用
区别于在 AT 模式直接使用数据源代理来屏蔽分布式事务细节,业务方需要自行定义 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);
}
Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。在业务接口中核心的注解是 @TwoPhaseBusinessAction,表示当前方法使用 TCC 模式管理事务提交,并标明了 Try,Confirm,Cancel 三个阶段。name属性,给当前事务注册了一个全局唯一的的 TCC bean name。同时 TCC 模式的三个执行阶段分别是:
- Try 阶段,预定操作资源(Prepare) 这一阶段所以执行的方法便是被
@TwoPhaseBusinessAction所修饰的方法。如示例代码中的prepare方法。 - Confirm 阶段,执行主要业务逻辑(Commit) 这一阶段使用
commitMethod属性所指向的方法,来执行Confirm 的工作。 - Cancel 阶段,事务回滚(Rollback) 这一阶段使用
rollbackMethod属性所指向的方法,来执行 Cancel 的工作。
其次,可以在 TCC 模式下使用 BusinessActionContext 在事务上下文中传递查询参数。如下属性:
xid全局事务idbranchId分支事务idactionName分支资源id,(resource id)actionContext业务传递的参数,可以通过@BusinessActionContextParameter来标注需要传递的参数。
在定义好 TCC 接口之后,我们可以像 AT 模式一样,通过 @GlobalTransactional 开启一个分布式事务。
@GlobalTransactional
public String doTransactionCommit(){
tccActionOne.prepare(null,"one");
tccActionTwo.prepare(null,"two");
}
注意,如果 TCC 参与者是本地 bean(非远程RPC服务),本地 TCC bean 还需要在接口定义中添加 @LocalTCC 注解,比如,
@LocalTCC
public interface TccActionTwo {
@TwoPhaseBusinessAction(name = "TccActionTwo", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
TCC的问题在于幂等、空回滚、业务悬挂。
幂等在于,由于网络等情况,业务中肯定会有重试机制,那么try、confirm、cancel这些方法就都需要实现幂等
空回滚在于有可能由于网络问题,或者是服务本身的问题,导致try方法被阻塞,根本没有执行,那自然需要做回滚。那cancel方法需要支持即便没有执行try方法也能正确回滚
业务悬挂在于可能在回滚后阻塞的调用到了,但因为此时事务早就结束,没有二阶段,因此永远不会confirm或者是cancel,因此需要避免这种情况
Saga
Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

Saga 的实现(状态机引擎)
目前 SEATA 提供的 Saga 模式是基于状态机引擎来实现的,机制是:
- 通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
- 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
- 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚(注意: 异常发生时是否进行补偿也可由用户自定义决定)
- 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
示例状态图:

状态机引擎原理:

- 图中的状态图是先执行stateA, 再执行stateB,然后执行stateC
- "状态"的执行是基于事件驱动的模型,stateA执行完成后,会产生路由消息放入EventQueue,事件消费端从EventQueue取出消息,执行stateB
- 在整个状态机启动时会调用Seata Server开启分布式事务,并生产xid, 然后记录"状态机实例"启动事件到本地数据库
- 当执行到一个"状态"时会调用Seata Server注册分支事务,并生产branchId, 然后记录"状态实例"开始执行事件到本地数据库
- 当一个"状态"执行完成后会记录"状态实例"执行结束事件到本地数据库, 然后调用Seata Server上报分支事务的状态
- 当整个状态机执行完成, 会记录"状态机实例"执行完成事件到本地数据库, 然后调用Seata Server提交或回滚分布式事务
状态机引擎设计

状态机引擎的设计主要分成三层, 上层依赖下层,从下往上分别是:
Eventing 层:
- 实现事件驱动架构, 可以压入事件, 并由消费端消费事件, 本层不关心事件是什么消费端执行什么,由上层实现
ProcessController 层:
- 由于上层的Eventing驱动一个“空”流程引擎的执行,"state"的行为和路由都未实现, 由上层实现
基于以上两层理论上可以自定义扩展任何"流程"引擎
StateMachineEngine 层:
- 实现状态机引擎每种state的行为和路由逻辑
- 提供 API、状态机语言仓库
状态机的高可用设计

状态机引擎是无状态的,它是内嵌在应用中。
当应用正常运行时(图中上半部分):
- 状态机引擎会上报状态到Seata Server;
- 状态机执行日志存储在业务的数据库中;
当一台应用实例宕机时(图中下半部分):
- Seata Server 会感知到,并发送事务恢复请求到还存活的应用实例;
- 状态机引擎收到事务恢复请求后,从数据库里装载日志,并恢复状态机上下文继续执行;
XA
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。

执行阶段:
- 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
- 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
完成阶段:
- 分支提交:执行 XA 分支的 commit
- 分支回滚:执行 XA 分支的 rollback
整体运行机制
XA 模式 运行在 Seata 定义的事务框架内:

执行阶段(E xecute):
- XA start/XA end/XA prepare + SQL + 注册分支
完成阶段(F inish):
- XA commit/XA rollback
数据源代理
XA 模式需要 XAConnection。
获取 XAConnection 两种方式:
- 方式一:要求开发者配置 XADataSource
- 方式二:根据开发者的普通 DataSource 来创建
第一种方式,给开发者增加了认知负担,需要为 XA 模式专门去学习和使用 XA 数据源,与 透明化 XA 编程模型的设计目标相违背。
第二种方式,对开发者比较友好,和 AT 模式使用一样,开发者完全不必关心 XA 层面的任何问题,保持本地编程模型即可。
我们优先设计实现第二种方式:数据源代理根据普通数据源中获取的普通 JDBC 连接创建出相应的 XAConnection。
类比 AT 模式的数据源代理机制,如下:

但是,第二种方法有局限:无法保证兼容的正确性。
实际上,这种方法是在做数据库驱动程序要做的事情。不同的厂商、不同版本的数据库驱动实现机制是厂商私有的,我们只能保证在充分测试过的驱动程序上是正确的,开发者使用的驱动程序版本差异很可能造成机制的失效。
这点在 Oracle 上体现非常明显。参见 Druid issue:https://github.com/alibaba/druid/issues/3707
综合考虑,XA 模式的数据源代理设计需要同时支持第一种方式:基于 XA 数据源进行代理。
类比 AT 模式的数据源代理机制,如下:

分支注册
XA start 需要 Xid 参数。
这个 Xid 需要和 Seata 全局事务的 XID 和 BranchId 关联起来,以便由 TC 驱动 XA 分支的提交或回滚。
目前 Seata 的 BranchId 是在分支注册过程,由 TC 统一生成的,所以 XA 模式分支注册的时机需要在 XA start 之前。
将来一个可能的优化方向:
把分支注册尽量延后。类似 AT 模式在本地事务提交之前才注册分支,避免分支执行失败情况下,没有意义的分支注册。
这个优化方向需要 BranchId 生成机制的变化来配合。BranchId 不通过分支注册过程生成,而是生成后再带着 BranchId 去注册分支。
XA 模式的使用
从编程模型上,XA 模式与 AT 模式保持完全一致。
可以参考 Seata 官网的样例:seata-xa
样例场景是 Seata 经典的,涉及库存、订单、账户 3 个微服务的商品订购业务。
在样例中,上层编程模型与 AT 模式完全相同。只需要修改数据源代理,即可实现 XA 模式与 AT 模式之间的切换。
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}