目录
从Spring Cloud netflix 到 Spring Cloud Alibaba
Sentinel自定义限流处理逻辑-处理sentinel本身异常
SpringCloudAlibaba介绍
简介
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。–摘自官网
ps: 本文中新增项目的操作,都在一个空项目中进行。
本文源码下载地址:
为什么要学SpringCloudAlibaba
SpringCloudAlibaba整合了SpringCloudNetflix,在SpringCloudAlibaba的学习中,我们可以看到SpringCloudNetflix的影子,而由于SpringCloudNetflix的停更,SpringCloudAlibaba普遍应用于市面之上,所以我们学习SpringCloudAlibaba尤为重要。
从Spring Cloud netflix 到 Spring Cloud Alibaba
1. 2020-12-22日**Spring** 官方博客宣布,`Spring Cloud 2020.0.0`正式发布。`2020.0.0`是第一个使用新的版本号命名方案的**Spring Cloud** 发行版本。在此之前**Spring Cloud** 使用英国伦敦地铁站的命名方式来命名一个大版本(`train version`),如果不按照新的版本号命名的话,本次的版本号应该是Ilford。
2. 更新版本没有什么大惊小怪的,但是本次更新却正式开启了**Spring Cloud Netflix** 体系的终结进程。**Netflix** 公司是目前微服务落地中最成功的公司。它开源了诸如**Eureka** 、**Hystrix** 、**Zuul** 、**Feign** 、**Ribbon** 等等广大开发者所知微服务套件,统称为**Netflix OSS** 。在当时**Netflix OSS** 成为微服务组件上事实的标准。但是在2018年**Netflix** 公司宣布其核心组件**Hystrix** 、**Ribbon** 、**Zuul** 、**Eureka** 等进入**维护状态** ,不再进行新特性开发,只修BUG。这直接影响了**Spring Cloud** 项目的发展路线,**Spring** 官方不得不采取了应对措施,在2019年的在 **SpringOne 2019** 大会中,**Spring Cloud** 宣布 **Spring Cloud Netflix项目进入维护模式** ,并在2020年移除相关的**Netflix OSS** 组件。
3. Spring Cloud Aalibaba 成为了主流
功能组件
1. Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
2. Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
3. RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
4. Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
5. Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
6. Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
7. Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
8. Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
Nacos
简介
1. Nacos(Naming Configuration Service) 是一个易于使用的动态服务发现、配置和服务管理平台,用于构建云原生应用程序。
2. 服务发现是微服务架构中的关键组件之一,Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
3. Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
个人理解
Nacos是作为Eureka的平替的技术,不同的是,Nacos集服务注册与发现、配置中心、负载均衡于一体,比Eureka更加完善与全面,我们以实际生活中的案例说明一下。
以网吧上网开包间流程举例说明:
需要开包间的话,首先我们需要去服务台,让网关通过服务台查看有哪些包间可用,服务台可以查看哪些包间可用,哪些不可用,付钱后,我们就可以进入指定的包间,在这个过程之中有以下角色:
网吧包间:我们把包间看做是微服务中一个个独立的系统。
服务台:我们把服务台看做是nacos的服务中心,服务中心知晓当前哪些服务也就是包间可用,并且监控包间的状态。
下图可做出解释:
通过上图我们可以了解到,nacos其实就是一个调度者,通过nacos可以注册服务、然后在不同的服务之间进行 调度与管理。
相关链接
官网网址:https://nacos.io/zh-cn/index.html
官网文档网址:https://nacos.io/zh-cn/docs/quick-start.html
注意:使用官网推荐的稳定版本:
下载地址:https://github.com/alibaba/nacos/releases
下载与安装
以下为nacos-server端的安装和使用过程:
1. 访问官网网址:https://github.com/alibaba/nacos/releases
2. 下载后直接解压压缩包即可,解压后的目录如下:
3. 通过window+R键,输入cmd进入命令行,进入nacos的bin目录下 。
4. 直接执行命令,以单机模式启动nacos:
startup.cmd -m standalone
5. 以下画面为启动成功。
6. 在浏览器中访问地址,用户名密码均输入nacos:
http://localhost:8848/nacos/#/login
7. 可以成功登录即可。
服务注册
介绍
Nacos可以直接提供注册中心(Eureka)+配置中心(Config),通过成功安装和启动Nacos以后就可以发现Nacos本身就是一个小平台,它要比之前的Eureka更加方便,不需要我们在自己做配置。
服务发现是微服务架构中的关键组件之一。在这样的架构中,手动为每个客户端配置服务列表可能是一项艰巨的任务,并且使得动态扩展极其困难。Nacos Discovery 帮助您自动将您的服务注册到 Nacos 服务器,Nacos 服务器会跟踪服务并动态刷新服务列表。此外,Nacos Discovery 将服务实例的一些元数据,如主机、端口、健康检查 URL、主页等注册到 Nacos。
使用naocs进行服务注册/发现案例
1. 创建新项目,通过聚合项目搭建案例:由于聚合带来的诸多好处,在SpringBoot项目开发中也广泛采用,开发中将SpringBoot项目按照功能分成子模块开发,所以我们在使用Spring Cloud Alibaba完成项目的时候,也是采用聚合项目来完成。
2. 首先我们来创建父工程,选择依赖:web。
3. 新建nacos子项目,无需选择依赖。
4. 修改nacos子项目的依赖,将父项目的信息加入pom文件。
<parent>
<groupId>com.springcloudAlibaba</groupId>
<artifactId>springcloudAlibaba</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
5. 在nacos子项目中增加依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
6. 在父项目中增加modules,引入nacos子项目
<!--引入nacos子项目-->
<modules>
<module>nacos-9001</module>
</modules>
7. 在nacos子项目中配置application.yml
#服务的端口
server:
port: 9001
#服务名称
spring:
application:
name: nacos-provider
#向哪个地址注册服务
cloud:
discovery:
server-addr: localhost:8848
#对外暴露的东西
management:
endpoint:
web:
exposure:
include: '*'
8. 在nacos项目的主类上增加注解:@EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
public class Nacos9001Application {
public static void main(String[] args) {
SpringApplication.run(Nacos9001Application.class, args);
}
}
9. 声明一个测试的controller,测试对外访问。
@RestController
public class MyController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/hello")
public String hello(){
return "Hello Nacos Port"+serverPort;
}
}
10. 启动nacos本地的服务,启动nacos子项目,
11. PS: 在这里可报错,找不到某个bean,需要降低springboot的版本,因为在springboot4之后删除了某个类,所以启动报错,这里使用springboot的2.3.12版本。
12. 访问controller
13. 最后,我们参照以上创建nacos子项目的方式,创建端口号为9002的子项目,为下一阶段的实操做准备。
Nacos实现服务消费者注册和负载均衡
Nacos中整合了ribbon去实现负载均衡,什么是ribbon呢,也可以通过小编的另一篇文章了解一下:
微服务入门篇(一),带你走进微服务之SpringCloudNetFix框架_只为code醉的博客-CSDN博客
ribbon是一个基于HTTP和TCP,客户端的负载均衡器。它虽然只是一个工具类库,它却是**每一个微服务**的基础设施。因为实际上,对于服务间调用、API网关请求转发都需要经过Ribbon负载均衡来实现。总体来说,Ribbon的主要作用是:从注册服务器端拿到对应服务列表后以负载均衡的方式访问对应服务。
要注意的是Nacos已经整合了Ribbon,所以我们想要使用只需要导入Spring Cloud Alibaba Nacos的依赖就可以直接使用了。
ribbon的使用案例总结
如果我们需要远程访问那么可以使用RestTemplate,其中getForObject是最常用方法,同时还要在服务消费者中配置RestTemplate:
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
restTemplate.getForObject(arg1,arg2,arg3...);
第一个参数url表示被调用的目标Rest接口位置
1. url的第一部分是在Nacos中注册的服务提供者名称,如果多个服务提供者注册相同名称,Ribbon会自动寻找其中一个服务提供者,并且调用接口方法。这个就是负载均衡功能。
2. url后半部是控制器的请求路径。
第二个参数是返回值类型
1. JavaBean类型或者JavaBean数组类型,如果控制器返回的是List集合,需要使用数组类型接收。
第三个参数是可变参数
1. 是传递给url的动态参数,使用参数时候需要在url上需要使用{1}、{2}、{3}进行参数占位,这样传递的参数就会自动替换占位符。
使用Nacos中的Ribbon实现负载均衡
1. 在上面创建的空项目中,创建consumer项目,依赖暂不选择。
2. 修改项目的pom文件:
<parent>
<groupId>com.springcloudAlibaba</groupId>
<artifactId>springcloudAlibaba</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
3. 添加nacos的依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
4. 在主类上增加注解:@EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
public class Consumer8083Application {
public static void main(String[] args) {
SpringApplication.run(Consumer8083Application.class, args);
}
}
5. 在application.yml中配置端口和服务名称:
#服务的端口
server:
port: 8083
#服务名称
spring:
application:
name: nacos-consumer1
6. 在父项目中将consumer项目引入:
<module>consumer-8083</module>
7. 在consumer项目的yml文件中,添加以下内容:
#消费方系统需要访问哪个服务方的nacos地址
service-url:
nacos-user-service: http://nacos-provider
8. 在消费者consumer的服务启动类上配置:
//消费方通过此注解调用服务方接口 需要使用restTemplete对象
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
9. 编写controller类,调用服务方接口:
@RestController
public class MyController {
//使用 restTemplate类 调用远程方法
@Autowired
RestTemplate restTemplate;
//读取配置文件中服务方的地址
@Value("${service-url.nacos-user-service}")
private String serverUrl;
@GetMapping(value = "consumer/hello")
public String getHello(){
//getforObject 参数1: 远程调用的地址 参数2: 返回值类型 参数3:可变参数(使用时有几个参数就在参数1路径后面拼接占位: {1})
return restTemplate.getForObject(serverUrl+"/hello",String.class);
}
}
10. 访问接口,可以看到轮询调用的效果,浏览器会打印出调用的服务端口。
Nacos之服务配置中心
Nacos不仅仅可以作为注册中心来使用,同时它支持作为配置中心。
Nacos配置规则
在 Nacos Spring Cloud 中,`dataId` 的完整格式如下(详情可以参考官网 https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html):
${prefix}-${spring.profiles.active}.${file-extension}
1. `prefix` 默认为 `spring.application.name` 的值,也可以通过配置项 `spring.cloud.nacos.config.prefix`来配置。
2. `spring.profiles.active` 即为当前环境对应的 profile,注意:**当 `spring.profiles.active` 为空时,对应的连接符 `-` 也将不存在,dataId 的拼接格式变成 `${prefix}.${file-extension}`**(不能删除)
3. `file-exetension` 为配置内容的数据格式,可以通过配置项 `spring.cloud.nacos.config.file-extension` 来配置。目前只支持 `properties` 和 `yaml` 类型。
4. 通过 Spring Cloud 原生注解 `@RefreshScope` 实现配置自动更新:
5. 所以根据官方给出的规则我们最终需要在Nacos配置中心添加的配置文件的名字规则和名字为:
# ${spring.application.name}-${spring.profiles.active}.${file-extension}
# nacos-config-client-dev.yaml
# 微服务名称-当前环境-文件格式
配置中心案例实现
1. 新建nacos配置项目。
2. 在config项目中修改父项目依赖
<parent>
<groupId>com.springcloudAlibaba</groupId>
<artifactId>springcloudAlibaba</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
3. 在config项目中增加依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
4. 新增bootstrap.yml文件。
server:
port: 3377
spring:
application:
name: nacos-config-client
#配置nacos服务注册中心的地址
cloud:
nacos:
discovery:
server-addr: localhost:8848
#配置nacos配置注册中心的地址
config:
server-addr: localhost:8848
#指定读取yaml格式的文件
file-extension: yaml
5. 在Application.yml中的配置:
#表示开发环境的配置
spring:
profiles:
active: dev
6. 在主类上增加注解:@EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
public class NacosconfigApplication {
public static void main(String[] args) {
SpringApplication.run(NacosconfigApplication.class, args);
}
}
7. 编写controller:
@RestController
@RefreshScope //配置文件修改后 不用重启就能生效
public class ConfigController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo(){
return configInfo;
}
}
8. 打开nacos平台,配置信息,配置规则:服务名称-application.yml中指定的active-yml
9. 访问接口,获取nacos配置中心配置的信息:
Nacos命名空间、分组、DataId之间的关系
名词解释
命名空间(Namespace)
用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
配置分组(Group)
Nacos 中的一组配置集,是组织配置的维度之一。通过一个有意义的字符串(如 Buy 或 Trade )对配置集进行分组,从而区分 Data ID 相同的配置集。当您在 Nacos 上创建一个配置时,如果未填写配置分组的名称,则配置分组的名称默认采用 DEFAULT_GROUP 。配置分组的常见场景:不同的应用或组件使用了相同的配置类型,如 database_url 配置和 MQ_topic 配置。
配置集 ID(Data ID)
Nacos 中的某个配置集的 ID。配置集 ID 是组织划分配置的维度之一。Data ID 通常用于组织划分系统的配置集。一个系统或者应用可以包含多个配置集,每个配置集都可以被一个有意义的名称标识。Data ID 通常采用类 Java 包(如 com.taobao.tc.refund.log.level)的命名规则保证全局唯一性。此命名规则非强制。
配置集:
一组相关或者不相关的配置项的集合称为配置集。在系统中,一个配置文件通常就是一个配置集,包含了系统各个方面的配置。例如,一个配置集可能包含了数据源、线程池、日志级别等配置项。
三者关系
这三者的关系类似于Java里面的package名和类名,最外层的Namespace是可以用于区分部署环境的,Group和DataID逻辑上区分两个目标对象。
默认情况
Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT
具体情况
Nacos默认的命名空间是public,我们就可以利用Namespace来实现隔离,比如我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。
Group本身就是分组的意思,它可以把不同的微服务划分到同一个分组里面去。
剩下的就是具体微服务,一个Service可以包含多个Cluster,Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。比如说,将一个Service部署在北京和和杭州的机房中,北京机房的Service就可以起名为(BJ),杭州机房中的Service就可以起名为(HZ),这样就可以尽量让同一个机房的微服务互相调用,提升性能。
通过DataId实现环境切换的案例实现
- 通过nacos的服务端,新建两套DataID,一套是dev、一套是test。
2. 可以修改nacosconfig项目,修改项目中的application.yml文件,将dev改为test
3. 访问接口测试,返回的是test配置文件中的内容
通过分组实现环境切换的案例实现
Group默认的分组是DEFAULT_GROUP,通过新建分组,也可以实现环境切换。
1. 通过配置列表,点击加号,新建dev分组。
2. 通过配置列表,点击加号,新增test分组。
3. 修改nacosconfig项目中的application.yml和bootstrap.yml文件
Bootstrap.yml:
group: TEST_GROUP
Application.yml:
active: info
4. 访问接口,返回信息与配置信息一致即可。
通过命名空间实现环境切换的案例实现
Nacos默认的命名空间为public,不能删除,我们可以新建两套命名空间去实现切换环境效果。
1. 新建dev和test命名空间。
2. 在服务管理中可以看到新增的命名空间,点击可切换。
3. 修改nacosconfig中的bootstrap.yml中的内容,配置的namespace就是命名空间的ID:
#通过命名空间的ID,指定命名空间
namespace: 26322169-dfe7-4455-8678-3d679feebc53
4. 打开application.yml文件中的active,指定测试环境中的配置:
spring:
profiles:
# active: info
# active: dev
active: test
5. 在对应的命名空间下新增配置
6. 重启服务,调用接口,查看是否调用了指定配置文件中的信息。
7. 切换命名空间和分组,查看切换效果。
Nacos集群环境的搭建
nacos的部署方式
Nacos支持三种部署模式
1. 单机模式 – 用于测试和单机试用
2. 集群模式 – 用于生产环境,确保高可用
3. 多集群模式 – 用于多数据中心场景
部署文档参考网站:https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html
部署文档图:
其实就是以下形式部署:
Linux下搭建Nacos的集群环境:
服务介绍:Nginx服务1+nacos服务3+mysql服务1
1. 下载linux版本的nacos,下载地址:Releases · alibaba/nacos · GitHub
2. 使用rz目录,将压缩包上传到虚拟机
3. 解压tar包,将压缩包解压到usr/local目录下,即可安装成功:
tar -zxvf nacos-server-2.1.1.tar.gz -C /usr/local/
4. 这里将192.168.84.128 服务器作为nacos服务器。
5. 准备:nacos、nginx、mysql,需要将以上三个安装在一个服务器上。
6. Nginx的安装参照:LINUX安装nginx详细步骤_大蛇王的博客-CSDN博客_linux安装nginx
7. Mysql的安装参照:呕心整理,项目中常用的Linux命令。_只为code醉的博客-CSDN博客
8. 将nacos文件夹复制出来三份,模拟集群。
9. 执行以下命令,运行sql脚本。
create database nacos_config;
use nacos_config;
source /usr/local/nacos/conf/nacos-mysql.sql
10. 修改application.properties 配置文件
vi /usr/local/nacos/conf/application.properties
11. 加入以下内容:
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://192.168.84.128:3306/nacos_config?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
db.user=root
db.password=123456
12. 改完之后将另外两个nacos目录也改一下
13. 开始修改nacos的集群配置cluter.conf,进入nacos、nacos2、nacos目录,都修改conf下的cluter.conf文件,增加配置:
192.168.84.128:8848
192.168.84.128:8868
192.168.84.128:8888
14. 修改nacos/conf/application.properties文件中的端口号,分别改为8848、8868、8888
15. 关闭本地防火墙:
systemctl disable firewalld
16. 在本地浏览器访问地址:如果无法访问,可以查看nacos的日志,
tail -f /usr/local/nacos/logs/ nacos.log
17. 如果报连接错误请参考以下地址的处理方式进行处理:
https://www.cnblogs.com/tingguoguoyo/p/11005584.html
18. 修改nginx安装目录下:
vi /usr/local/nginx/nginx-1.13.7/conf/nginx.conf
在http里面添加以下内容:
upstream nacos {
server 192.168.84.129:8848;
server 192.168.84.129:8868;
server 192.168.84.129:8888;
}
在server里面添加以下内容:
listen 81;
location / {
proxy_pass http://nacos;
}
19. 添加完毕后,即可访问:
http://192.168.84.129:81/nacos/#/login
20. 本地项目配置nacos的server-addr为: http:// 192.168.84.129:81 即可注册服务和读取配置从nacos。
Nacos与其他注册中心的对比
各种服务注册中心对比
CAP模型
计算机专家 埃里克·布鲁尔(Eric Brewer)于 2000 年在 ACM 分布式计算机原理专题讨论会(简称:PODC)中提出的分布式系统设计要考虑的三个核心要素:
一致性(Consistency):同一时刻的同一请求的实例返回的结果相同,所有的数据要求具有强一致性(Strong Consistency)
可用性(Availability):所有实例的读写请求在一定时间内可以得到正确的响应
分区容错性(Partition tolerance):在网络异常(光缆断裂、设备故障、宕机)的情况下,系统仍能提供正常的服务
以上三个特点就是CAP原则(又称CAP定理),但是三个特性不可能同时满足,所以分布式系统设计要考虑的是在满足P(分区容错性)的前提下选择C(一致性)还是A(可用性),即:CP或AP
CP原则:一致性 + 分区容错性原则
CP 原则属于强一致性原则,要求所有节点可以查询的数据随时都要保持一直(同步中的数据不可查询),即:若干个节点形成一个逻辑的共享区域,某一个节点更新的数据都会立即同步到其他数据节点之中,当数据同步完成后才能返回成功的结果,但是在实际的运行过程中网络故障在所难免,如果此时若干个服务节点之间无法通讯时就会出现错误,从而牺牲了以可用性原则(A),例如关系型数据库中的事务。
AP原则:可用性原则 + 分区容错性原则
AP原则属于弱一致性原则,在集群中只要有存活的节点那么所发送来的所有请求都可以得到正确的响应,在进行数据同步处理操作中即便某些节点没有成功的实现数据同步也返回成功,这样就牺牲一致性原则(C 原则)。
使用场景:对于数据的同步一定会发出指令,但是最终的节点是否真的实现了同步,并不保证,可是却可以及时的得到数据更新成功的响应,可以应用在网络环境不是很好的场景中。
Nacos支持CP和AP
Nacos无缝支持一些主流的开源生态,同时再阿里进行Nacos设计的时候重复的考虑到了市场化的运作(市面上大多都是以单一的实现形式为主,例如:Zookeeper使用的是 CP、而 Eureka采用的是AP),在Nacos中提供了两种模式的动态切换。
Nacos切换模式
1. 一般来说,如果不需要储存服务界别的信息且服务实例通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。如Spring Cloud 和 Dubbo,都适用于AP模式,AP模式为了服务的可用性减弱了一致性,因此AP模式下只支持注册临时实例。
2. 如果需要在服务级别编辑或者储存配置信息,那么CP是必须的,K8S服务和DNS服务则是用于CP模式。CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
3. 切换命令(默认是AP):
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
注意:临时和持久化的区别主要在健康检查失败后的表现,持久化实例健康检查失败后会被标记成不健康,而临时实例会直接从列表中被删除。
Sentinel
简介
1. 分布式系统的流量防卫兵:随着微服务的普及,服务调用的稳定性变得越来越重要。[Sentinel](https://github.com/alibaba/Sentinel)以“流量”为切入点,在流量控制、断路、负载保护等多个领域开展工作,保障服务可靠性。
2. 特点:
1.丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
3. 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
4. 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
5. 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
官网文档:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
Sentinel的优势
分布式系统面临的问题:复杂的体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的失败,比如如下的例子中,当我们调用A、E、F、J、K这几个服务的时候如果其中一个服务出现问题会造成什么问题?其实就会出现整体系统效率全部下降,而且严重就会出现。
服务雪崩
多个微服务之间调用的时候,假设A调用B和C,B和C又调用其他的微服务,这就是所谓的扇出。
如果扇出的某个链路上某个微服务调用的响应时间过程或者不可用,微服务A的调用就用占用越来越多的系统资源,从而引起系统崩溃,这也就是服务雪崩。其实就是服务的高可用遭到了破坏。
对于高流量的应用来说,单一的后端依赖可能会导致服务器上的所有资源都在几秒钟内饱和。同时还有可能造成这些应用程序导致服务之间的延迟增加,备份列队,线程和其他的系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系失败,不能取消整个应用程序或系统,所以通常发生了一个模块的某个实例失败后,这时候这个模块依然还会接受流量,然后这个有问题的模块还调用其他的模块,这样就会发生级联故障,或者叫做雪崩。
要解决这种问题的出现我们就需要用到服务降级,而Sentinel就可以保证在一个依赖出现问题的情况下,不会导致整体服务失败,避免级联故障,提高分布式系统的弹性。
Sentinel的熔断降级通过断路器实现:
断路器:它本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似于熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法出的异常,这样就保证了服务调用方的不会被长时间、不必要的占用,从而避免了故障在分布式系统中蔓延(类似于病毒传染),从而避免了故障在系统中蔓延,乃至崩溃。
下载与安装
下载地址:https://github.com/alibaba/Sentinel/releases
Sentinel 分为两个部分
– 核心库(Java客户端)不依赖任何框架/库,只需要Java运行时环境,同时对Dubbo/SpringCloud 等框架也有较好的支持。
– 控制台(Dashboard)基于 SpringBoot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器。
启动步骤
– 前提:jdk1.8环境和8080端口不能被占用
– 启动命令:java -jar sentinel-dashboard-1.8.2.jar
– 访问地址:localhost:8080
– 输入默认账号密码:sentinel/sentinel
Sentinel初始化监控的搭建
1. 创建项目:sentinel-service8401
2. 导入依赖:
<!--nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
3. 配置yaml文件,目的是让当前的8401注册到nacos,然后被sentinel8080进行监控。
server:
port: 8401
spring:
application:
name: sentinel-service
#配置nacos服务注册中心的地址
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置sentinel控制面板的地址
dashboard: localhost:8080
#配置sentinel默认端口
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
4. 在项目的主类上增加注解:@EnableDiscoveryClient
5. 新增controller,简单演示
@RestController
public class Mycontroller {
@GetMapping("getA")
public String getA(){
return "result******A***";
}
@GetMapping("getB")
public String getB(){
return "result******B***";
}
}
6. 启动nacos、sentinel、再去启动sentinel8401项目。
7. 访问地址:
http://localhost:8080/#/dashboard/identity/sentinel-service
Sentinel流控规则-QPS和线程
名词解释
– 资源名:唯一名称,默认请求路径
– 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
– 阈值类型/单机阈值:
– QPS(每秒钟的请求数量):当调用该API的QPS达到阈值的时候,进行限流
– 线程数:当调用该API的线程数量达到阈值的时候,进行限流
– 是否集群:当前不需要集群
– 流控模式:
– 直接:API达到限流条件时,直接限流
– 关联:当关联的资源达到阈值时,就限流自己
– 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)(API级别的针对来源)
– 流控效果:
– 快速失败:直接失败,抛异常
– Wam Up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFacotor,经过预热时长,才达到设置的QPS阈值
– 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
新增流控规则-QPS
QPS直接失败案例
1. 添加有两种方式,可以直接在流控规则选项中添加,也可以在簇点链路中添加,一般会采取第二种方式
新增流控规则-阈值:
代表一秒内只能有一个请求访问。
在浏览器中多次刷新,请求被sentinel阻塞,报错如下:
新增流控规则-线程数
- 新增流控规则-线程数:代表一秒内只能有一个线程访问
在controller中阻塞线程
在浏览器中开启两个窗口访问同一接口,出现阻塞:
两种方式的区别:
QPS代表一次只能有一个请求进来,线程数则代表,有多个线程之间争夺资源。
Sentinel流程规则-关联
官方解释:当关联的资源达到阈值时,就限流自己,通俗解释来说,比如那我们的程序,现在有testA接口和testB接口,当A关联的资源B达到阈值后,就限流自己,也就是B到达阈值,限流A本身。就好像我家孩子在外面打架,我来处理一样。换到程序里面来说比如一个电商系统中,支付系统达到阈值,就限流下订单系统。
流控规则-关联使用案例
1. 新增关联规则
2. 在postman中新增集合
3.输入访问地址与参数信息
4. 点击集合,点击右边的run
5. 配置请求次数信息
6. 此时访问接口A
Sentinel流控规则-链路
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流,它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。
比如在一个微服务中,两个接口都调用了同一个Service中的方法,并且该方法用SentinelResource(用于定义资源)注解标注了,然后对该注解标注的资源(方法)进行配置,则可以选择链路模式。
Sentinel流控规则-链路使用案例
1. 在service层使用@SentinelResource注解修饰资源。
2. 在controller调用此service
3. 在application.yml中配置,使sentinel可以针对不同的URL进行链路限流。
sentinel:
transport:
dashboard: localhost:8080
# dashboard: http://39.98.46.94:8080
port: 8719
web-context-unify: false
4. 选择对应的资源,点击新增,填写对哪个接口限流
5. 访问接口,会出现限流情况
Sentinel流控规则-预热
官网手册地址:https://sentinelguard.io/zh-cn/docs/flow-control.html
概念:Warm Up方式,即预热/冷启动方式。该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。
预热公式:阈值/coldFactor(默认值为3),经过预热时间后才会达到阈值。
使用场景:一般秒杀系统中会有这样的流控设置,为了防止秒杀瞬间造成系统崩溃。
Sentinel流控规则-预热使用案例
1. 新增预热流控规则。
Sentinel流控规则-排队等待
官方文档:https://sentinelguard.io/zh-cn/docs/flow-control.html
概念:匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求(削峰填谷)。
匀速器
它的中心思想是,以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过。否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过(排队等待处理);若预期的通过时间超出最大排队时长,则直接拒接这个请求。
Sentinel 匀速排队等待策略是漏桶算法结合虚拟队列等待机制实现的。
Sentinel流控规则-排队等待实现案例
1. 在controller中打印出当前的线程信息。
2. 新增流控规则,多少秒之内一秒处理一个请求。
3. 使用postman调用接口,此时会发现,不管postman设置不设置延迟,都是一秒钟处理一个请求。
Sentinel熔断策略
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
Sentinel 提供了一下几种熔断策略:
– 慢调用比例 (`SLOW_REQUEST_RATIO`):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
– 异常比例 (`ERROR_RATIO`):当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 `[0.0, 1.0]`,代表 0% – 100%。
– 异常数 (`ERROR_COUNT`):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
Sentinel在1.8.0版本对熔断降级做了大的调整,可以定义任意时长的熔断时间,引入了半开启恢复支持。下面梳理下相关特性。
熔断状态有三种状态,非别为OPEN、HALF_OPEN、CLOSED
熔断规则
Sentinel熔断策略-慢调用比例
概念:选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
个人理解:慢调用比例的使用其实就是在规定时间内有多少交易低于规定的时间则进行熔断熔断的时长是自己规定的时长。
举例:
Sentinel熔断策略-慢调用比例使用案例
1. 首先我们先添加一个控制器方法:
//FlowLimitController.java
@GetMapping("/testC")
public String testC(){
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "----testC";
}
2. 设置熔断策略,1QPS>5 并且这些请求的RT>300 并且大于比例阈值触发熔断
3. 通过JMeter测试,1秒钟发起10个线程请求/testC,此时就会触发熔断效果,停止测试以后,10秒钟以后恢复正常
Sentinel熔断策略-异常比例
当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 `[0.0, 1.0]`,代表 0% – 100%。
PS: 仅针对业务异常,对 Sentinel 限流降级本身的异常(`BlockException`)不生效。
简单理解:
Sentinel熔断策略-异常比例-使用案例
1. 编写测试接口
//FlowLimitController
@GetMapping("/testD")
public String testD(Integer id){
if(id != null && id > 1){
throw new RuntimeException("异常比例测试");
}
return "------------testD";
}
2. 设置熔断策略异常比例
3. 我们通过JMeter来测试,设定HTTP请求地址
1秒钟发送10个请求
当启动JMeter的时候,就会触发熔断,因为此时我们1秒钟发送10个请求超过了最小请求数5,同时超过了阈值,满足了两个条件,当熔断时长过后就会恢复正常。
Sentinel熔断策略-异常数
当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
简单理解:
Sentinel熔断策略-异常数-使用案例
1. 编写接口
//FlowLimitController
@GetMapping("/testE")
public String testE(Integer id){
if(id != null && id > 1){
throw new RuntimeException("异常数测试");
}
return "------------testE";
}
2. 设置异常数策略,当1秒钟内请求超过5并且异常数大约5个的时候触发熔断
3. 通过JMeter来测试
4. 1秒钟发送10个请求
5. 测试访问结果
Sentinel 热点规则(上)
概念
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
– 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
– 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效
官网:https://sentinelguard.io/zh-cn/docs/parameter-flow-control.html
使用案例
1. 编写controller层代码,设置热点。
2. 新增热点规则
3. 设置出现限流时返回的信息
4. 测试热点限流效果
参数级别细粒度的限流
当一般情况下,走的是上面的规则,当参数值达到下面的条件时,限流规则走的是下方的规则。
Sentinel 系统规则
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效**。入口流量指的是进入应用的流量,比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
– Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1(1分钟平均负载) 作为启发指标,进行自适应系统保护。当系统 load1(1分钟平均负载) 超过设定的启发值(阈值),且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 `maxQps(秒级统计的最大QPS) * minRt(秒级统计的最小响应时间)` 估算得出。设定参考值一般是 `CPU cores * 2.5`。
– CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
– 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
– 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
– 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
Sentinel自定义限流处理逻辑-处理sentinel本身异常
Sentinel 提供了@SentinelResource注解用于定义资源,并提供了AspectJ的扩展用于自定义资源,处理BlockException等。
1. 创建自定义处理sentinel限流的类:CustomerBlockHandler
2. 在controller类上增加注解@SentinelResource,声明处理类的信息。
3. 新增限流规则,测试。
4. 测试
Sentinel服务熔断环境搭建
为了演示操作,所以在这里我们需要利用Ribbon进行负载均衡的调用,所以我们需要创建一个服务消费者cloudalibaba-consumer8084和两个服务提供者cloudalibaba-provider9003和cloudalibaba-provider9004,以下是结构图
1. 新建commons项目,定义实体类,使其他项目可以使用,依赖选择lombok。
2. 新建服务端项目,端口9003/9004,依赖选择,nacos-descovery,将commons项目作为依赖导入服务端项目中。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.commons</groupId>
<artifactId>commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
3. 配置服务端项目的yml文件
server:
port: 9003
spring:
application:
name: nacos-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
4. 在主类上增加注解
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceProvider9003Application {
public static void main(String[] args) {
SpringApplication.run(ServiceProvider9003Application.class, args);
}
}
5. 编写controller代码
@RestController
public class DataController {
@Value("${server.port}")
private String serverPort;
//模仿数据库存储数据
public static HashMap<Integer,String> hashMap = new HashMap<>();
static {
hashMap.put(1,"鼠标");
hashMap.put(2,"键盘");
hashMap.put(3,"耳机");
}
@GetMapping("info/{id}")
public JsonResult<String> lcSql(@PathVariable("id") Long id){
JsonResult<String> result = new JsonResult(1,hashMap.get(id));
return result;
}
}
6. 创建consumer消费方项目,依赖如下:
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.commons</groupId>
<artifactId>commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
7. 配置yml文件
server:
port: 8084
spring:
application:
name: nacos-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-provider
8. 启动类增加注解@EnableDiscoveryClient,并且注入远程调用的对象RestTemplete
@SpringBootApplication
@EnableDiscoveryClient
public class Consumer8083Application {
public static void main(String[] args) {
SpringApplication.run(Consumer8083Application.class, args);
}
//消费方通过此注解调用服务方接口 需要使用restTemplete对象
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
9. 编写controller类,调用9003/9004服务的接口。
@RestController
@Slf4j
public class DemoController {
//服务提供者URL
@Value("${service-url.nacos-user-service}")
private String SERVICE_URL;
@Autowired
private RestTemplate restTemplate;
/**
* 传参调用远程接口服务
*/
@GetMapping("/consumer/fallback/{id}")
public JsonResult<String> fallback(@PathVariable Long id){
//通过Ribbon发起远程访问,访问9003/9004
JsonResult<String> result = restTemplate.getForObject(SERVICE_URL+"/info/"+id,JsonResult.class);
return result;
}
}
10. 启动nacos、启动sentinel、启动项目,开始调用。
访问http://localhost:8084/consumer/fallback/2
Sentinel中的fallback属性-处理程序本身异常
fallback属性简介
概念:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 `exceptionsToIgnore` 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
– 返回值类型必须与原函数返回值类型一致;
– 方法参数列表需要和原函数一致,或者可以额外多一个 `Throwable` 类型的参数用于接收对应的异常。
– fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 `fallbackClass` 为对应的类的 `Class` 对象,注意对应的函数必需为 static 函数,否则无法解析。
其实通过官网上提供的概念,我们不难看出这个属性类似于blockHandler,但是各位一定要注意他们有本质的不同。
注意:fallback属性和blockHandler属性的本质不同在于他们作用的异常不同:
– blockHandler:针对违反Sentinel控制台配置规则时触发BlockException异常时对应处理的属性
– fallback:针对Java本身出现的异常进行处理的对应属性。
fallback属性使用案例
1. 在消费者的controller层,定义处理sentinel和处理程序异常的类,当取不到对应的值时,抛出异常。
Controller层代码如下:
/**
* 传参调用远程接口服务
*/
@GetMapping("/consumer/fallback/{id}")
//value 资源名称 当前是哪个资源进行异常处理
//blockHandler sentinel异常返回信息的方法走哪个
//blockHandlerClass sentinel异常返回信息的类走哪个
//fallback java程序异常返回信息的方法走哪个
//fallbackClass java程序异常返回信息的类走哪个
@SentinelResource(value = "fallback",blockHandler = "sentinelHandler",fallback = "appHandler",
blockHandlerClass = SentinelExceptionHandler.class,fallbackClass = AppExceptionHandler.class )
public JsonResult<String> fallback(@PathVariable Integer id){
log.info("URL: "+SERVICE_URL+"/info/"+id);
if(id<=3){
//通过Ribbon发起远程访问,访问9003/9004
JsonResult<String> result = restTemplate.getForObject(SERVICE_URL+"/info/"+id,JsonResult.class);
return result;
}else{
throw new NullPointerException("没有对应的数据记录");
}
}
处理程序异常的代码如下:
public class AppExceptionHandler {
public static JsonResult<String> appHandler(Integer id,Throwable throwable){
JsonResult<String> jsonResult=new JsonResult<>(401,"呜呜呜....客官.....您搜索的资源暂未搜到哦.........");
return jsonResult;
}
}
处理sentinel异常的代码如下:
public class SentinelExceptionHandler {
public static JsonResult<String> sentinelHandler(Integer id, BlockException blockException){
JsonResult<String> jsonResult=new JsonResult<>(400,"呜呜呜....访问人数太多...暂时无法访问哦。");
return jsonResult;
}
}
2. 新增异常限制规则
3. 测试:首先处理程序中定义的异常,如果超过sentinel的熔断规则,就触发sentinel的异常。
忽略某些异常,直接抛出,使用属性:exceptionsToIgnore
OpenFeign
概念
OpenFeign是一种声明式、模板化的HTTP客户端。在Spring Cloud中使用OpenFeign,可以做到使用HTTP请求访问远程服务,就像调用本地方法一样的,开发者完全感知不到这是在调用远程方法,更感知不到在访问HTTP请求,用法其实就是编写一个接口,在接口上添加注解即可。
可以简单理解它是借鉴Ribbon的基础之上,封装的一套服务接口+注解的方式的远程调用器。
OpenFeign能干什么
它的宗旨是在编写Java Http客户端接口的时候变得更加容易,其底层整合了Ribbon,所以也支持负载均衡。
之前我们使用Ribbon的时候,利用RestTemplate对Http请求进行封装处理,但是在实际开发中,由于对服务依赖的调用不可能就一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以OpenFeign在此基础之上做了进一步的封装,由它来帮助我们定义和实现依赖服务接口的定义,我们只需创建一个接口并使用注解的方式来配置它,即可完成对微服务提供方的接口绑定,简化Ribbon的操作。
使用案例
1. 在父项目的pom文件中引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
2. 找到消费方项目8084,配置yml文件
server:
port: 8084
spring:
application:
name: nacos-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
service-url:
nacos-user-service: http://nacos-provider
3. 在主启动类中增加注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceCosumer8084Application {
public static void main(String[] args) {
SpringApplication.run(ServiceCosumer8084Application.class, args);
}
//消费方通过此注解调用服务方接口 需要使用restTemplete对象
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
4. 在消费方的项目中,创建一个service接口。
/**
* 配合openfeign使用的接口
* 添加@FeignClient注解 声明需要远程调用的服务名称
* 声明需要远程调用的方法
*/
@Service
@FeignClient("nacos-provider")
public interface OpenfeignService {
//GetMapping中 配置远程服务所声明的地址
//方法名称和返回值 以远程服务为准
@GetMapping("info/{id}")
public JsonResult<String> fallback(@PathVariable Integer id);
}
5. 创建controller层代码
@RestController
public class OpenFeignController {
@Autowired
OpenfeignService openfeignService;
@GetMapping("getInfo/{id}")
public JsonResult<String> fallback(@PathVariable Integer id){
return openfeignService.fallback(id);
}
}
6. 浏览器测试访问效果
7. Openfeign和sentinel的结合使用
@RestController
public class OpenFeignController {
@Autowired
OpenfeignService openfeignService;
//使用sentinel处理程序异常
//使用openfeign进行远程调用
@GetMapping("getInfo/{id}")
@SentinelResource(value = "fallback",blockHandler = "sentinelHandler",fallback = "appHandler",
blockHandlerClass = SentinelExceptionHandler.class,fallbackClass = AppExceptionHandler.class )
public JsonResult<String> fallback(@PathVariable Integer id){
if(id<=3){
//通过Ribbon发起远程访问,访问9003/9004
return openfeignService.fallback(id);
}else{
throw new NullPointerException("没有对应的数据记录");
}
}
}
超时时间控制
OpenFeign 客户端默认等待1秒钟,但是如果服务端业务超过1秒,则会报错。为了避免这样的情况,我们需要设置feign客户端的超时控制。
1. 在服务端项目,睡眠3秒钟,使用客户端调用接口。
2. 调用时会出现超时的提示
3. 解决案例:使用ribbon去解决超时时间响应的问题,配置yml文件
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接后从服务器读取到可用资源所用的时间
ReadTimeout: 5000
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ConnectTimeout: 5000
日志打印
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。
简单理解,就是对Feign接口的调用情况进行监控和输出
日志级别:
– NONE:默认的,不显示任何日志;
– BASIC:仅记录请求方法、URL、响应状态码及执行时间;
– HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
– FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
1. 在消费方项目的主类上,注入feign的log,注入日志。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceCosumer8084Application {
public static void main(String[] args) {
SpringApplication.run(ServiceCosumer8084Application.class, args);
}
//消费方通过此注解调用服务方接口 需要使用restTemplete对象
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
//注入logger
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
2. 配置yml文件
#监控哪个接口
logging:
level:
com.cosumer.service.OpenfeignService: debug
3. 测试效果
Sentinel的持久化配置
概念
我们首先需要知道:在Sentinel Dashboard中配置规则之后重启应用就会丢失,所以实际生产环境中需要配置规则的持久化实现,Sentinel提供多种不同的数据源来持久化规则配置,包括file,redis、nacos、zk。
将限流规则持久化进Nacos保存,只要刷新8401某个接口地址,Sentinel控制台的流控规则就能感应到,同时只要Nacos里面的配置不删除,针对8401上Sentinel的流控规则就持续有效。
通过Nacos配置文件修改流控规则—拉取—>Sentinel Dashboard界面显示最新的流控规则
注意:在Nacos控制台上修改流控制,虽然可以同步到Sentinel Dashboard,但是Nacos此时应该作为一个流控规则的持久化平台,所以正常操作过程应该是开发者在Sentinel Dashboard上修改流控规则后同步到Nacos,遗憾的是目前Sentinel Dashboard不支持该功能,
也就是说sentinel只能去nacos中同步定义好的限流规则数据,不能直接将限流规则写入nacos之中。
使用案例
1. 在项目中导入依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.1</version>
</dependency>
2. 配置yml文件
sentinel:
transport:
#配置sentinel的服务地址
dashboard: localhost:8080
#默认端口8719
port: 8719
# 配置Sentinel的持久化
datasource:
nacos:
nacos:
# 持久化到哪个nacos服务
serverAddr: localhost:8848
# 持久化到nacos中的哪个分组
groupId: DEFAULT_GROUP
# 读取的是nacos的哪个配置文件
dataId: order-sentinel.json
# 默认不用管
ruleType: flow
3. 进入nacos中配置json信息
[
{
"resource": "test1",
"limitApp": "default",
"grade": 1,
"count": 2,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
4. 在controller层配置代码:
@RestController
public class OrderController {
@GetMapping("/order/test1")
@SentinelResource(value = "test1")
public String test1() throws InterruptedException {
return "test1 ";
}
}
5. 测试
6. Ps: sentinel的持久化配置,需要依靠nacos实现,如果需要保留限流规则或者熔断规则,需要在nacos中的json文件中配置,直接在sentinel控制台配置是不行的。
GateWay
概念
在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。这样的话会产生很多问题,例如:
– 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
– 认证复杂,每个微服务都有独立认证
– 存在跨域请求,在一定场景下处理相对复杂
为解决上面的问题所以引入了网关的概念:所谓的API网关,就是指系统的统一入口,提供内部服务的路由中转,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等。
各类型网关对比
– Zuul 1.x
Netflix开源的网关,基于Servlet框架构建,功能丰富,使用JAVA开发,易于二次开发 问题:即一个线程处理一次连接请求,这种方式在内部延迟严重、设备故障较多情况下会引起存活的连接增多和线程增加的情况发生。
– Zuul 2.x
Zuul2 采用了Netty实现异步非阻塞编程模型,每个 CPU 核一个线程,处理所有的请求和响应,请求和响应的生命周期是通过事件和回调来处理的,这种方式减少了线程数量,因此开销较小。
– GateWay
Spring公司为了替换Zuul而开发的网关服务,底层为Netty,将在下面具体介绍。
– Nginx+lua
使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用,lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本,问题在于:无法融入到微服务架构中
– Kong
基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。 问题:只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。
GateWay
Spring Cloud Gateway 基于Spring Boot 2.x、Spring WebFlux和Project Reactor,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
特点:
1. 性能强劲:是Zuul的1.6倍
2. 功能强大:内置了很多实用的功能,例如转发、监控、限流等
3. 设计优雅,容易扩展
基本概念
路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个信息:
– id:路由标识、区别于其他route
– uri:路由指向的目的地uri,即客户端请求最终被转发到的微服务
– order:用于多个route之间的排序,数值越小排序越靠前,匹配优先级越高
– predicate:断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由
– filter:过滤器用于修改请求和响应信息
执行流程
1. Gateway Client向Gateway Server发送请求
2. 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
3. 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping
4. RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
5. 如果过断言成功,由FilteringWebHandler创建过滤器链并调用
6. 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应
搭建gateway环境
因为GateWay输入SpringCloud的,所以我们要导入对应依赖,一定要注意版本关系:
版本对应地址:https://spring.io/projects/spring-cloud
1. 父项目导入依赖:
<!--spring cloud Hoxton.SR5-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
2. 子项目导入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
3. 配置yml文件
server:
port: 9999
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
#开启注册中心路由功能
gateway:
discovery:
locator:
enabled: true
# 路由
routes:
- id: nacos-provider #路由ID,没有固定要求,但是要保证唯一,建议配合服务名
uri: http://localhost:9001/nacos-provider # 匹配提供服务的路由地址
predicates: # 断言
- Path=/lc/** # 断言,路径相匹配进行路由
4. 在9001项目中增加controller
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/lc")//路由路径
public class DemoController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/get")
public String getServerPort(){
return "库存-1:"+serverPort;
}
}
5. 启动nacos服务、启动9001、网关项目,访问接口:
GateWay实现负载均衡
自动负载均衡功能
Gateway注册到nacos中后,会通过nacos获取其他服务的信息,在yml中配置自动路由功能后,可以根据服务的名称和接口路径,直接访问项目,
1. 在yml中配置自动路由功能:
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
#开启注册中心路由功能
gateway:
discovery:
locator:
enabled: true
2. 访问规则:
http://localhost:网关端口/服务提供系统名称/接口路径
http://localhost:9999/nacos-provider/lc/get
配置负载均衡—推荐
主要为了解决访问过程中暴露服务端项目的问题,在yml中配置以下信息:
server:
port: 9999
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
#开启注册中心路由功能
gateway:
discovery:
locator:
enabled: true
# 路由
routes:
#路由ID,没有固定要求,但是要保证唯一,建议配合服务名
- id: nacos-provider
# 匹配提供服务的路由地址 lb代表负载均衡
uri: lb://nacos-provider
# 断言
predicates:
# 断言,路径相匹配进行路由
- Path=/lc/**
访问地址:
Gateway断言Predicate
概念
* After:匹配在指定日期时间之后发生的请求。
* Before:匹配在指定日期之前发生的请求。
* Between:需要指定两个日期参数,设定一个时间区间,匹配此时间区间内的请求。
* Cookie:需要指定两个参数,分别为name和regexp(正则表达式),也可以理解Key和Value,匹配具有给定名称且其值与正则表达式匹配的Cookie。
* Header:需要两个参数header和regexp(正则表达式),也可以理解为Key和Value,匹配请求携带信息。
* Host:匹配当前请求是否来自于设置的主机。
* Method:可以设置一个或多个参数,匹配HTTP请求,比如GET、POST
* Path:匹配指定路径下的请求,可以是多个用逗号分隔
* Query:需要指定一个或者多个参数,一个必须参数和一个可选的正则表达式,匹配请求中是否包含第一个参数,如果有两个参数,则匹配请求中第一个参数的值是否符合正则表达式。
* RemoteAddr:匹配指定IP或IP段,符合条件转发。
* Weight:需要两个参数group和weight(int),实现了路由权重功能,按照路由权重选择同一个分组中的路由
After
匹配在指定时间之后发生的请求,可以对应提前上线业务
server:
port: 9999
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: false # 是否与服务发现进行组合,通过ServiceID转发到具体的服务实例,默认为false,
# 设置为true便开启通过服务注册中心来自动根据SeviceID创建路由功能。
routes:
- id: nacos-provider # 路由ID,唯一不可重复,最好配合服务名
uri: lb://nacos-provider # 匹配提供服务的路由地址 lb://代表开启负载均衡
predicates: # 断言
- Path=/lc/** # 匹配对应地址
- After=2022-01-07T14:39:10.529+08:00[Asia/Shanghai] # 在这个时间之后的请求都能通过,当前没有为题以后,故意改为1个小时以后
Cookie
需要指定两个参数,分别为name和regexp(正则表达式),也可以理解Key和Value,匹配具有给定名称且其值与正则表达式匹配的Cookie。
简单理解就是路由规则会通过获取Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果匹配不上则不执行。
我们可以分为两种情况演示,Cookie匹配,Cookie不匹配
server:
port: 9999
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: false # 是否与服务发现进行组合,通过ServiceID转发到具体的服务实例,默认为false,
# 设置为true便开启通过服务注册中心来自动根据SeviceID创建路由功能。
routes:
- id: nacos-provider # 路由ID,唯一不可重复,最好配合服务名
uri: lb://nacos-provider # 匹配提供服务的路由地址 lb://代表开启负载均衡
predicates: # 断言
- Path=/lc/** # 匹配对应地址
# - After=2022-01-07T14:39:10.529+08:00[Asia/Shanghai] # 在这个时间之后的请求都能通过
- Cookie=username,[a-z]+ # 匹配Cookie的key和value(正则表达式)
Header
需要两个参数header和regexp(正则表达式),也可以理解为Key和Value,匹配请求携带信息。实际上就是请求头携带的信息,官网给出的案例是X-Request-Id,那我们就用这个做实验
server:
port: 9999
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: false # 是否与服务发现进行组合,通过ServiceID转发到具体的服务实例,默认为false,
# 设置为true便开启通过服务注册中心来自动根据SeviceID创建路由功能。
routes:
- id: nacos-provider # 路由ID,唯一不可重复,最好配合服务名
uri: lb://nacos-provider # 匹配提供服务的路由地址 lb://代表开启负载均衡
predicates: # 断言
- Path=/lc/** # 匹配对应地址
#- After=2022-01-07T14:39:10.529+08:00[Asia/Shanghai] # 在这个时间之后的请求都能通过
#- Cookie=username,[a-z]+
- Header=X-Request-Id,\d+ #表示数字
Host
匹配当前请求是否来自于设置的主机,这个比较比较简单,我们直接来试验
server:
port: 9999
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: false # 是否与服务发现进行组合,通过ServiceID转发到具体的服务实例,默认为false,
# 设置为true便开启通过服务注册中心来自动根据SeviceID创建路由功能。
routes:
- id: nacos-provider # 路由ID,唯一不可重复,最好配合服务名
uri: lb://nacos-provider # 匹配提供服务的路由地址 lb://代表开启负载均衡
predicates: # 断言
- Path=/lc/** # 匹配对应地址
#- After=2022-01-07T14:39:10.529+08:00[Asia/Shanghai] # 在这个时间之后的请求都能通过
#- Cookie=username,[a-z]+
#- Header=X-Request-Id,\d+ #表示数字
- Host=**.lc.com #匹配当前的主机地址发出的请求
Method
可以设置一个或多个参数,匹配HTTP请求,比如GET、POST
server:
port: 9999
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: false # 是否与服务发现进行组合,通过ServiceID转发到具体的服务实例,默认为false,
# 设置为true便开启通过服务注册中心来自动根据SeviceID创建路由功能。
routes:
- id: nacos-provider # 路由ID,唯一不可重复,最好配合服务名
uri: lb://nacos-provider # 匹配提供服务的路由地址 lb://代表开启负载均衡
predicates: # 断言
- Path=/lc/** # 匹配对应地址
#- After=2022-01-07T14:39:10.529+08:00[Asia/Shanghai] # 在这个时间之后的请求都能通过
#- Cookie=username,[a-z]+
#- Header=X-Request-Id,\d+ #表示数字
#- Host=**.lc.com #匹配当前的主机地址发出的请求
- Method=GET,POST # 匹配GET请求或者POST请求
Query
需要指定一个或者多个参数,一个必须参数和一个可选的正则表达式,匹配请求中是否包含第一个参数,如果有两个参数,则匹配请求中第一个参数的值是否符合正则表达式。
server:
port: 9999
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: false # 是否与服务发现进行组合,通过ServiceID转发到具体的服务实例,默认为false,
# 设置为true便开启通过服务注册中心来自动根据SeviceID创建路由功能。
routes:
- id: nacos-provider # 路由ID,唯一不可重复,最好配合服务名
uri: lb://nacos-provider # 匹配提供服务的路由地址 lb://代表开启负载均衡
predicates: # 断言
- Path=/lc/** # 匹配对应地址
#- After=2022-01-07T14:39:10.529+08:00[Asia/Shanghai] # 在这个时间之后的请求都能通过
#- Cookie=username,[a-z]+
#- Header=X-Request-Id,\d+ #表示数字
#- Host=**.lc.com #匹配当前的主机地址发出的请求
#- Method=GET,POST
- Query=id,.+ # 匹配请求参数,这里如果需要匹配多个参数,可以写多个Query
Weight
需要两个参数group和weight(int),实现了路由权重功能,按照路由权重选择同一个分组中的路由官网提供的演示yml
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: https://weighthigh.org
predicates:
- Weight=group1, 8
- id: weight_low
uri: https://weightlow.org
predicates:
- Weight=group1, 2
GateWay的Filter
路由过滤器允许以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应。路由过滤器的范围是特定的路由。Spring Cloud Gateway 包含许多内置的 GatewayFilter 工厂。
自定义filter:
1. 在gateway项目中新增filter包,新增过滤器类:
@Component
@Slf4j
public class MyFilter implements Ordered, GlobalFilter {
/**
*检查上送的信息中是否含有用户名字段,如果有,就放行
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String username=exchange.getRequest().getQueryParams().getFirst("username");
log.info("************username is "+username);
if(username==null){
log.info("************username is null ");
//返回状态码
exchange.getResponse().setStatusCode(HttpStatus.MULTI_STATUS);
//返回信息
return exchange.getResponse().setComplete();
}
//放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
2. 没传username时,过滤器生效。
3. 传username时,可以正常通过。
Seata
在学习seata之前,我们先来了解一下分布式事务的概念。
分布式事务概念
事务特性
* A(Atomic):原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失 败的情况。
* C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。比如:张三向李四转100元, 转账前和转账后的数据是正确状态这叫一致性,如果出现张三转出100元,李四账户没有增加100元这就出现了数 据错误,就没有达到一致性。
* I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事 务不能看到其他事务运行过程的中间状态。通过配置事务隔离级别可以避脏读、重复读等问题。
* D(Durability):持久性,事务完成之后,该事务对数据的更改会被持久化到数据库,且不会被回滚。
分布式事务概念
* 本地事务:同一数据库和服务器,称为本地事务
在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。
* 分布式事务:
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于**不同的分布式系统**的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
* 举例:
分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操 作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。
通过以上的图中我们可以看出,其实只要涉及到操作多个数据源,就可能会产生事务问题,当然在实际开发中我们要尽量避免这种问题的出现,当然如果避免不了,我们就需要进行解决,在我们的微服务系统架构中,目前比较好,比较常用的解决方案就是Seata。
分布式事务理论
随着互联化的蔓延,各种项目都逐渐向分布式服务做转换。如今微服务已经普遍存在,本地事务已经无法满足分布式的要求,由此分布式事务问题诞生。 分布式事务被称为世界性的难题,目前分布式事务存在两大理论依据:CAP定律 BASE理论。
CAP定律
这个定理的内容是指的是在一个分布式系统中、Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
一致性(C)在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
– 可用性(A)在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
– 分区容错性(P)以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择
CAP是无法同时存在的,以下通过这个例子来说明
当库存服务减库存以后,那么需要将数据同步到其他的服务上,这是为了保证数据一致性C,但是网络是不可靠的,所以我们系统就需要保证分区容错性P,也就是我们必须容忍网络所带来的的一些问题,此时如果我们想保证C那么就需要舍弃A,也就是说我们在保证C的情况下,就必须舍弃A,也就是CP无法保证高可用。
如果为了保证A,高可用的情况下,也就是必须在限定时间内给出响应,同样由于网络不可靠P,订单服务就有可能无法拿到新的数据,但是也要给用户作出响应,那么也就无法保证C一致性。所以AP是无法保证强一致性的。
如果我们想保证CA,也就是高可用和一致性,也就是必须保证网络良好才能实现,那么也就是说我们需要将库存、订单、用户放到一起,但是这种情况也就丧失了P这个保证,这个时候系统也就不是分布式系统了。
总结:在分布式系统中,p是必然的存在的,所以我们只能在C和A之间进行取舍,在这种条件下就诞生了BASE理论
BASE理论
BASE是Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
基本可用:基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性—-注意,这绝不等价于系统不可用。比如:
(1)响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
(2)系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面
– 软状态:软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
– 最终一致性:最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
基本可用:保证核心服务是可以使用的,至于其他的服务可以适当的降低响应时间,甚至是服务降级
**软状态:**存在中间状态,不影响整体系统使用,数据同步存在延时
**最终一致性:**再过了流量高峰期以后,经过一段时间的同步,保持各服务数据的一致
Seata的概念
2PC即两阶段提交协议,是将整个事务流程分为两个阶段,P是指准备阶段,C是指提交阶段。
1. 准备阶段(Prepare phase)
2. 提交阶段(commit phase)
– 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。
– (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
– 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的资源。
Seata术语
官网地址:https://seata.io/zh-cn/docs/overview/terminology.html
要了解Seata,首先我们要了解一下Seata的几个关键的概念:
– TC (Transaction Coordinator) – 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
– TM (Transaction Manager) – 事务管理器(发起者,同时也是RM的一种)
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
– RM (Resource Manager) – 资源管理器(每个参与事务的微服务)
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata-server下载与启动
官方下载地址:https://github.com/seata/seata/releases
- 解压下载的zip文件。
- 进入conf目录,修改文件registry.conf
4. 将registry.conf文件中registry下方的type改为nacos、将config下方的type改为nacos,作用是修改Seata的注册中心和配置中心为Nacos
5. 修改conf目录下的file.conf文件,把Seata的默认存储模式修改为数据库”DB”,同时需要配置JDBC
6. 将file.conf文件中,store下的mode改为db、修改db配置下的url、user、passowrd信息。
7. 启动nacos、启动seataserver,访问nacos,有seata的服务注册进来即可。
Seata-Server(TC)环境搭建
Server端存储模式(store.mode)支持三种:
1. file:单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高(默认)
2. DB:高可用模式,全局事务会话信息通过DB共享,相对性能差一些
3. redis:Seata-Server1.3及以上版本支持,性能较高,存在事务信息丢失风险,需要配合实际场景使用
- 修改seata-server模式为db-参考下载与启动。
- 在数据库创建database、执行建表语句
分别是global_table,branch_table,lock_table分别是全局事务会话表,分支事务会话表,锁数据表;
建表语句地址:https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
Seata配置Nacos注册中心和配置中心
Seata支持注册服务到Nacos,以及支持Seata所有配置放到Nacos配置中心,在Nacos中统一维护;高可用模式下就需要配合Nacos来完成
注册中心
Seata-server端配置注册中心,在registry.conf中加入配置注册中心nacos
注意:确保client与server的注册处于同一个namespace和group,不然会找不到服务。
配置中心
从v1.4.2版本开始,已支持从一个Nacos dataId中获取所有配置信息,你只需要额外添加一个dataId配置项。
首先你需要在nacos新建配置,此处dataId为seataServer.properties,配置内容参考https://github.com/seata/seata/tree/develop/script/config-center
的config.txt并按需修改保存
- 在nacos中新增配置信息如下:
2. 参照以下信息修改:
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.rpcRmRequestTimeout=5000
transport.rpcTmRequestTimeout=10000
transport.rpcTcRequestTimeout=10000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
#-------------修改这个区域的映射--------------
transport.shutdown.wait=3
service.vgroupMapping.mygroup=default # 事务分组
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
#------------------------------------------
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
store.mode=db
store.lock.mode=file
store.session.mode=file
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
#-----------修改这个区域的JDBC连接-----------
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
#------------------------------------------
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h
在client参考如下配置进行修改,
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group : "SEATA_GROUP"
namespace: ""
dataId: "seataServer.properties"
username: "nacos"
password: "nacos"
Seata-AT模式
概念:AT模式是一种无侵入的分布式事务解决方案,在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
整体机制
一阶段
在一阶段中,Seata会拦截“业务SQL“,首先解析SQL语义,找到要更新的业务数据,在数据被更新前,保存下来”undo”,然后执行”业务SQL“更新数据,更新之后再次保存数据”redo“,最后生成行锁,这些操作都在本地数据库事务内完成,这样保证了一阶段的原子性。
二阶段
相对一阶段,二阶段比较简单,负责整体的回滚和提交,如果之前的一阶段中有本地事务没有通过,那么就执行全局回滚,否在执行全局提交,回滚用到的就是一阶段记录的”undo Log”,通过回滚记录生成反向更新SQL并执行,以完成分支的回滚。当然事务完成后会释放所有资源和删除所有日志。
操作案例
模拟张三向李四转账100块,张三的账号减去100块,李四的账号加上100块。
1. 创建两个表,A银行的账户表以及B银行的账户表、undo_log表(事务回滚表)
CREATE TABLE `B_ACCOUNT` (
`ACCOUNT_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '账号id',
`ACCOUNT_NM` varchar(255) NOT NULL COMMENT '账号名称',
`ACCOUNT_MONEY` int(20) NOT NULL COMMENT '账号余额',
PRIMARY KEY (`ACCOUNT_ID`),
UNIQUE KEY `uk_A_ACCOUNT` (`ACCOUNT_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='账号信息表B';
CREATE TABLE `A_ACCOUNT` (
`ACCOUNT_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '账号id',
`ACCOUNT_NM` varchar(255) NOT NULL COMMENT '账号名称',
`ACCOUNT_MONEY` int(20) NOT NULL COMMENT '账号余额',
PRIMARY KEY (`ACCOUNT_ID`),
UNIQUE KEY `uk_A_ACCOUNT` (`ACCOUNT_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='账号信息表A';
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2. 创建项目Account1、Account2,依赖选择mybatis、web、driver
3. 加入nacos依赖、openfeign依赖、seata依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
4. 配置9005和9006的yml文件
server:
port: 9005
spring:
application:
name: seata-zhangsan
cloud:
nacos:
discovery:
server-addr: localhost:8848
alibaba:
# 事务组, 随便写
seata:
tx-service-group: mygroup
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
seata:
# 事务组名称,要和服务端对应
tx-service-group: mygroup
service:
vgroup-mapping:
# key是事务组名称 value要和服务端的机房名称保持一致
mygroup: default
5. 将seata服务端中的文件file.conf、registry.conf复制到当前项目的resourcees目录下
6. 编写代码,扣除9005项目中的100块钱,加上9006项目中的100块钱。
9005项目,扣除钱,controller,加上注解@GlobalTransactional
@RequestMapping("/zs")
@RestController
public class AccountController {
@Autowired
AccountService accountService;
@Autowired
OpenFeignService openFeignService;
@RequestMapping("/turnAccount")
@GlobalTransactional
public String turnAccount(){
int minusResult= accountService.minusMoney(100,1);
JsonResult<String> jsonResult = openFeignService.addMoney(100);
if(minusResult>0&&jsonResult.getCode()==200){
return "turnAccount success**********";
}
return "turnAccount error**********";
}
}
9005项目,对外通信的service:
@Service
@FeignClient("seata-lisi")//调用的服务方应用名称
public interface OpenFeignService {
@RequestMapping("/lisi/addAccount")//调用远程服务的哪个接口
JsonResult<String> addMoney(@PathVariable("money") int money);
}
9005项目,修改余额的mapper:
@Mapper
public interface AccountMapper {
@Update("UPDATE A_ACCOUNT SET ACCOUNT_MONEY=ACCOUNT_MONEY-#{money} WHERE ACCOUNT_ID=#{accountId} ")
int minusMoney(int money,int accountId);
}
9006项目,controller:
@RestController
@RequestMapping("/lisi")
public class AddAccountController {
@Autowired
AddAccountService accountService;
@RequestMapping("/addAccount")
@GlobalTransactional
public JsonResult<String> addAccount(@PathVariable("money") int money){
int addResult= accountService.addMoney(money);
if(addResult>0){
return new JsonResult<String>(200,"success");
}
return new JsonResult<String>(500,"error");
}
}
9006项目,修改余额的mapper:
@Mapper
public interface AddAccountMapper {
@Update("UPDATE B_ACCOUNT SET ACCOUNT_MONEY=ACCOUNT_MONEY-#{money} WHERE ACCOUNT_ID='1' ")
int addMoney(int money);
}
Seata-XA模式
Seata 1.2.0 版本重磅发布新的事务模式:XA 模式,实现对 XA 协议的支持。
我们从三个方面来深入分析:
1. XA模式是什么?
2. 为什么支持XA?
3. XA模式如何实现的,以及如何使用?
XA模式
首先我们需要先了解一下什么是XA?
XA 规范早在上世纪 90 年代初就被提出,用以解决分布式事务处理这个领域的问题。
注意:不存在某一种分布式事务机制可以完美适应所有场景,满足所有需求。
现在,无论 AT 模式、TCC 模式还是 Saga 模式,这些模式的提出,本质上都源自 XA 规范对某些场景需求的无法满足。
什么是XA协议
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的事务管理器与局部的资源管理器之间的接口。 XA规范 的目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。
XA 规范 使用两阶段提交(2PC,Two-Phase Commit)来保证所有资源同时提交或回滚任何特定的事务。
XA 规范 在上世纪 90 年代初就被提出。目前,几乎所有主流的数据库都对 XA 规范 提供了支持。
DTP模型定义如下角色:
– AP:即应用程序,可以理解为使用DTP分布式事务的程序
– RM:资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库的实例(MySql),通过资源管理器对该数据库进行控制,资源管理器控制着分支事务
– TM:事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理实务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
– DTP模式定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现的2PC又称为XA方案。
案例解释:
1. 应用程序(AP)持有订单库和商品库两个数据源。
2. 应用程序(AP)通过TM通知订单库(RM)和商品库(RM),来创建订单和减库存,RM此时未提交事务,此时商品和订单资源锁定。
3. TM收到执行回复,只要有一方失败则分别向其他RM发送回滚事务,回滚完毕,资源锁释放。
4. TM收到执行回复,全部成功,此时向所有的RM发起提交事务,提交完毕,资源锁释放。
Seata的XA模式
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
– 执行阶段:
– 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
– 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 *持久化*(即,之后任何意外都不会造成无法回滚的情况)
– 完成阶段:
– 分支提交:执行 XA 分支的 commit
– 分支回滚:执行 XA 分支的 rollback
为什么要在Seata中支持XA
为什么要在 Seata 中增加 XA 模式呢?支持 XA 的意义在哪里呢?
本质上,Seata 已经支持的 3 大事务模式:AT、TCC、Saga 都是 补偿型 的。
补偿型 事务处理机制构建在 事务资源 之上(要么在中间件层面,要么在应用层面),事务资源 本身对分布式事务是无感知的。
事务资源 对分布式事务的无感知存在一个根本性的问题:无法做到真正的 全局一致性 。
比如,一条库存记录,处在 补偿型 事务处理过程中,由 100 扣减为 50。此时,仓库管理员连接数据库,查询统计库存,就看到当前的 50。之后,事务因为异外回滚,库存会被补偿回滚为 100。显然,仓库管理员查询统计到的 50 就是 脏 数据。所以补偿型事务是存在中间状态的(中途可能读到脏数据)
XA的价值
与 补偿型 不同,XA 协议 要求 事务资源 本身提供对规范和协议的支持。
因为 事务资源 感知并参与分布式事务处理过程,所以 事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离,满足全局数据一致性。
比如,刚才提到的库存更新场景,XA 事务处理过程中,中间状态数据库存 50 由数据库本身保证,是不会仓库管理员的查询统计看到的。
除了 全局一致性 这个根本性的价值外,支持 XA 还有如下几个方面的好处:
1. 业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。
2. 数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。
3. 多语言支持容易:因为不涉及 SQL 解析,XA 模式对 Seata 的 RM 的要求比较少。
4. 传统基于 XA 应用的迁移:传统的,基于 XA 协议的应用,迁移到 Seata 平台,使用 XA 模式将更平滑。
XA模式的使用
我们从官方案例入手,具体的官方案例下载地址:https://github.com/seata/seata-samples
Seata中的Saga事务模式
基本概念
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务(执行处理时候出错了,给一个修复的机会)都由业务开发实现。
Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。
为什么需要Saga
之前我们学习的Seata分布式三种操作模型中所使用的的微服务全部可以根据开发者的需求进行修改,但是在一些特殊环境下,比如老系统,封闭的系统(无法修改,同时没有任何分布式事务引入),那么AT、XA、TCC模型将全部不能使用,为了解决这样的问题,才引用了Saga模型。
比如:事务参与者可能是其他公司的服务或者是遗留系统,无法改造,可以使用Saga模式。
Saga模式是Seata提供的长事务解决方案,提供了**异构系统的事务统一处理模型**。在Saga模式中,所有的子业务都不在直接参与整体事务的处理(只负责本地事务的处理),而是全部交由了最终调用端来负责实现,而在进行总业务逻辑处理时,在某一个子业务出现问题时,则自动补偿全面已经成功的其他参与者,这样一阶段的正向服务调用和二阶段的服务补偿处理全部由总业务开发实现。
Saga状态机
目前Seata提供的Saga模式只能通过状态机引擎来实现,需要开发者手工的进行Saga业务流程绘制,并且将其转换为Json配置文件,而后在程序运行时,将依据子配置文件实现业务处理以及服务补偿处理,而要想进行Saga状态图的绘制,一般需要通过Saga状态机来实现。
基本原理:
– 通过状态图来定义服务调用的流程并生成json定义文件
– 状态图中一个节点可以调用一个服务,节点可以配置它的补偿节点
– 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
– 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
Saga状态机的应用
官方提供了一个状态机设计器
官方文档地址:https://seata.io/zh-cn/docs/user/saga.html
Seata Safa状态机可视化图形设计器使用地址:https://github.com/seata/seata/blob/develop/saga/seata-saga-statemachine-designer/README.zh-CN.md
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/116521.html