前言
对于spring cloud的学习和复习,这里主要分为两部分:一是spring cloud的基本概念介绍,二是spring cloud的常用组件说明
1. spring cloud概述
1.1 分布式与微服务概念
-
微服务:微服务是指,将一个完整的系统按照业务划分(也可以按照其他维度划分)为一个个更小系统的架构风格;举个例子说明:一个商城系统,可以按微服务的架构风格,划分出来商品服务、订单服务、库存服务等等,这每个服务是各自独立的,可以独立开发、独立部署。
-
分布式:分布式是指,部署这些个微服务的方式(分布式也可以是部署其他类型系统的方式),在分布式系统下,微服务架构中的一个个服务被作为一个个组件部署到不同的物理或者虚拟机器上,这些组件之间可以通过网络进行通信和协作。
总的来说,微服务架构通常部署在分布式系统上,每个微服务部署到不同的机器上,并且可以通过网络进行通信和协作,就形成了分布式系统,而spring cloud就是一个专注于全局微服务协调和治理、简化分布式系统开发的开源框架。
1.2 spring cloud介绍
spring cloud基于spring boot集合了各种成熟的框架,提供了服务发现、配置管理、负载均衡等功能,是一个专注于全局微服务协调和治理、简化分布式系统开发的开源框架。
1.3 spring cloud的优缺点
- 优点:
- 简化了分布式系统的开发:让开发人员可以专注于业务逻辑而不是解决分布式系统的各种问题
- 快速开发和部署微服务:spring boot为其提供了默认配置和快速启动器,spring cloud本身又有自己一套微服务架构的解决方案,让开发人员可以快速开发和部署一套功能完整的微服务架构
- 缺点:
- 数据管理麻烦
- 系统集成测试麻烦
1.4 spring cloud的组件
- Eureka:服务注册与发现,每个微服务在启动时,会向Eureka服务器注册自己的信息,包括服务名、IP地址、端口号,其他服务可以通过Eureka来发现和调用这个服务
- Zuul:服务网关,提供动态路由,监控等功能
- Ribbon:负载均衡,对客户端进行负载均衡
- Feign:声明式的http客户端,简化了服务之间的调用代码
- Hystrix:断路器,当服务不可用时,进行熔断,防止故障扩散
- config:分布式统一配置管理
2. Eureka
2.1 背景介绍
在传统单体应用中,服务的调用通常以硬编码的方式实现,但是在微服务架构中,服务数量可能众多,而且服务实例可能会动态增加减少改变,这时候硬编码的方式就变得难以维护,因此需要一种机制来自动发现和管理这些服务,而Eureka就是一个实现了这样功能的组件
2.2 工作原理
- 一个微服务启动时,会向Eureka服务器注册自己的信息,这其中包括:服务名、IP地址、端口号
- Eureka服务器会维护一个服务注册表,里面记录了所有注册的服务实例信息
- 其他微服务可以通过Eureka服务器来查询需要调用的服务实例信息,从而实现服务之间的动态调用
2.3 高可用的实现
Eureka的高可用是通过集群的方式来实现的,通过部署多个服务器实例,并让它们相互注册,构建成为一个多节点的服务器集群。通过相互注册的方式,让这些实例相互知道对方的存在,从而当一个实例宕机时,其他实例仍能提供服务,下面是spring cloud中,Eureka相互注册的配置:
eureka:
client:
serviceUrl:
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/
这个配置中配置了eureka这个实例启动时,需要注册的其他实例(eureka1、eureka2)地址,当它启动时,它会向eureka1、eureka2注册自己的信息,同时也会接收来自eureka1、eureka2的注册信息来实现服务器实例之间的相互注册
2.4 自我保护机制
Eureka服务器会定期检查注册表中实例的健康状况,如果一个实例没有在一定时间内向服务器发送心跳,那么这个实例就会被从注册表中删除,但是会有网络故障或者其他什么原因,导致是Eureka服务器无法正常收到心跳,而不是实例没有发送心跳,这就会造成误删除。
为了解决这个问题,就引入了自我保护机制,当Eureka服务器在一定时间内没有收到一个实例的心跳,它不会立即从注册表中删除这个实例,而是把它标记为“保护”状态,让它继续对外提供服务,只有超过一定时间,仍没有收到这个实例的心跳,才会把它删除
2.5 Eureka与zooKeeper的区别
- Eureka是对服务发现与注册进行了专门的优化,而zooKeeper是一个分布式协调服务,服务的注册与发现只是它的功能之一
- Eureka集群中的节点是平级的,而zooKeeper集群中的节点有主从之分
- Eureka遵循CAP原则中的AP、而zooKeeper遵循CP
CAP原则:分布式系统中的三个特性:
C:一致性,在分布式系统中,对任意节点的访问得到的数据应该是一致的
A:可用性,在分布式系统重,对任意节点的访问都应该得到及时响应,不响应或者超时响应都认为是不可用
P:分区容错性,由于网络等原因出现故障导致部分节点与其他节点失去连接形成独立分区后,整个系统仍然能对外提供服务根据CAP原则,一个分布式系统最多只能满足其中的两个特性;由于网络延迟或者其他故障等不可控的因素,分区容错性是必须保证的,剩余的两个特性中,Eureka主要用于服务之间的调用,更注重可用性,它通过集群模式实现高可用;
而zooKeeper是一个分布式协调服务,用于协调分布式系统中的各种任务和状态信息,需要保证数据的一致性;zooKeeper的一致性是强一致性,通过ZAB协议实现,简单说明ZAB协议的过程如下:
①. leader选举:zooKeeper集群中,会有一个leader节点负责处理客户端的读写请求
②. 提案:当客户端发送读写请求时,leader节点会生成一个提案,然后广播给其他节点
③. 多数派确认:从节点收到提案后,会向leader发送确认信息,只有收到了多数节点的确认信息后,提案才会被提交
④. 提交:提案被提交后,leader会把写操作广播给其他节点,从而保证数据一致性
2.6 Eureka与Nacos
Eureka实际上已经停止更新,以下由Netfilx开发的组件许多也停止更新,因为对应的功能实现也有了其他的解决方案(主要以阿里系为主),目前在服务注册与发现功能上,更普遍使用的是Nacos,Nacos相比与Eureka,除了服务注册与发现功能以外,还包含了spring cloud config的分布式配置管理功能。
以下是Nacos主要功能的基本原理:
- 服务注册与发现:Nacos的服务注册与发现功能实现的原理与Eureka基本相同,它们都是通过服务提供者将自己的信息注册到注册中心(Eureka server或者Nacos server),然后服务消费者到注册中心获取可用服务列表,从而实现服务与服务之间的通信。
- 配置管理:与spring cloud config不同,Nacos的配置并不是存储在远程仓库(通常是git)中的,而是存储在Nacos自己的服务器中,Nacos还提供了一些API和工具类封装了与Nacos服务器交互的细节,使得开发人员可以快速集成Nacos,比如通过ConfigService类就可以调用getConfig方法获取注册中心的服务信息
3. Zuul
3.1 背景介绍
在客户端与后端服务之间,有一个中间层系统,用于简化它们的交互,这个中间层系统就是网关,网关可以提供路由转发、权限控制、监控等功能,Zuul就属于一个网关,它可以作为整个微服务架构的入口,为客户端提供统一的访问接口
网关可以分为:
- API网关:侧重于管理和暴露API,提供对外服务的统一访问方式
- 微服务网关,侧重于管理和路由微服务之间的通信
Zuul根据具体的配置和要求,既可以是API网关也可以是微服务网关
3.2 工作流程
- 客户端发送请求到Zuul
- Zuul根据预定义的路由规则,将请求转发到对应的后端服务
- 后端服务处理完成,将响应返回给Zuul
- Zuul将响应返回给客户端
3.3 主要功能
- 路由转发:Zuul可以根据路由规则,将请求转发给对应的后端服务
- 过滤器:允许在请求的各个阶段执行自定义逻辑,例如日志记录、性能监控
- 负载均衡:Zuul可以集成Ribbon,实现负载均衡,将请求转发到多个实例中
3.4 使用场景
- API网关:所有API请求都经过Zuul,实现统一的路由、认证、监控和日志记录
例如:当客户端发起请求时,Zuul的过滤器拦截这个请求,记录请求的相关信息(请求的路径、参数等等),同时记录请求的处理结果(响应码、处理时间等等),实现日志记录 - 负载均衡:集成Ribbon,将请求分发到多个后端服务实例中
- 安全认证:Zuul可以实现认证、授权等安全功能
例如:当前有一个微服务架构的电子商务系统,当一个客户端请求订单服务的时候,Zuul的过滤器可以拦截这个请求,分析请求中的身份信息(token、session),进行用户身份认证、认证成功后,通过检查用户是否有权限访问这个资源,实现授权检查,检查成功再将请求转发到对应的后端服务,防止恶意访问和非法操作
3.5 过滤器
Zuul过滤器是一种基于自身过滤体系扩展而来的机制,它允许用户在请求进入网关、路由过程中、响应返回的不同阶段,插入自定义的逻辑,实现认证,授权、日志记录等操作,具体实现过程如下:
- 实现Zuul提供的抽象类,编写自定义逻辑
- 重写filterType方法,确定过滤器类型,Zuul提供了四种标准的过滤器类型,分别是:
①. pre:请求被路由前
②. route:路由中
③. post:路由完成
④. error:发生错误时 - 重写run方法,进行身份验证等等操作
下面简单展示相关代码:
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.stereotype.Component;
@Component
public class AuthFilter extends ZuulFilter { // 1. 实现抽象类
@Override
public String filterType() { // 2. 重写filterType方法
return "pre";
}
// ...省略其他代码,诸如定义过滤器执行顺序、是否执行
@Override
public Object run() { // 3. 重写方法,实现自定义过滤逻辑
RequestContext ctx = RequestContext.getCurrentContext();
String token = ctx.getRequest().getHeader("Authorization");
// 进行用户认证逻辑,这里简单地检查token是否为空
if (token == null) {
// 如果认证失败,拦截请求,返回401 Unauthorized错误
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("Unauthorized");
}
return null;
}
}
3.6 Zuul集群
实现过程:
- 部署多个实例:在不同机器上部署多个实例
- 服务注册与发现:将每个实例都注册到注册中心(如Eureka),使得Zuul实例可以动态注册和发现其他实例,实现集群的自动扩展和缩减
- 负载均衡:使用负载均衡器(nginx、Ribbon等)进行请求的分发,将请求分发到不同的服务器
4. Ribbon
4.1 背景介绍
- 负载均衡
顾名思义,负载均衡就是让集群(或者网络设备等等)中的每个资源(服务器实例、节点等)的负载达到均衡,不让某个资源负载过大导致性能下降,也不让某个资源负载过小导致性能浪费,实现吞吐量最大化、响应时间最小化 - Ribbon
Ribbon与上面的Eureka、Zuul一样,都是Netflix开发是组件,它被用来把客户端的请求均衡地转发给多个服务实例 - 它与Nginx的区别和优势
- 区别
- Ribbon是一个客户端负载均衡库,通常嵌在应用程序客户端中,由客户端完成负载均衡策略的选择,再使用Ribbon进行请求转发
- Nginx是一个独立的反向代理服务器,负载均衡策略的选择和请求转发都是由自己完成
- 优势
- 在spring cloud框架中,Ribbon相比与Nginx可以更好地与其他服务治理组件集成,实现更灵活的负载均衡
- 区别
4.2 工作过程
- 客户端想Ribbon发起请求
- Ribbon根据配置的负载均衡策略选择一个服务实例
- Ribbon将请求转发给选定的服务器实例
负载均衡策略:
- 轮训:将请求轮流分发到每个服务实例上
- 随机:随机分发到服务实例上
- 服务权重:根据服务实例的权重来分发请求,服务权重通常根据服务器的性能和负载情况来动态调整
- 最少连接:将请求分发给当前连接最少的服务实例
- 响应时间加权:将请求转发到响应时间最短的服务实例,或者说响应时间短的服务会获得更多请求
4.3 与Eureka集成
Ribbon与Eureka集成后,它可以从Eureka注册中心获取当前可用的服务实例列表,然后根据自己的负载均衡策略选择服务实例,而不再需要在自己的配置文件中,以硬编码的方式配置服务实例
5. Hystrix
5.1 背景介绍
- 雪崩效应
当一个服务调用另一个服务出现问题时,调用者就会等待被调用者的响应,当更多服务请求这些不能正常提供服务的资源时,就会造成更多的请求等待,这种连锁反应就是雪崩效用 - Hystrix
Hystrix就是一个用于防止雪崩的工具
5.2 工作过程
- 封装命令:每一个可能出现故障的远程调用都需要经过Hystrix的命令模式进行注册和封装成为命令
- 执行命令:当远程调用发起时,Hystrix会通过命令模式执行封装好的命令;Hystrix会为每个命令维护一个线程池,和其他需要的资源,用于执行这些命令
- 监控命令:Hystrix会实时监控这些命令的执行情况,并根据监控数据对出现异常的调用执行对应的决策,防止出现雪崩,对系统造成进一步伤害
5.3 雪崩产生的原因
- 代码bug
- 访问量激增:Tomcat默认情况下,只有一个线程池维护客户端发送的请求,某一接口在某一时间被大量访问就会占据线程池中的现场,其他请求就会被等待
- 服务器硬件问题,导致服务不可用
5.4 防雪崩方式
- 服务降级:远程调用失败或者超时时,Hystrix可以提供降级策略,返回一个默认值或者进行其他补偿逻辑
- 服务熔断:当一个服务被超时等待,系统不断尝试重试也会造成资源浪费,可能导致后续影响,Hystrix可以通过断路器,对错误率超过阈值的服务进行停用,减少对系统的压力
- 服务隔离:通过限制资源的访问和调用,对服务进行隔离,比如说使用Hystrix的线程池隔离机制,为问题服务分配单独的线程池,防止影响其他服务
- 服务监控:Hystrix可以实时监控服务调用情况,记录失败率,响应时间,可以及时发现问题,防止进一步恶化雪崩
5.5 服务降级的实现
- 继承HystrixCommand类
- 重写run方法,实现调用逻辑
- 重写getFallback方法,实现降级逻辑,当run方法执行错误时,执行这个方法
以下是简单代码展示:
public class RemoteCallCommand extends HystrixCommand<String> {
@Override
protected String run() throws Exception {
// 在这里执行远程调用的逻辑
// 返回远程调用的结果
return "Remote call result";
}
@Override
protected String getFallback() {
// 降级逻辑,当远程调用失败或超时时执行
return "Fallback result";
}
}
6. Feign
在传统服务调用过程中,我们往往需要手动创建http请求、处理请求和响应、序列号、反序列化,这些代码是冗长且不好维护的,而Feign就可以解决这种场景的问题;我们只需要简单定义一个接口,在接口上通过注解的方式描述http请求的细节,Feign就会根据这个接口自动生成实际的http请求,可以大大简化服务调用的流程。
目前更推荐使用Feign的升级版本,openFeign,下面以openFeign为例,简单说明其实现过程:
- 引入openFeign依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 定义一个接口,使用注解(@FeignClient)来说明所调用的服务详情
@FeignClient(name = "example", url = "http://example.com")
public interface ExampleClient {
@RequestMapping(method = RequestMethod.GET, value = "/api/example")
String getExample();
}
其中name是服务名,URL是服务地址
3. 在需要调用远程服务的地方调用getExample()
@Service
public class ExampleService {
@Autowired
private ExampleClient exampleClient;
public String getExample() {
return exampleClient.getExample();
}
}
7. config
7.1 背景介绍
spring cloud config组件可以用来集中管理微服务架构中的应用程序配置,将配置信息从应用中分离出来,集中管理
7.2 工作过程
- 配置文件仓库:我们把配置信息存储到一个配置仓库中,比如git,或者本地文件
- config server:搭建config server,从配置仓库中读取配置信息,并提供API接口供程序读取配置
- config 客户端:从config server处获取配置信息;在程序启动时,会向Config server请求自己的配置
- 动态刷新:客户端可以监听server中的配置变更时间,动态刷新并加载最新的配置
8. Gateway
8.1 背景介绍
Gateway是spring cloud提供的下一代网关服务,相比于Zuul有更好的性能,更灵活的路由配置和更丰富的过滤器功能
8.2 与Zuul的区别
Gateway在工作流程上与Zuul区别不大,都是请求先到到组件,由组件根据路由规则,选择合适的服务实例,这过程中还会经过滤器过滤,负载均衡器分发等,它与Zuul的主要区别如下:
- 响应式编程:Gateway提供了响应式编程支持,在响应式编程中,数据流可以被观察,可以触发响应的处理逻辑,在高并发情况下,可以提供更好的性能
- 过滤器链:Gateway支持过滤器链,这是一组过滤器组成的链条,请求需要依次通过这些过滤器,这种方式可以实现对请求的统一管理
- 动态路由:Gateway支持动态路由,支持根据请求的条件,动态选择路由规则,将请求路由到不同的服务实例,这些条件包括请求的URL、请求头、请求参数
9. Sentinal
Sentinal是阿里系的容错与熔断工具,相比与Hystrix,它更轻量,性能更好,功能也更加丰富,它提供了实时监控、流量控制、熔断降级等一系列功能
- 工作原理
Sentinal的核心工作原理是通过实时统计系统的负载情况,然后根据预设的规则进行流量控制和熔断降级工作:
- 实时统计系统的负载情况:Sentinal会不断统计系统中各个资源的请求量、响应时间、错误率这些指标,根据这些指标来实时监控系统的负载情况
- 根据预设规则进行相应处理:在Sentinal中可以通过代码或者配置文件的形式配置相应规则,比如说设置一个方法每秒最多通过多少个请求,当系统的实际情况超过了这个阈值,Sentinal就会进行相对于的流量控制或者熔断降级操作
具体来说,要完成这个过程的话,需要完成以下操作
- 定义规则:使用Sentinal提纲的FlowRule来设置规则,flowRule.setXXX这种方式
- 在需要保护的方法上添加注解,@SentinelResource(value = “上面的flowRule名”, blockHandler = “降级方法”),这里的blockHandler与Hystrix中的getFallback方法类似,都是当被包含方法出现异常出现问题时,自动调用
10. Seata
参考自:分布式事务Seata
Seata由阿里开发,是一套高性能和易于使用的分布式事务解决方案,在认识Seata前,需要了解一些分布式事务的理论原则,了解分布式事务需要满足哪些原则,为了满足这些原则又提出了哪些思路
10.1 基础理论
1. CAP理论
这里是cap理论就是上面比较Eureka和zooKeeper时提到的cap理论,要满足某一项的时候,就得考虑放弃另外的某一项,比如要满足一致性,就需要更多的资源,包括等待时间等,这样就会失去可用性
2. BASE理论
BASE理论是完成上面cap的一种解决思路
- BA,Basically Available ,基本可用;分布式系统中出现故障时,允许部分失去可用性,保证核心部分可用性
- S,Soft State,软状态;在一定时间内可以出现中间状态,比如临时的不一致性
- E,Eventually Consistent;最终一致性,允许出现过程的不一致性,但是需要达到分布式系统中数据的最终一致性,比如记录异常信息,做补偿操作等
10.2 Seata中的工作角色
Seata中有三个重要角色来完成分布式事务管理工作:
- TM(Transaction Manager):事务管理器,用于定义全局事务的范围,着眼于全局事务的协调和控制
- TC(Transaction Coordinator) :事务协调者,维护全局和分支事务的状态,相比与TM,更着眼于具体到分支的协调和控制
- RM (Resource Manager) :资源管理器(比如说数据库),管理分支事务处理的资源,每个分支都有自己的RM,负责向TC注册分支和报告分支状态(已准备好提交或回滚)
举一个简单例子来说明他们的角色关系和对应工作:在学校中有两个人数相同的班级,现要向这两个班级中的学生分苹果,需要保证每个班分到的苹果数量一致,其中:
- TC相当于校长,他负责协调和管理分苹果的整个具体过程,根据RM提交的苹果分配工作进度报告,确认工作完成,或者在分配出现问题的时候,通知RM把苹果收回来,即TC在全局事务管理中,负责具体分支事务的协调和控制
- TM相当于辅导员,他会向校长申请开始分苹果工作,或者向校长申请收回苹果,即TM在全局事务管理中,负责全局事务的协调和控制
- RM相当于班主任,他负责跟校长说明班级情况和报告分苹果工作的进度,并且进行具体分发苹果的工作,即RM负责具体资源的管理和向TC报告分支状态
它们的协调关系如下:
- TM向TC申请开启全局事务
- TC创建全局事务后返回全局唯一的XID,XID会在全局事务的上下文中传播
- RM向TC注册分支,告知自己参与了全局事务,它将会被TC记录下分支信息,归属到拥有相同XID的全局事务
- RM执行分支事务
- TM向TC发起全局事务提交或回滚
- TC调度XID下的分支事务完成提交或回滚,具体来说,TC会根据分支事务的执行情况通知RM进行具体事务的提交或回滚工作
10.3 分布式事务解决方案
Seata提供了四种分布式事务的解决方案:
1. XA模式
XA模式是基于XA协议来实现事务的一致性的,在这么模式下,Seata可以保证分支事务要么全部提交,要么全部回滚,具体来说,在XA模式下,Seata通过二阶段提交的方式来完成分布式事务控制:
第一阶段(准备阶段):
- TM向TC申请开启全局事务
- TM向各个RM发送请求,要求其执行自己的分支事务
- RM向TC注册分支事务,告诉TC自己参与了全局事务
- RM执行事务,此时SQL已执行但事务未提交
- RM向TC报告执行状态(是否准备好提交事务,是否能安全参与到全局事务的提交阶段)
第二阶段(提交阶段):
- TM通知TC进行全局事务提交、回滚
- TC检测各分支执行情况
- 如果全部执行成功,则通知RM进行所有分支的事务提交
- 如果存在执行失败,则通知RM进行所有分支的事务回滚
- RM根据TC的指示,完成事务提交或回滚工作
简单总结:
XA模式主要依靠关系型数据库本身的事务机制来实现分布式事务的全局控制,优点是事务满足ACID原则,是强一致性,而且没有代码入侵,较为容易实现;同时缺点也比较明显,就是它需要依赖于关系型数据库的事务机制,非关系型数据库的服务或者说系统,它就无能为力,另外,在二阶段结束之前,事务都未提交,都在相互等待,占用的资源较多
2. AT模式
AT模式弥补了XA模式中,资源锁定时间过长的缺陷,是Seata的默认模式
AT模式同样采用了二阶段提交的方式完成分布式事务控制:
第一阶段:
- TM开启全局事务
- TM调用分支,通知RM进行事务操作
- RM向TC注册分支
- RM记录undo log
- RM执行分支事务中的操作
- RM提交事务并向TC报告事务状态:重点是AT模式下,第一阶段,RM就会提交事务
第二阶段:
二阶段(提交)
- 在收到TM的全局事务提交请求后,如果TC检测到分支事务的执行情况是全部成功,那么就会向RM发送提交请求
但其实这个提交请求其实只是让RM将第一阶段记录的undo log删除,实际分支事务在第一阶段就已经提交了,因为事务都执行成功了,undo
log就没有用了,删除掉就可以完成全局事务了
-
RM收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC
-
异步任务阶段批量地删除相应 UNDO LOG 记录。
二阶段(回滚)
- 收到 TC 的分支回滚请求,开启一个本地事务
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改,需要回滚事务
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句
- 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC
简单总结:
AT模式与XA模式的最大不同在于,AT模式在第一阶段就直接让各个分支各自提交自己的事务,不需要等待TC全局校验过后再提交,节省了资源的占用,而分支各自提交自己的事务,就可能带来数据不一致问题,为了解决这个问题就引入了undo log日志做补偿操作的机制,在出现异常的时候,对已经执行成功的事务进行回滚,显然这是一种最终一致性的方案,是存在中间状态的;最后总结AT模式的优缺点:
优点:
- 第一阶段提交事务,减少了资源占用
- 通过undo log的方式回滚事务,不依赖关系型数据库本身的事务机制
缺点:
- 存在中间状态,属于最终一致性的解决方案
- 并发场景下存在脏写问题
补充说明脏写问题
脏写问题发生的具体场景如下:
- 事务A要对一条记录(字段count= 10)进行更新,比如,set count= count+ 1;
- 获取BD锁,保存快照
- 执行SQL,提交事务,释放DB锁,此时count= 11
- 等待TC指示提交或回滚
与此同时,在34之间,事务A释放DB锁后,事务B获取了DB锁,也进行了count的更新:set count = count + 2 ,并提交了事务,此时count= 13
- 回到第5步,TC要将事务回滚
- 那么RM会根据undo log中记录的 更新前的记录,也叫前镜像,将count改回10,那么事务B的更新就失效了
通过时间轴的方式来看更直观一些:

为了解决这个问题,AT模式引入全局锁的概念
全局锁:由TC维护的一张表,表中记录了当前正在操作数据的事务ID,操作的表,操作的表中的记录,表示在某个时候只有这个事务能操作这一行数据
全局锁添加的时机在事务提交前,在这个例子中,事务A在提交事务前,获取了全局锁,然后提交等待TC指令,这个过程中,即便事务B更改了count,但是由于无法获取到全局锁,所以无法提交事务,避免了无效的更新

此时还有一个问题是,如果Seata之外的事务C操作了这行记录,那么上面全局锁避免脏写的机制就会失效,因为事务C是步遵循全局锁的概念的,对于这种场景,AT模式的做法是这样的:
-
在记录undo log日志的时候,除了记录更新前的数据,还会记录更新后的数据,也就是后镜像,在这个例子中,前镜像count的值为10,后镜像的值为11,假如事务C更新count = count + 5,那么此时数据库中,count的值应该为16
-
AT模式下,Seata会比较后镜像与数据库中对应字段的值,如果值一致,说明数据库中的数据是在Seata控制下修改的,直接回滚就行了,如果不一致,说明有Seata之外的事务进行了修改,这是Seata没法进行回滚的,这种情况下可能就需要人工干预,Seata可以记录异常日志,通知异常信息,让人工进行干预
3. TCC模式
TCC模式与AT模式的
相同之处在于:它们都在第一阶段就提交了事务,因此都存在中间状态,最终实现的是最终一致性
不同之处在于:它们在事务的隔离性上进行的处理不同,TCC模式不需要通过全局锁和undo log的方式做隔离和回滚,因此TCC模式的性能要强于AT模式
TCC模式的具体原理如下:
第一阶段:
- TM开启全局事务
- TM调用分支
- RM向TC注册分支
- RM预留资源(try)
- RM提交事务并向TC报告事务状态
第二阶段:
- TM向TC申请提交或回滚全局事务
- TC检测分支事务状态,检测预留的资源是否足够
- 如果分支事务没有异常,那么通知RM提交事务(confirm)
- 否则通知RM释放预留的资源(cancel)
对上述过程做一些解释:
- 首先try是对资源进行冻结,这些冻结的资源只属于当前事务,其他事务对同一数据进行操作的时候,也会冻结自己需要的资源,资源是数据的一部分,举个例子,数据库中的count = 10,事务A要扣减count - 2,那么事务A就暂时占有这“2”,事务B要扣减比如3的话,事务B就暂时占有10中的这“3”的部分
- 第二阶段的提交与AT模式的提交类似,并不是真的提交事务,因为事务已经提交过了,这里的提交是删除冻结的那部分资源,以上面的例子来说,这里的提交是删除冻结的“2”,因为事务执行成功的话,此时数据库中count的值应该是8,冻结的这部分资源已经被扣减了
- 第二阶段的cancel相当于回滚,这里是删除冻结资源,然后反向操作恢复到初始资源状态,如果事务A要回滚,就+2
简单总结:
优点:
- TCC模式第一阶段提交事务,就释放了DB锁,与AT模式一样减少了资源的占用
- TCC模式通过冻结当前事务所需要的资源的方式,自然做到了事务隔离,各个事务之间互不干扰,也免去了全局锁的开销,这种机制也不再需要记录undo log,通过cancel的方式回滚,性能进一步提高
缺点:
- 这种方式的实现比较麻烦,需要在业务代码中添加上述的几个方法,一方面对业务代码有入侵,另一方面实现更复杂
- 要支持空回滚:如果一个分支事务的try方法阻塞了,导致全局回滚,那么那个分支的事务也会收到回滚指令,但是实际上它是没有执行事务的,所以需要进行空的回滚操作,直接返回回滚结果
支持空回滚的方法如下:
使用TCC模式进行事务控制的时候,需要额外创建一张表存储冻结的资源信息,里面包括事务ID,冻结的内容(比如count),全局事务的状态(在执行try、confirm、cancel中的什么方法)
空回滚的实现在于在cancel方法执行时,先根据事务ID去查询冻结信息表,如果没查询到记录,那就说明这个分支事务没有执行,在try阶段就阻塞了,直接返回回滚结果而不进行具体回滚逻辑
- 要避免业务悬挂(右悬挂):全局事务回滚完成的时候,try方法阻塞结束,开始执行,那么这时候就是没有意义的,这种情况被称为业务悬挂
避免业务悬挂的实现方法如下:
如果事务已经回滚过了,此时,全局事务的状态是cancel,那么显然,在try方法执行时,判断分支事务的状态,如果是cancel就不执行try业务即可
4. Saga模式
Saga模式是Seata提供的长事务解决方案,它与TCC模式模式类似,都是第一阶段进行本地事务的提交,二阶段进行全局事务提交的确认或者回滚(补偿操作),不同的是,TCC模式通过释放预留资源的方式进行回滚,而Saga模式则是在事务失败时,通过状态引擎执行已成功节点对应的补偿节点将事务回滚
具体来说,状态引擎包含了以下几个部分,用于完成整个分支事务链的补偿操作:
- 事务状态:定义了事务在执行过程中的状态,包括初始状态、正常执行状态、异常状态、补偿状态
- 状态转换规则:定义了事务在不同状态之间的转换规则,包括正向执行和补偿操作触发的条件
- 补偿操作:定义了每个状态下需要执行的操作,可以编写补偿逻辑,用于回滚之前的操作
如此一来,当分支事务中的某一个出现异常时,状态引擎会根据定义的状态转换规则将事务状态转为补偿状态,并通过补偿操作来执行相对应的操作,这样最终可以确保事务的一致性
309

被折叠的 条评论
为什么被折叠?



