# SpringCloudAlibaba-All-in-one **Repository Path**: hrbu-2022/spring-cloud-alibaba-all-in-one ## Basic Information - **Project Name**: SpringCloudAlibaba-All-in-one - **Description**: SpringCloudAlibaba 学习 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-05-19 - **Last Updated**: 2025-06-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SCA https://spring.io/projects/spring-cloud-alibaba # 介绍 在介绍Spring Cloud 全家桶之前,首先要介绍一下Netflix ,Netflix 是一个很伟大的公司,在Spring Cloud项目中占着重要的作用,Netflix 公司提供了包括Eureka、Hystrix、Zuul、Archaius等在内的很多组件,在微服务架构中至关重要,Spring在Netflix 的基础上,封装了一系列的组件,命名为:Spring Cloud Eureka、Spring Cloud Hystrix、Spring Cloud Zuul等,下边对各个组件进行分别得介绍: (1)Spring Cloud Eureka 我们使用微服务,微服务的本质还是各种API接口的调用,那么我们怎么产生这些接口、产生了这些接口之后如何进行调用那?如何进行管理哪? 答案就是Spring Cloud Eureka,我们可以将自己定义的API 接口注册到Spring Cloud Eureka上,Eureka负责服务的注册于发现,如果学习过Zookeeper的话,就可以很好的理解,Eureka的角色和 Zookeeper的角色差不多,都是服务的注册和发现,构成Eureka体系的包括:服务注册中心、服务提供者、服务消费者。 2018年6月底,Eureka 2.0 开源工作宣告停止,继续使用风险自负。 ![img](imgs/components.jpg) 1. 对应第3章内容:Naocs服务治理(服务注册与发现+配置中心) 2. 对应第4章内容:OpenFeign服务调用 3. 对应第5章内容:LoadBalance负载均衡 4. 对应第6章内容:Sentinele服务容错 5. 对应第7章内容:基于Spring Cloud Gateway的微服务网关 6. 对应第8章内容:Naocs配置中心 # 版本关系 [sca 和 cloud 以及boot的版本对应关系](https://sca.aliyun.com/docs/2022/overview/version-explain/?spm=7145af80.5dae7e57.0.0.54ea2fa53iqguN) # 项目准备 需要有一个聚合项目 ,组织子项目之间的关系,根项目 > sca-all-in-one maven项目 packing( **pom**) > > SCA SpringCLoud Alibaba ![image-20250519142553802](imgs/image-20250519142553802.png) ## 项目依赖 ```xml 4.0.0 com.neuedu.sca sca-all-in-one pom 1.0-SNAPSHOT sca-p01-resttemplate-provider-order sca-p02-restemplate-consumer-user 17 17 UTF-8 neuedu 2022.0.0.0-RC2 2022.0.0 3.0.2 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring-cloud-alibaba.version} pom import org.projectlombok lombok provided 1.18.36 com.baomidou mybatis-plus-boot-starter ${mp.version} org.apache.maven.plugins maven-compiler-plugin 3.8.1 17 17 UTF-8 -parameters org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} ``` # RestTempate服务调用 ![image-20250519145240647](imgs/image-20250519145240647.png) ## 准备项目 - - sca-p01-restemplate-provider-order 订单微服务(服务提供者)-用于演示restemplate调用 【端口7001】 - - sca-p02-restemplate-consumer-user 用户微服务(服务调用者、消费者)用于演示restemplate调用【端口8001】 ![image-20250519143435890](imgs/image-20250519143435890.png) ![image-20250519143513460](imgs/image-20250519143513460.png) ## 编写-provider-order ### 添加依赖 - start-web ``` sca-all-in-one com.neuedu.sca 1.0-SNAPSHOT 4.0.0 sca-p01-resttemplate-provider-order 17 17 org.springframework.boot spring-boot-starter-web ``` ### 编写启动类 ```java package org.jshand.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 作者: 张金山 * 创建时间:2025/2/11 19:32 星期二 * 描述: 启动类 * 项目: spring-cloud-alibaba - org.jshand.cloud * 作者的博客: https://blog.fulfill.com.cn */ @SpringBootApplication public class ProviderOrderApp7001 { public static void main(String[] args) { SpringApplication.run(ProviderOrderApp7001.class, args); } } ``` ### 配置文件 application.yaml ```yaml server: port: 7001 spring: application: name: ProviderApp ``` ### 编写Controller ```java package org.jshand.cloud.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.text.SimpleDateFormat; import java.util.Date; /** * 作者: 张金山 * 创建时间:2025/2/11 19:33 星期二 * 描述: 服务提供者控制器 * 项目: spring-cloud-alibaba - org.jshand.cloud.controller * 作者的博客: https://blog.fulfill.com.cn */ @RestController @RequestMapping("/provider") public class ProviderOrderController { /** * 获取当前应用名称 */ @Value("${spring.application.name}") private String appName; /** * 普通方法 * @return */ @RequestMapping("/index") String index(){ String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); return appName+"@"+time; } /** * 供用户微服模块调用获取用户订单的功能 * @param userId * @return */ @RequestMapping("/order/{userId}") String order(@PathVariable("userId") String userId){ String info = String.format("用户【%s】的订单信息",userId); return info; } } ``` ### 启动测试, 使用浏览器访问测试 [localhost:7001/provider/index](http://localhost:7001/provider/index) ![image-20250519144503119](imgs/image-20250519144503119.png) ## 编写-consumer-order ### 编写依赖 ```xml sca-all-in-one com.neuedu.sca 1.0-SNAPSHOT 4.0.0 sca-p02-restemplate-consumer-user 17 17 org.springframework.boot spring-boot-starter-web ``` ### 启动类 ```java package org.jshand.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 作者: 张金山 * 创建时间:2025/2/11 19:32 星期二 * 描述: 启动类 * 项目: spring-cloud-alibaba - org.jshand.cloud * 作者的博客: https://blog.fulfill.com.cn */ @SpringBootApplication public class ConsumerUserApp8001 { public static void main(String[] args) { SpringApplication.run(ConsumerUserApp8001.class, args); } } ``` ### 配置文件 ```yaml server: port: 8001 spring: application: name: ConsumerApp ``` ### 容器中声明一个RestTemplate类 ```java package org.jshand.cloud.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; /** * 作者: 张金山 * 创建时间:2025/2/11 19:44 星期二 * 描述: * 项目: spring-cloud-alibaba - org.jshand.cloud.config * 作者的博客: https://blog.fulfill.com.cn */ @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } ``` ### Controller 使用RestTemplate调用Provider ```java package org.jshand.cloud.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.text.SimpleDateFormat; import java.util.Date; /** * 作者: 张金山 * 创建时间:2025/2/11 19:33 星期二 * 描述: 服务提供者控制器 * 项目: spring-cloud-alibaba - org.jshand.cloud.controller * 作者的博客: https://blog.fulfill.com.cn */ @RestController @RequestMapping("/consumer") public class ConsumerUserController { /** * 获取当前应用名称 */ @Value("${spring.application.name}") private String appName; @Autowired private RestTemplate restTemplate; @RequestMapping("/user/index") String index(){ String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); return appName+"@"+time; } @RequestMapping("/user/order/{userId}") String getProvider(@PathVariable("userId") int userId){ String url = "http://localhost:7001/provider/order/"+userId; String orderInfo = restTemplate.getForObject(url, String.class); return "用户微服务:"+orderInfo; } } ``` ### 启动测试 ``` http://localhost:8001/consumer/user/order/100 ``` ![image-20250519145907451](imgs/image-20250519145907451.png) # Nacos注册微服务 ## 安装Nacos - 下载 [官网下载地址](https://nacos.io/download/nacos-server/?spm=5238cd80.47ee59c.0.0.189fcd36KZFYBc) ,[github下载地址](https://github.com/alibaba/nacos/releases?spm=5238cd80.1f77ca18.0.0.4d31e37ea9qQP6) - 解压 ![image-20250519153708624](imgs/image-20250519153708624.png) - 启动 打开cmd 执行 `startup.cmd -m standalone` ![image-20250519153916241](imgs/image-20250519153916241.png) ### 访问管理界面 ``` http://localhost:8848/nacos/index.html ``` ![image-20250519154141049](imgs/image-20250519154141049.png) ## 复制p01、p02 ![image-20250519161937044](imgs/image-20250519161937044.png) ## 将Prodiver注册到Naocs中 ### 添加依赖 添加nacos-discovery依赖 ```xml sca-all-in-one com.neuedu.sca 1.0-SNAPSHOT 4.0.0 sca-p03-discovery-nacos-provider-order 17 17 org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` ### 配置Naocs服务中心的地址 application.yaml ```yaml server: port: 7001 spring: application: name: ProviderApp cloud: nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} # 是否开启服务注册,默认为true enabled: true ``` ### 观察Naocs服务列表 启动应用 ![image-20250519162435515](imgs/image-20250519162435515.png) ![image-20250519162534925](imgs/image-20250519162534925.png) ## 将Consumer注册到Naocs中 ### 添加依赖 ```xlm sca-all-in-one com.neuedu.sca 1.0-SNAPSHOT 4.0.0 sca-p04-discovery-nacos-consumer-user 17 17 org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` ### 配置Naocs服务中心的地址 application.yaml ``` server: port: 8001 spring: application: name: ConsumerApp cloud: nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} ``` ### 观察Naocs服务列表 启动应用 ![image-20250519162847996](imgs/image-20250519162847996.png) ## 使用DiscoveryClient 调用微服务 ### 修改Controller ```java package org.jshand.cloud.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; /** * 作者: 张金山 * 创建时间:2025/2/11 19:33 星期二 * 描述: 服务提供者控制器 * 项目: spring-cloud-alibaba - org.jshand.cloud.controller * 作者的博客: https://blog.fulfill.com.cn */ @RestController @RequestMapping("/consumer") public class ConsumerUserController { /** * 获取当前应用名称 */ @Value("${spring.application.name}") private String appName; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; /** * 测试 discoveryClient 功能 * @return */ @RequestMapping("/user/serviceInstance") List serviceInstance(){ String orderServiceName = "ProviderApp"; // 微服务可是是集群,此处是集合,目前只有一个服务,获取第0个元素 List instances = discoveryClient.getInstances(orderServiceName); return instances; } /*** * { * "serviceId": "ProviderApp", * "instanceId": "192.168.232.1#7001#DEFAULT#DEFAULT_GROUP@@ProviderApp", * "host": "192.168.232.1", * "port": 7001, * "secure": false, * "metadata": { * "nacos.instanceId": "192.168.232.1#7001#DEFAULT#DEFAULT_GROUP@@ProviderApp", * "nacos.weight": "1.0", * "nacos.cluster": "DEFAULT", * "nacos.ephemeral": "true", * "nacos.healthy": "true", * "preserved.register.source": "SPRING_CLOUD" * }, * "uri": "http://192.168.232.1:7001", * "scheme": null * } * @param userId * @return */ @RequestMapping("/user/order/{userId}") String getProvider(@PathVariable("userId") int userId){ String orderServiceName = "ProviderApp"; // 微服务可是是集群,此处是集合,目前只有一个服务,获取第0个元素 List instances = discoveryClient.getInstances(orderServiceName); // ServiceInstance 代表服务实例 ServiceInstance serviceInstance = instances.get(0); String url = serviceInstance.getUri()+"/provider/order/"+userId; String orderInfo = restTemplate.getForObject(url, String.class); return "用户微服务(使用discoveryClient):"+orderInfo; } } ``` ### 测试 需要先启动Provider 在Naco中注册服务 启动Consumer 在浏览器中测试 请求Consumer - 获取微服务实例:http://localhost:8001/consumer/user/serviceInstance - 调用微服务: http://localhost:8001/consumer/user/order/100 ![image-20250519163727548](imgs/image-20250519163727548.png) ## 使用使用OpenFeign 调用微服务 封装了 discoveryClient动态获取微服务实例以及发送请求的过程 ### 添加依赖 在调用方ConsumerApp(p04项目) ```xml org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-loadbalancer ``` ### 编写接口 ```java @FeignClient("ProviderApp") public interface OrderProviderService { @RequestMapping("/provider/order/{userId}") String order(@PathVariable("userId") String userId); } ``` ### 调用OpenFeign接口实例 将ConsumerUserController的getProvider方法替换 成掉i用OpenFeign的形式 ![image-20250520093549931](imgs/image-20250520093549931.png) ```java @Autowired private OrderProviderService orderProviderService; @RequestMapping("/user/order/{userId}") String getProvider(@PathVariable("userId") String userId){ String orderInfo=orderProviderService.order(userId); return"用户微服务(使用 OpenFeign):"+orderInfo; } ``` ### 在启动类中添加@EnableFeignClients ![image-20250520093634578](imgs/image-20250520093634578.png) ### 测试 ![image-20250520093734891](imgs/image-20250520093734891.png) ## 使用OpenFeign 集群 复制 sca-p03-discovery-nacos-provider-order项目 到 sca-p03-discovery-nacos-provider-order-02 - pom.xml `sca-p03-discovery-nacos-provider-order`-->`sca-p03-discovery-nacos-provider-order-02` - application.yaml `7001` --> `7002` - 启动类 `ProviderOrderNoacsApp7001` --->`ProviderOrderNoacsApp7002` - 为了测试方便, 重新编写Controller用于区分两个应用的 ```java package org.jshand.cloud.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.text.SimpleDateFormat; import java.util.Date; /** * 作者: 张金山 * 创建时间:2025/2/11 19:33 星期二 * 描述: 服务提供者控制器 * 项目: spring-cloud-alibaba - org.jshand.cloud.controller * 作者的博客: https://blog.fulfill.com.cn */ @RestController @RequestMapping("/provider") public class ProviderOrderController { /** * 获取当前应用名称 */ @Value("${spring.application.name}") private String appName; @Value("${server.port}") private int port; /** * 普通方法 http://localhost:7001/provider/index * * @return */ @RequestMapping("/index") String index() { String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); return appName + "@" + time; } /** * http://localhost:7001/provider/index/order/100 * 供用户微服模块调用获取用户订单的功能 * * @param userId * @return */ @RequestMapping("/order/{userId}") String order(@PathVariable("userId") String userId) { String info = String.format("应用: %d,用户【%s】的订单信息", port,userId); return info; } } ``` 启动应用 两个P03 一个 p04 ### 测试 ![image-20250520100508363](imgs/image-20250520100508363.png) # Loadbalancer负载均衡 ## 常见的负载均衡策略 ### 轮询(Round Robin) 轮询是一种很简单的实现:**按顺序分配,每个服务器轮流接收一个连接**。 优点就是实现简单,请求均匀分配。缺点也恰恰在于请求均匀分配,因为后端服务器通常性能会有差异,所以希望性能好的服务器能够多承担一部分。也不适合对长连接和命中率有要求的场景。 ### 加权轮询(Weighted Round Robin) 加权本质是一种带优先级的方式,加权轮询就是一种**改进的轮询算法**,类似轮询,但服务器根据权重获取更多或更少请求 需要给后端每个服务器设置不同的权值,决定分配的请求数比例。这个算法应用就相当广泛了,对于无状态的负载场景,非常适合。 优点解决了服务器性能不一的情况,缺点是权值需要静态配置,无法自动调节。也不适合对长连接和命中率有要求的场景。 ### 随机Random ​ **随机把请求分配给后端服务器,分散负载**。请求分配的均匀程度依赖于随机算法了,因为实现简单,常常用于配合处理一些极端的情况,如出现热点请求,这个时候就可以random到任意一台后端,以分散热点。当然缺点也不言而喻。 ### 哈希Hash IP哈希:根据用户IP分配,相同IP的请求总是发给同一服务器。 显然,这非常适合维护长连接和提高命中率。但是它天生也有一些缺点。比如说,现在某个请求通过哈希被映射到S3上去了,如果S3宕机了,就不得不二次Hash,重新计算路由时会剔除宕机的后端。 ### 最小连接数LC 最小连接数(Least Connection),**把请求分配给活动连接数最小的后端服务器**。 它通过活动来估计服务器的负载。比较智能,但**需要维护后端服务器的连接列表**。 ### 加权最小连接数WLC 加权最小连接数(Weighted Least Connection),在后端服务器性能差异较大的情况下,可以优化LC的性能,权重高的服务器有更高几率被选中。 ### 最短响应时间LRT 最短响应时间(Least Response Time),**响应时间短的服务器优先接收新请求**。 平均响应时间可以通过ping探测请求或者正常请求响应时间获取。 RT(Response Time)是衡量服务器负载的一个非常重要的指标。对于响应很慢的服务器,说明其负载一般很高了,应该降低它的QPS。 **使用的比较多的是轮询、哈希(IP)、加权负载均衡、最短响应时间负载均衡策略。** # 早期的负载均衡Ribbon(**过时,了解即可**) Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。 简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。 因为 Ribbon 作为早期的客户端负载均衡工具,在 Spring Cloud 2020.0.0 版本之后已经被移除了,取而代之的是 Spring Cloud LoadBalancer,而且 Ribbon 也已经不再维护,所以LoadBalancer是 Spring 官方推荐的负载均衡解决方案。 ## LoadBalancer 内置的两种负载均衡策略 ### [轮询](https://so.csdn.net/so/search?q=轮询&spm=1001.2101.3001.7020)负载均衡策略(默认的) 从它的源码实现可以看出来默认的负载均衡策略是轮询的策略。 IDEA 搜索它的配置类 LoadBalancerClientConfiguration: ![img](imgs/1714100882702-38e152a7-81c0-404c-a687-5db572a44897.png) 进入到 RoundRobinLoadBalancer 这个类里边,定位到 getInstanceResponse 方法,就能看到轮询策略的关键代码: ```java private Response getInstanceResponse(List instances) { if (instances.isEmpty()) { if (log.isWarnEnabled()) { log.warn("No servers available for service: " + this.serviceId); } return new EmptyResponse(); } else if (instances.size() == 1) { return new DefaultResponse((ServiceInstance)instances.get(0)); } else { //关键算法 int pos = this.position.incrementAndGet() & Integer.MAX_VALUE; //进行轮询选择 ServiceInstance instance = (ServiceInstance)instances.get(pos % instances.size()); return new DefaultResponse(instance); } } ``` 理解关键代码: ```java int pos = this.position.incrementAndGet() & Integer.MAX_VALUE; ``` 观察源码我们发现: - this.position.incrementAndGet() 方法等价于 "++随机数 "。因为incrementAndGet是一个原子操作,保证了每次调用都会得到一个唯一的递增数值,position在构造方法中利用new Random()).nextInt(1000)进行赋值。 - & Integer.MAX_VALUE 这部分是一个位运算,它确保了如果 position 的值增加到超过 Integer.MAX_VALUE 时,不会产生负数。其一,在轮询算法中,如果计数器变成负数,那么取余操作可能会产生负的索引值,这是无效的; 其二,也可也保证在相同规则底下的公平性。 其实本质就是让下标从 0到1,再从1到0,如此循环往复,那么以上这种做法就很好实现,比如现在我初始化的随机数是6,那么6经过与Integer.MAX_VALUE(这个数值除了符号位为0,其他都为1)的与操作之后还是6。 6跟2进行取模运算的话,得到下标为0,由于随机数是自增的,接下来随机数为7,那么7进行与操作之后还是7,7进行模2之后得到下标为1。循环往复,就实现了轮询算法。 ### 随机负载均衡策略 搜索**ReactorLoadBalancer**观察源码可以知道,SpringCloud LoadBalancer内置了两种负载均衡策略: ![img](imgs/1714101970976-af409248-10e7-4649-bdf2-227b594cbccd.png) 第一种轮询的负载均衡策略(默认),上面已经介绍过了,那么接下来我们来看如何实现随机负载均衡策略。 实现随机负载均衡策略的步骤: ① 创建随机负载均衡策略 ② 设置随机负载均衡策略 ## 自定义配置轮训策略 添加配置类 @Configuration ```java package org.jshand.cloud.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer; import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/5/2010:48 */ @Configuration public class LoadBalancerConfiguration { @Bean ReactorLoadBalancer randomLoadBalancer( Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { // 获取微服务名称 String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); // 创建 RandomLoadBalancer // 注意这里注入的是 LazyProvider, // 这主要因为在注册这个 Bean 的时候相关的 Bean 可能还没有被加载注册, // 利用 LazyProvider 而不是直接注入所需的 Bean 防止报找不到 Bean 注入的错误。 return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name); } } ``` 在启动类中刚添加@loadbanancerClient注解 ``` @LoadBalancerClient(value="ProviderApp",configuration = LoadBalancerConfiguration.class) ``` ![image-20250520105308338](imgs/image-20250520105308338.png) ### 启动测试 ![image-20250520105425905](imgs/image-20250520105425905.png) # 熔断和限流 1.1.服务雪崩效应 在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。 如果下图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。 ![image.png](imgs/1714113517115-8165009e-824d-48a9-9c00-88d6fbd6e231.webp) 雪崩发生在微服务架构中,雪崩效应是一种因 “服务提供者” 的不可用导致 “服务消费者” 的不可用,并将不可用逐渐放大的一种蝴蝶效应。 导致雪崩效应发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。从源头上我们无法完全杜绝雪崩源头的发生,但是雪崩的根本原因来源于服务之间的强依赖,所以我们可以提前评估,做好熔断、降级、隔离与限流。 1.2 常见容错方案 为了防止雪崩效应的发生,可以采取以下一些措施,其中流程控制属于预防,其他属于出现故障后的应对 1.2.1 隔离(出现故障后的应对) 它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离。 ![image.png](imgs/1708583287608-910c3da4-e4d6-4321-9874-1cefd0beec07.webp) 1.2.2. 超时(出现故障后的应对) 服务容错中的超时设置是一种常见的策略,用于防止系统因为等待服务响应而发生长时间的阻塞,从而影响系统的整体性能和可用性。设置适当的超时时间可以使系统在出现问题时快速失败,避免长时间等待导致的资源浪费和性能下降。 在分布式系统中,服务之间的调用是通过网络进行的,网络延迟、服务响应时间等因素都会影响到调用的时间。如果一个服务调用等待超过了设定的超时时间,系统可以认为这个服务调用失败,进而进行相应的容错处理,如返回默认值、尝试重试、切换到备用服务等。要设置服务调用的超时时间,通常需要在应用程序中明确指定超时时间,例如在发起服务调用时设置请求的超时时间。 ![image.png](imgs/1708583426288-17901cf0-c57a-49c7-afb9-1a09a09bb7f9.webp) 1.2.3. 限流降级(预防故障) 在微服务系统中,一个对外的业务功能可能会涉及很长的服务调用链路。当其中某个服务出现异常,如果没有服务调用保护 机制可能会造成该服务调用链路上大量相关服务直接或间接调用的服务器仍然持续不断发起请求,最终导致相关的所有服务资源耗尽产生异常发生雪崩效应。限流和降级分别作为在流量控制和服务保护方面的两个重要手段,可以有效地应对此类问题。 1.2.3.1. 限流 限流是一种针对服务提供者的策略,用于控制对特定服务接口或服务实例的访问量。其目的在于保护服务提供者免受过大请求流量的影响,确保服务稳定性。限流措施可以在服务提供者或服务消费者两端实现,通过设定流量阈值并采取排队、拒绝请求或返回错误信息等方式来控制流量,从而保护服务。 ![img](imgs/1708583785102-c455cde4-39d1-428e-92ca-80cca803c3f6.webp) 1.2.3.2. 降级 降级是针对服务消费者的应对策略,在服务出现异常或限流时,通过对服务调用进行降级处理,确保消费者端能够在异常情况下正常工作。降级的目的在于转变为弱依赖状态,使系统能够在服务不可用时提供基本的功能或数据。这种策略可以在服务消费者端实施,通过返回默认值、提供备用数据或简化功能等方式来保证系统的可用性。 ![image.png](imgs/1708591386687-37b87bbe-eaef-4d4e-bab4-835232ec7a0c.webp) 总体而言,限流和降级作为微服务架构中的重要机制,尽管在实现上可能有多种方式,但它们都着眼于保护服务提供者和消费者,在面对异常情况时确保系统稳定运行。限流关注于保护服务提供者,控制请求流量;而降级则关注于服务消费者,确保在服务不可用或异常情况下提供基本的功能。 1.2.4. 熔断(出现故障后的应对) 服务熔断是一种用于保护分布式系统免受故障的影响的机制。它是通过监控服务调用的情况,并在出现故障时快速失败来防止故障的扩散。服务熔断可以有效地减少系统的资源消耗,并提高系统的稳定性和可用性。 服务熔断的核心思想是:当服务调用失败率超过了一定的阈值时,系统会进入熔断状态,停止向故障服务发送请求,而是直接返回预先设定的错误响应,从而避免对故障服务的继续请求,减轻其负载,防止故障扩散。当服务熔断一段时间后,系统会尝试恢复,重新允许对服务的请求。 ![image.png](imgs/1708591608908-39dce1aa-e4d6-43ff-90d9-0238123e265f.webp) 以下是服务熔断的一些关键特点和优势: 1快速失败:在出现故障时,服务熔断会快速失败,避免长时间的等待和资源浪费。 2自我修复:一段时间后,服务熔断会尝试恢复,重新允许对服务的请求,实现自我修复。 3减少资源消耗:服务熔断可以减少对故障服务的请求,降低系统的资源消耗,提高系统的性能和可用性。 4提高系统稳定性:通过快速失败和限制对故障服务的请求,服务熔断可以防止故障扩散,提高系统的稳定性。 在实践中,服务熔断通常与断路器模式结合使用。断路器是一种状态机,用于监控服务调用的状态,并在现故障时打开或者关闭断路器。一旦断路器打开,系统就会进入熔断状态,停止对故障服务的请求。当一定时间内服务调用正常时,断路器会尝试关闭,重新允许对服务的请求。 服务熔断一般有三种状态: 熔断关闭状态(Closed) 服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制 熔断开启状态(Open) 后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法 半熔断状态(Half-Open) 尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状 态。 1.3 常见的容错组件(仅介绍主流两种) 在微服务架构中,容错组件是保障系统稳定性和可靠性的关键部分。容错组件有很多,比如: 1.Netflix Hystrix 2.Alibaba Sentinel 3.Resilience4j 4.Spring Retry 5.Hazelcast 以下是两款容错组件的对比: Hystrix Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。 Sentinel Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。 下面是2个组件在各方面的对比: ![image.png](imgs/1714281026496-0fb4bfb7-95eb-44a7-b64d-0281ccc79235.webp) Hystrix和Sentinel都是服务熔断器,用于提高分布式系统的弹性。它们的主要区别在于实现方式、适用场景和资源模型设计。 ●Hystrix基于命令模式设计,将外部资源的调用封装在命令对象中,通过线程池或信号量来实现隔离。它提供了丰富的配置选项,如线程池大小、超时时间等,以实现对系统资源的有力控制。Hystrix更适用于需要高并发、快速响应的场景,因为它可以快速隔离和恢复故障。 ●Sentinel则基于流量控制和熔断降级的思想,可以与Spring Cloud、gRPC、Dubbo等框架集成。它通过定义资源规则和应用策略来实现对系统资源的控制。Sentinel更适用于需要流量控制和熔断降级的场景,它可以根据系统负载和响应时间来实现自动熔断和降级操作。 具体选择哪个工具取决于系统的具体需求和场景。 若有收获,就点个赞吧 # Sentinel入门 ## 1.1 限流 流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。 限流是一种流量控制的有效手段,用于控制对特定服务接口或服务实例的访问量。其目的在于保护服务提供者免受过大请求流量的影响,确保服务稳定性。限流措施可以在服务提供者或服务消费者两端实现,通过设定流量阈值并采取排队、拒绝请求或返回错误信息等方式来控制流量,从而保护服务。 限流的策略包括: 1采取排队 2拒绝请求 3返回错误信息 ## 1.2 降级 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。 降级是针对服务消费者的应对策略,在服务出现异常或限流时,通过对服务调用进行降级处理,确保消费者端能够在异常情况下正常工作。降级的目的在于转变为弱依赖状态,使系统能够在服务不可用时提供基本的功能或数据。这种策略可以在服务消费者端实施,通过返回默认值、提供备用数据或简化功能等方式来保证系统的可用性。 ![img](imgs/1715082352519-55bb8dd2-7892-4a02-afa8-f5a1eda773da.webp) 降级策略包括: 1、返回默认值 2、提供备用数据 3、简化功能 ## 1.3 熔断 熔断就像电路中的保险丝短路保护电路系统一样,A服务调用B服务的某个方法,但是由于B服务的网络不稳定,导致我们整个调用链路变慢,那么这个时候我们就停止调用B服务,防止把整个系统拖垮。所以说熔断是为了防止系统因某些服务的故障而整体崩溃,类似于电路中的保险丝。在检测到下游服务异常时,自动停止向该服务发送请求,并在一定时间后** 尝试恢复**。 **注意:** 即使 Sentinel 实现了服务熔断,Nacos 作为服务注册和发现的组件,理论上仍然能够发现注册的服务实例,但是调用这些服务可能会失败,因为熔断机制已经介入。服务消费者需要适当地处理这种失败情况。 # Sentinel 概述 官网: https://sentinelguard.io/zh-cn/docs/introduction.html Spring Cloud Alibaba 集成的开箱即用限流降级方案来自 [Sentinel](https://github.com/alibaba/Sentinel) ,其以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 ![img](imgs/1719190749724-6a547942-27b3-42a0-b2c5-7cbc68e3658b.webp) Sentinel工作原理图如下: ![img](imgs/1719190767510-4ce80de4-7779-44d6-bbe6-9ea8d0d7879c.webp) # 3、快速开始 Sentinel 的使用可以分为两个部分: ●核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。 ●控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。 ## 3.1 Sentinel控制台 Sentinel 提供了开箱即用的控制台,开启控制台需要 3 个步骤: ### 3.1.1 获取控制台 您可以从 [Release 页面](https://github.com/alibaba/Sentinel/releases/download/1.8.7/sentinel-dashboard-1.8.7.jar) 下载最新版本的控制台 jar 包。 此次课程sentinel-dashboard-1.8.7.jar ### 启动控制台 ``` java -jar sentineldashboard1.8.7.jar --server.port=9000 或者 java -jar sentineldashboard1.8.7.jar -Dserver.port=9000 ``` ```` D:\app>java -jar sentineldashboard1.8.7.jar --server.port=9000 INFO: Sentinel log output type is: file INFO: Sentinel log charset is: utf-8 INFO: Sentinel log base directory is: C:\Users\Administrator\logs\csp\ INFO: Sentinel log name use pid is: false INFO: Sentinel log level is: INFO . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.5.12) 2025-05-26 14:53:54.409 INFO 178884 --- [ main] c.a.c.s.dashboard.DashboardApplication : Starting DashboardApplication using Java 17.0.12 on WIN-20240903EPN with PID 178884 (D:\app\sentineldashboard1.8.7.jar started by Administrator in D:\app) 2025-05-26 14:53:54.412 INFO 178884 --- [ main] c.a.c.s.dashboard.DashboardApplication : No active profile set, falling back to 1 default profile: "default" 2025-05-26 14:53:56.539 INFO 178884 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 9000 (http) 2025-05-26 14:53:56.550 INFO 178884 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2025-05-26 14:53:56.550 INFO 178884 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.60] 2025-05-26 14:53:56.665 INFO 178884 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2025-05-26 14:53:56.666 INFO 178884 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2163 ms 2025-05-26 14:53:56.748 INFO 178884 --- [ main] c.a.c.s.dashboard.config.WebConfig : Sentinel servlet CommonFilter registered 2025-05-26 14:53:57.417 INFO 178884 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9000 (http) with context path '' 2025-05-26 14:53:57.426 INFO 178884 --- [ main] c.a.c.s.dashboard.DashboardApplication : Started DashboardApplication in 3.681 seconds (JVM running for 4.652) ```` ### 登录控制台 地址: http://192.168.84.47:9000/#/dashboard 默认的用户名、密码 sentinel /sentinel ![image-20250526145529194](imgs/image-20250526145529194.png) ## 在ProviderApp中集成sentinel 在`sca-p03-discovery-nacos-provider-order`项目中添加操作 - 添加依赖 ``` com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` ```xml sca-all-in-one com.neuedu.sca 1.0-SNAPSHOT 4.0.0 sca-p03-discovery-nacos-provider-order 17 17 org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` - 配置 - sentinel 客户端的http server的端口号 : 8719 - 配置sentinel 控制台的地址,就是浏览器中的访问地址`192.168.84.47:9000` 在application.yaml中添加配置 ```yaml server: port: 7001 spring: application: name: ProviderApp cloud: nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} # 配置 sentinel sentinel: transport: # 客户端的端口号 port: 8719 # 控制台地址 dashboard: localhost:9000 ``` *sentinel*是 *懒加载*的,也就是第一次被调用时才加载 启动 p03、p04项目,从浏览器发请求p04间接请求03 使用Apifox 请求 p04 ![image-20250526150632034](imgs/image-20250526150632034.png) # Sentinel控制台 ## 1. 查看机器列表以及健康情况 https://sentinelguard.io/zh-cn/docs/dashboard.html 当您在机器列表中看到您的机器,就代表着您已经成功接入控制台;如果没有看到您的机器,请检查配置,并通过 ${user.home}/logs/csp/sentinel-record.log.xxx 日志来排查原因,详细的部分请参考 [日志文档](https://sentinelguard.io/zh-cn/docs/logs.html)。 ## 2. 实时监控和簇点链路 QPS(Query Per Second):每秒请求数,即服务器一秒内处理多少个请求。 ### 2.1 "簇点链路"中显示刚刚调用的资源 簇点链路页面实时的去拉取指定客户端资源的运行情况。它一共提供两种展示模式:一种用树状结构展示资源的调用链路,另外一种则不区分调用链路展示资源的运行情况。 **注意:** 簇点监控是内存态的信息,它仅展示启动后调用过的资源。 树状视图 ![img](imgs/1719209743620-ac9033ca-ec80-43d8-8672-35b6e67d8e61.png) 列表视图 ![img](imgs/1719209825944-74f44946-1576-4e25-a337-5e90d6b1069a.png) ![img](imgs/1719194242779-959a9d56-1c1f-4247-8f97-b0b65a57952a.png) ### 2.2 "实时监控"汇总资源信息(集群聚合) 同时,同一个服务下的所有机器的簇点信息会被汇总,并且秒级地展示在"实时监控"下。 监控接口的通过的QPS和拒绝的QPS 。同一个服务下的所有机器的簇点信息会被汇总,并且秒级地展示在”实时监控”下。 **注意: 实时监控仅存储 5 分钟以内的数据**,如果需要持久化,需要通过调用实时监控接口来定制。 ![img](imgs/1719194113573-c34a6e34-fd88-475e-b6a8-dd2b9d256700.png) ![img](imgs/1719209879096-4bc2c666-c05e-47dc-b4f7-2da091cfb68c.png) ## 3. 流控规则(避免故障) 为了方便观察,我们停止掉8072服务,只保留8071 ### 3.4.1 限流(流量控制)配置 需求在1秒钟只处理1个请求,同1秒钟发生多个请求 后面的请求限流 簇点链路菜单下->点击对应接口右侧的流控按钮,打开流控设置窗口 ![image-20250526153320040](imgs/image-20250526153320040.png) ![image-20250526153605318](imgs/image-20250526153605318.png) 1.资源名:需要进行限流的资源对象 2.针对来源:流控来源,default 不区分调用来源 3.阈值类型:QPS、并发线程数 QPS(Query Per Second):每秒请求数,即服务器一秒内处理多少个请求。 并发线程数:1秒内并发多少个线程 **举个例子** QPS:假设一个请求响应时间是0.2秒,那么1秒的QPS就是5,如果我们设置1秒只允许5个请求的话,新的请求进来就会被拒绝。 线程数:1秒内只允许5个线程,此时5个线程的时间窗口(运行周期,请求处理时间)是2秒,如果有新的请求进来会直接被拒绝,因为在1秒内5个线程都占满了。 4.单机阈值:限流阈值 5.是否集群:如果是集群对象,这里要勾上,如果是单机,不勾 6.流控模式:关联流控和链路流控 关联流控用于处理资源竞争情况,当核心资源(资源1)达到阈值后,限流资源链路流控可在业务层指定某个链路进行控流。 7.流控效果 快速失败:直接拒绝请求。 Warm Up(预热):前期QPS是设置的QPS的三分之一,随着时间的加载,慢慢到达设置QPS。 排队:当设置QPS之后,后续请求默认进行排队,有限时间内排队。 ### 3.4.2 QPS限流测试 QPS :1 http://127.0.0.1:8001/consumer/user/order/200 请求上面的地址会如果间隔超过1秒钟请求一次 能正常获取结果,如果1秒钟多次点击比较快就会触发快速失败的规则 ![image-20250526154142331](imgs/image-20250526154142331.png) # 使用Jmeter进行压力测试 ![image-20250527091035999](imgs/image-20250527091035999.png) # 熔断 ### 4.1慢调用比例 #### 4.1.1概念 指定多长时间的请求判定为慢请求,当慢请求比例到达设置的值时,自动熔断,熔断的时间自行设置。 解释:1、最大RT:调用接口的最大时间(单位毫秒) 2、比例阈值:持续一段时间后的错误比例临界值 例如,1000ms 的统计时长,最小请求数为5,也就是熔断需要满足每秒请求数大于5,才开始统计慢调用比例。 为了测试方便我们新添加一个方法 ```java @GetMapping("/slow") public String slow(){ System.out.println(">>收到请求"); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } return"慢调用响应"; } ``` ![image-20250527104951244](imgs/image-20250527104951244.png) ![image-20250527105008201](imgs/image-20250527105008201.png) ![image-20250527105046914](imgs/image-20250527105046914.png) ## 慢调用熔断器的源码执行流程 1. 当一个请求进入服务器后,会先被sentinel管控 - - 如果不符合熔断规则,直接放行 - 如果符合了熔断规则,熔断器就会从close状态变成 open状态 1. 当下一个请求进入sentinel后,会判断状态是否为open状态 - - 如果是,就判断当前时间是否超过熔断时间(超时时间) - - - 如果超过就把状态变成halfOpen状态并进入业务逻辑 - 如果不超过,返回false执行熔断 - - 如果不是,就返回 flase 执行熔断 1. 当业务逻辑执行结束后判断业务执行的时间是否超过熔断阈值 - - 如果超过了,就会把熔断器从halfOpen变成open状态 - 如果没有超过,就把熔断器都变成close 状态 ## 4.1异常比例 ![image-20250528135132091](imgs/image-20250528135132091.png) 新增测试方法 ```java @GetMapping("/withException") public String withException(){ System.out.println(">>收到请求==withException"); int result=0/0; return"慢调用响应"; } ``` 当请求的异常比例达到设置的阈值时,触发熔断。 ![img](imgs/1719196805212-bd8fa554-cc54-4910-a265-53cc6fc1c412.png) ### 4.2异常数 当请求的异常数量达到设置的阈值时,触发熔断。 ![img](imgs/1719196822785-de92ea37-754f-48e2-bc6a-06fea25f7ed6.png) # 自定义相应内容 当触发流控规则、熔断的规则Sentinel默认返回的是 "Blocked by Sentinel (flow limiting)",需要返回个性化的内容 @SentinelResource注解进行配置 ```java @SentinelResource(value = "provider_slow",blockHandler = "block") @GetMapping("/slow") public String slow() { System.out.println(">>收到请求"); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } return "慢调用响应"; } public String block(BlockException exception){ System.out.println(exception.getMessage()); return "被限流了"; } ``` 浏览器第一次访问 http://localhost:7001/provider/slow ![image-20250529093930951](imgs/image-20250529093930951.png) 添加流控规则 ![image-20250529094034157](imgs/image-20250529094034157.png) ![image-20250529094054700](imgs/image-20250529094054700.png) 快速的访问让他触发流控规则,查看响应结果似乎自定义的 内容 ![image-20250529094005258](imgs/image-20250529094005258.png) # 全局的降级的策略 可能产生限流方法 ``` package org.jshand.cloud.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource; import org.jshand.cloud.CustomerBlockHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/5/2910:30 */ @RestController public class RateLimitController { @SentinelResource(value = "customerBlockHandlerAndFallback" , blockHandlerClass = CustomerBlockHandler.class , blockHandler = "handleException1" , fallbackClass = CustomerBlockHandler.class , fallback = "handleFallBackException1") @GetMapping("/rateLimit/customerBlockHandlerAndFallback/{p1}") public String customerBlockHandlerAndFallback(@PathVariable("p1") int p1){ if(p1 == 0){ throw new RuntimeException("p1参数为0,报错!!!"); } return " customerBlockHandlerAndFallback 程序 正常OK " ; } } ``` 全局的的服务降级响应策略 ```java package org.jshand.cloud; import com.alibaba.csp.sentinel.slots.block.BlockException; import org.springframework.web.bind.annotation.PathVariable; public class CustomerBlockHandler { // 方法需要是 static ,参数与controller中的方法参数一致,另外加 BlockException参数 public static String handleException1(@PathVariable("p1") int p1 , BlockException exception){ return " sentinel 配置 自定义 -- 限流 --- handleException1 "; } // 方法需要是 static ,参数与controller中的方法参数一致,另外加 Throwable 参数 public static String handleFallBackException1(@PathVariable("p1") int p1 , Throwable exception){ return " sentinel 配置 自定义 -- 服务降级 --- handleFallBackException1 , 异常::: " + exception.getMessage(); } } ``` 启动应用 7001 测试没有被限流的 状态 ![image-20250529103339740](imgs/image-20250529103339740.png) ![image-20250529103434202](imgs/image-20250529103434202.png) 快速访问触发限流 http://localhost:7001/rateLimit/customerBlockHandlerAndFallback/100 ![image-20250529103456626](imgs/image-20250529103456626.png) http://localhost:7001/rateLimit/customerBlockHandlerAndFallback/0 ![image-20250529103635346](imgs/image-20250529103635346.png) # 跟OpenFeign整合Sentinel 当8001调用的资源 7001(Provider)被限流了,OpenFeign客户端抛异常的 ![image-20250529113206097](imgs/image-20250529113206097.png) 同时Consumer接口到错误,并打印到控制台、 ![image-20250529113309387](imgs/image-20250529113309387.png) 希望当OpenFeign调用过程中服务不可以用,已有一个降级的策略 ## 整合 - 在ConsumerApp中添加 Sentinel依赖 - 设置OpenFeign客户端 如果发生错误激活Sentinel配置 - 定义一个 OrderProviderService 客户端的实现 - 在@FeignClient注解中添加配置 ### 依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` ### 激活Sentinel配置 ```yaml # 让Feign客户端 使用Sentinel feign: sentinel: enabled: true ``` ![image-20250529113822982](imgs/image-20250529113822982.png) ### 编写客户端降级的代码 客户端添加 讲解的实现代码`fallback = OrderProviderServiceSentinelClient.class, configuration = FeignConfiguration.class ` ```java package org.jshand.cloud.service; import org.jshand.cloud.config.FeignConfiguration; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @FeignClient(value = "ProviderApp",fallback = OrderProviderServiceSentinelClient.class, configuration = FeignConfiguration.class ) public interface OrderProviderService { @RequestMapping("/provider/order/{userId}") String order(@PathVariable("userId") String userId); } ``` ```java package org.jshand.cloud.service; import com.alibaba.csp.sentinel.annotation.SentinelResource; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/5/2911:39 */ public class OrderProviderServiceSentinelClient implements OrderProviderService { @Override @SentinelResource(value = "/provider/order/{userId}", fallback = "fallback") public String order(String userId) { return "服务被讲解了,服务器开小差了..."; } } ``` ```java package org.jshand.cloud.config; import org.jshand.cloud.service.OrderProviderServiceSentinelClient; import org.springframework.context.annotation.Bean; public class FeignConfiguration { @Bean public OrderProviderServiceSentinelClient client() { return new OrderProviderServiceSentinelClient(); } } ``` ### 测试 重新启动8001 ![image-20250529114421038](imgs/image-20250529114421038.png) # sca-SpringCloudGateWay https://docs.spring.io/spring-cloud-gateway/reference/index.html - 准备用于gateway转发的 两个项目 - payment 支付 - PaymentController - pay: 支付成功了 - Order 订单 - OrderController - orderList: 支付成功了 ## 创建项目 第一个payment项目 Controller App ![image-20250529140240117](imgs/image-20250529140240117.png) ### 编写配置文件 ```yaml spring: application: name: PayMentApp # 应用端口 7001 server: port: 7001 ``` ### 编写启动类 ```java package com.neuedu.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/5/2914:04 */ @SpringBootApplication public class PaymentApp7001 { public static void main(String[] args) { SpringApplication.run(PaymentApp7001.class); } } ``` ### 编写controller ```java package com.neuedu.gateway.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/5/2914:05 */ @RestController public class PaymentController { /** * http://locahost:7001/pay * @return */ @RequestMapping("/pay") public String pay(){ return "订单已支付"; } } ``` ### 测试 http://localhost:7001/pay ![image-20250529140906432](imgs/image-20250529140906432.png) ## 另一个项目 ![image-20250529141136971](imgs/image-20250529141136971.png) ### 编写配置文件 ```yaml spring: application: name: OrderApp # 应用端口 8001 server: port: 8001 ``` ### 编写启动类 ```java package com.neuedu.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/5/2914:04 */ @SpringBootApplication public class OrderApp8001 { public static void main(String[] args) { SpringApplication.run(OrderApp8001.class); } } ``` ### 编写controller ```java package com.neuedu.gateway.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/5/2914:05 */ @RestController public class OrderController { /** * http://locahost:8001/orderList * @return */ @RequestMapping("/orderList") public String orderList(){ return "以获取订单列表"; } } ``` ### 测试 http://localhost:8001/orderList ![image-20250529141446344](imgs/image-20250529141446344.png) ## 引入gateway - 创建项目 - sca-gateway: 8080 - 引入依赖 - gateway - 设置路由表 ![image-20250529142531270](imgs/image-20250529142531270.png) ```xml sca-all-in-one com.neuedu.sca 1.0-SNAPSHOT ../../pom.xml 4.0.0 sca-07-gateway 17 17 org.springframework.cloud spring-cloud-starter-gateway ``` ### 配置路由规则 ```yaml server: port: 7000 spring: application: name: neusoft-gateway cloud: gateway: # 配置网关的路由规则 routes: - id: payment # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: http://localhost:7001 # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 # 路由的优先级,数字越小级别越高 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 # localhost:7000/payment/pay --> http://localhost:7001/pay # localhost:7000/payment/* --> http://localhost:7001/* - Path=/payment/** # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 #http://localhost:7000/payment/pay --> http://localhost:7001/payment/pay #http://localhost:7000/payment/pay --> http://localhost:7001/pay filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - id: order # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: http://localhost:8001 # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 # 路由的优先级,数字越小级别越高 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 # localhost:7000/order/orderList --> http://localhost:7001/orderList # localhost:7000/order/* --> http://localhost:8001/* - Path=/order/** # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 #http://localhost:7000/order/orderList --> http://localhost:7001/order/orderList #http://localhost:7000/order/orderList --> http://localhost:7001/orderList filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 ``` ### 编写启动类 ```java package com.neuedu.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/5/2914:34 */ @SpringBootApplication public class GateWayApp7000 { public static void main(String[] args) { SpringApplication.run(GateWayApp7000.class, args); } } ``` ### 测试 单独访问 payment 、order - http://localhost:7001/pay - http://localhost:8001/orderList 使用gateway转发 - http://localhost:7000/order/orderList - http://localhost:7000/payment/pay # JDK8+中的断言 ```java // List list = Arrays.asList(100,400,200,700); // // Stream result = list.stream().filter(new Predicate() { // @Override // public boolean test(Integer num) { // return num % 200 == 0; // } // }); // // result.forEach(System.out::println); // // // System.out.println("======================="); // // result = list.stream().filter(new Predicate() { // @Override // public boolean test(Integer num) { // return num % 200 != 0; // } // }); // // result.forEach(System.out::println); ``` # 使用Nacos注册中心整合服务和gateway(网关) ## 将Payment(7001)服务 注册到Nacos中 启动Naocs ![image-20250530084556882](imgs/image-20250530084556882.png) ### 添加依赖 在sca-05-payment的pom.xml中添加 starter-discoveryClient ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` ### 配置naocs服务地址,以及当前应用的名字 ```yaml spring: application: name: PayMentApp cloud: nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} # 应用端口 7001 server: port: 7001 ``` 添加EnableDiscoveryClient ![image-20250530085252155](imgs/image-20250530085252155.png) ## 将sca-06-order、sca-07-gateway ### 06添加依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` ### sca-07-gateway ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-loadbalancer ``` ### 修改两个配置文件 sca-06-order ```xml spring: application: name: OrderApp cloud: nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} # 应用端口 8001 server: port: 8001 ``` sca-07-gateway ```yaml server: port: 7000 spring: application: name: neusoft-gateway # 微服务相关配置 cloud: # 服务注册 和发现 nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} gateway: # 配置网关的路由规则 routes: - id: payment # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentApp # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 # 路由的优先级,数字越小级别越高 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 # localhost:7000/payment/pay --> http://localhost:7001/pay # localhost:7000/payment/* --> http://localhost:7001/* - Path=/payment/** # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 #http://localhost:7000/payment/pay --> http://localhost:7001/payment/pay #http://localhost:7000/payment/pay --> http://localhost:7001/pay filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - id: order # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://OrderApp # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 # 路由的优先级,数字越小级别越高 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 # localhost:7000/order/orderList --> http://localhost:7001/orderList # localhost:7000/order/* --> http://localhost:8001/* - Path=/order/** # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 #http://localhost:7000/order/orderList --> http://localhost:8001/order/orderList #http://localhost:7000/order/orderList --> http://localhost:8001/orderList filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 ``` ### 添加注解 @EnableDiscoveryClient 在sca-06-order、sca-07-gateway每个项目的启动类上添加 @ EnableDiscoveryClient ![image-20250530085746081](imgs/image-20250530085746081.png) ![image-20250530085857293](imgs/image-20250530085857293.png) ``` ``` 修改网关中路由的 URI的写法 - 原来没有使用注册中心 硬编码: uri: http://localhost:8001 # 请求要转发到的地址 ,也就是payment的微服务地址 - 从注册中心中获取 uri: lb://微服务的名字 ```yaml server: port: 7000 spring: application: name: neusoft-gateway # 微服务相关配置 cloud: # 服务注册 和发现 nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} gateway: # 配置网关的路由规则 routes: - id: payment # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentApp # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 # 路由的优先级,数字越小级别越高 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 # localhost:7000/payment/pay --> http://localhost:7001/pay # localhost:7000/payment/* --> http://localhost:7001/* - Path=/payment/** # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 #http://localhost:7000/payment/pay --> http://localhost:7001/payment/pay #http://localhost:7000/payment/pay --> http://localhost:7001/pay filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - id: order # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://OrderApp # 请求要转发到的地址 ,也就是Order的微服务地址 order: 1 # 路由的优先级,数字越小级别越高 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 # localhost:7000/order/orderList --> http://localhost:7001/orderList # localhost:7000/order/* --> http://localhost:8001/* - Path=/order/** # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改 #http://localhost:7000/order/orderList --> http://localhost:8001/order/orderList #http://localhost:7000/order/orderList --> http://localhost:8001/orderList filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 ``` ![image-20250530091517089](imgs/image-20250530091517089.png) ### 简化的配置文件 ```yaml server: port:2 7000 spring: application: name: neusoft-gateway # 微服务相关配置 cloud: # 服务注册 和发现 nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} gateway: # routes: # 使用discovery动态发现应用 生成路由规则 discovery: locator: enabled: true # 让gateway可以发现nacos中的微服务 ``` ![image-20250530093928543](imgs/image-20250530093928543.png) - 通过网关访问 Payment 的地址 http://localhost:7000/PayMentApp/pay - 通过网关访问 Payment 的地址 http://localhost:7000/OrderApp/orderList # SpringCloudGateWay断言 https://docs.spring.io/spring-cloud-gateway/docs/4.0.9/reference/html/#gateway-request-predicates-factories Predicate来自于java8的接口。Predicate接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。add--与、or--或、negate--非。 Spring Cloud Gateway 将路由作为 Spring WebFlux HandlerMapping 基础结构的一部分进行匹配。Spring Cloud Gateway 包含许多内置的路由谓词工厂,所有这些谓词都与 HTTP 请求的不同属性匹配。可以将多个路由谓词工厂与逻辑 and 语句组合在一起以达到更加细化管理的目的。SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。具体如下: ![image.png](imgs/1708698112962-6243ea19-5d58-4c14-b1e4-4fac62c5652f.webp) ## Path Path Route Predicate Factory 采用两个参数:Spring PathMatcher patterns 列表和名为 matchTrailingSlash (默认为 true ) 的可选标志。以下示例配置路径路由谓词: -Path=/foo/{segment} ```yaml spring: cloud: gateway: routes: - id: path_route uri: https://example.org predicates: - Path=/red/{segment},/blue/{segment} ``` 如果请求路径为,则此路由匹配,例如: /red/1 或 /red/1/ 或 /red/blue 和 /blue/green 。但是如果 matchTrailingSlash 设置为 false ,则请求路径 /red/1/ 将不匹配。此谓词将 URI 模板变量(如 segment 前面示例中定义的 )提取为名称和值的映射,并将其放在 ServerWebExchange.getAttributes() 中,并在 中 ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE 定义了键。然后,这些值可供 GatewayFilter 工厂使用。可以使用实用程序方法(称为 get )来更轻松地访问这些变量。下面的示例演示如何使用该 get 方法: ## Query QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。 -Query=baz, ba. ```yaml spring: cloud: gateway: routes: - id: query_route uri: https://example.org predicates: - Query=green ``` 如果请求包含 green 查询参数,则上述路由匹配。 ```yaml spring: cloud: gateway: routes: - id: query_route uri: https://example.org predicates: - Query=red, gree. ``` 如果请求包含值与 gree. 正则表达式匹配的 red 查询参数,则上述路由匹配,因此 green 将 greet 匹配。 - http://localhost:7000/PayMentApp/pay?gbt=true ![image-20250530105300244](imgs/image-20250530105300244.png) - http://localhost:7000/PayMentApp/pay ![image-20250530105308271](imgs/image-20250530105308271.png) ## Mehtod ```yaml server: port: 7000 spring: application: name: neusoft-gateway # 微服务相关配置 cloud: # 服务注册 和发现 nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} gateway: # 配置网关的路由规则 routes: - id: PayMentAppNew # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentAppNew # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 - Path=/PayMentApp/** # 使用参数进行断言 - Method=GET filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - id: PayMentApp # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentApp # 请求要转发到的地址 ,也就是payment的微服务地址 order: 10 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 - Path=/PayMentApp/** - Method=POST filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 ``` ### Post请求 ![image-20250530110613707](imgs/image-20250530110613707.png) ### Get请求 ![image-20250530110649677](imgs/image-20250530110649677.png) ## Weight ```yaml server: port: 7000 spring: application: name: neusoft-gateway # 微服务相关配置 cloud: # 服务注册 和发现 nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} gateway: # 配置网关的路由规则 routes: - id: PayMentAppNew # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentAppNew # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 - Path=/PayMentApp/** # 使用参数进行断言 - Weight=group1, 8 filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - id: PayMentApp # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentApp # 请求要转发到的地址 ,也就是payment的微服务地址 order: 10 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 - Path=/PayMentApp/** - Weight=group1, 1 filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 ``` ![image-20250530112922067](imgs/image-20250530112922067.png) ![image-20250530112931063](imgs/image-20250530112931063.png) ![image-20250530113000891](imgs/image-20250530113000891.png) ## Host ```yaml server: port: 7000 spring: application: name: neusoft-gateway # 微服务相关配置 cloud: # 服务注册 和发现 nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} gateway: # 配置网关的路由规则 routes: - id: PayMentAppNew # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentAppNew # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 # - Path=/PayMentApp/** # 使用参数进行断言 - Host=localhost filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - id: PayMentApp # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentApp # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 # - Path=/PayMentApp/** - Host=127.0.0.1 # - Host=www.baidu.com # - Host=*.baidu.com filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 ``` ![image-20250530115530450](imgs/image-20250530115530450.png) ![image-20250530115547173](imgs/image-20250530115547173.png) ## Cookie ```yaml server: port: 7000 spring: application: name: neusoft-gateway # 微服务相关配置 cloud: # 服务注册 和发现 nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} gateway: # 配置网关的路由规则 routes: - id: PayMentAppNew # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentAppNew # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 - Cookie=chocolate1, ch.p - Cookie=token, tokenValue filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - id: PayMentApp # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentApp # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 - Cookie=chocolate2, abcde filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 ``` ### Cookie=chocolate1=ch.p;token=tokenValue ![image-20250530141539146](imgs/image-20250530141539146.png) ### Cookie=chocolate2=abcde ![image-20250530141556051](imgs/image-20250530141556051.png) ## Header ```yaml server: port: 7000 spring: application: name: neusoft-gateway # 微服务相关配置 cloud: # 服务注册 和发现 nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} gateway: # 配置网关的路由规则 routes: - id: PayMentAppNew # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentAppNew # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 predicates: - Header=X-Request-Id, \d+ filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - id: PayMentApp # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentApp # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 - Header=token, \d+ filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 ``` ### 测试 Header=X-Request-Id, \d+ ![image-20250530142840233](imgs/image-20250530142840233.png) ### 测试Header=token, \d+ ![image-20250530142916011](imgs/image-20250530142916011.png) # SpringCloudGateWayFilter pring Cloud Gateway除了具备请求路由功能之外,也支持对请求的过滤。 通俗的讲:过滤器就是对请求或响应做一些手脚。 ### 2.4.1过滤器分类 #### 2.4.1.1按生命周期分类 Gateway过滤器按生命周期分有“pre”和“post”两种方式的filter。 pre:这种过滤器在请求被路由之前调用。我们可以利用这类过滤器做参数校验、权限校验、流量监控、日志输出、协议转换等 post:这种过滤器在路由到微服务以后执行。这类过滤器可以做响应内容、响应头的修改、收集统计信息和指标、日志的输出等 ![img](imgs/1708732011419-e5964b90-dba5-4893-8d54-affafa1f8a9d.png) #### 2.4.1.2 按过滤器作用范围 从过滤器的作用范围,可分为局部过滤器和全局过滤器 - 局部过滤器(GatewayFilter接口),是针对单个路由的过滤器。 - 全局过滤器(GlobalFilter接口),作用于所有路由,不需要单独配置。开发者可以通过全局过滤器实现一些共通功能,并且全局过滤器也是开发者使用比较多的过滤器。 Spring Cloud Gateway 内置的过滤器工厂一览表如下: | **过滤器工厂** | **作用** | ** 参数** | | --------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | AddRequestHeader | 为原始请求添加Header | Header的名称及值 | | AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 | | AddResponseHeader | 为原始响应添加Header | Header的名称及值 | | DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 | | Hystrix | 为路由引入Hystrix的断路器保护 | Hystrixcommand的名称 | | FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 | | PrefixPath | 为原始请求路径添加前缀 | 前缀路径 | | PreserveHostHeader | 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 | | RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus | | RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url | | RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的一系列Header | Header名称 | | RemoveResponseHeader | 为原始请求删除某个Header | Header的名称 | | RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则表达式 | | RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正则表达式,重写后的值 | | SaveSession | 在转发请求之前,强制执行websession::save操作 | 无 | | secureHeaders | 为原始响应添加一系列起安全作用的响应头 | 无,支持修改这些安全响应头的值 | | SetPath | 修改原始的请求路径 | 修改后的路径 | | SetResponseHeader | 修改原始响应中某个Header的值 Header名称,修改后的值 | | | SetStatus | 修改原始响应的状态码 | HTTP状态码,可以是数字,也可以是字符串 | | StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的路径的数量 | | Retry | 针对不同的响应进行重试 | retries、statuses、methods、 series | | RequestSize | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返413Payload Too Large | 请求包大小,单位为字节,默认值为5M | | ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 | | ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 | Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理。 ![img](imgs/1714353165972-c651281a-0411-4a94-a77f-59c5cc1430ec.png) ### ```yaml server: port: 7000 spring: application: name: neusoft-gateway # 微服务相关配置 cloud: # 服务注册 和发现 nacos: # nacos 地址,用于 服务注册 和 服务配置 所以此处相当于定义变量 server-addr: 127.0.0.1:8848 # 服务注册 discovery: server-addr: ${spring.cloud.nacos.server-addr} # 服务的名称(在Nacos注册中心 和 服务调用的时候 使用) service: ${spring.application.name} gateway: # 配置网关的路由规则 routes: - id: PayMentAppNew # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentAppNew # 请求要转发到的地址 ,也就是payment的微服务地址 order: 1 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 - Path=/mynew/** filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - AddRequestHeader=X-Request-red, blue - AddRequestParameter=name, lisi - AddResponseHeader=Allow-CROS-Method, GET - AddResponseHeader=Allow-CROS-origin, localhost - id: PayMentApp # 路由id必须是全局唯一的 , 一般可以采用微服务的名字 uri: lb://PayMentApp # 请求要转发到的地址 ,也就是payment的微服务地址 order: 10 predicates: # 根据具体的访问路径决定请求是否要转发,及转发到具体的那个微服务 - Path=/old/** filters: # 路由之后执行的过滤器 - StripPrefix=1 # 内置的去掉前缀的过滤器 转发之前去掉1层路径 - AddRequestHeader=X-Request-red, orange - AddRequestParameter=name, wangwu - AddResponseHeader=Allow-CROS-Method, GET - AddResponseHeader=Allow-CROS-origin, localhost ``` ## 自定义过滤器 ```java package com.neuedu.gateway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 webflux webmvc * @data 2025/6/39:40 */ @Component public class TokenFileter implements GlobalFilter { Logger log = LoggerFactory.getLogger(TokenFileter.class); @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // http://localhost:7000/mynew/pay?token=admin // http://localhost:7000/mynew/pay?token=abc // http://localhost:7000/old/pay log.info("校验 权限"); //判断是否存在token String token = exchange.getRequest().getQueryParams().getFirst("token"); log.info("token = " + token); if(!"admin".equals(token)){ log.info("验证token未通过"); //返回401 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } log.info("验证token通过"); //验证token 通过了 return chain.filter(exchange); } } ``` ![image-20250603095512490](imgs/image-20250603095512490.png) ![image-20250603095531519](imgs/image-20250603095531519.png) # 配置管理 [官网](https://nacos.io/docs/latest/ecology/use-nacos-with-spring-cloud/?spm=5238cd80.2ef5001f.0.0.3f613b7cQ0r9Uy) 使用Naocs 启动naocs http://localhost:8848/nacos/index.html#/newconfig?serverId=center&namespace=&edasAppName=&edasAppId=&searchDataId=&searchGroup= ## 新建配置 ![image-20250603105544782](imgs/image-20250603105544782.png) ![image-20250603111808176](imgs/image-20250603111808176.png) ## 在项目中使用 1. 添加依赖。 ```xml sca-all-in-one com.neuedu.sca 1.0-SNAPSHOT 4.0.0 sca-p08-nacos-config 17 17 org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` - 编写配置文件 application.properties 可以使用application.yaml ```properties spring.application.name=springclouddemo2023x spring.config.import[0]=nacos:springclouddemo2023x.properties?group=DEFAULT_GROUP spring.cloud.nacos.config.server-addr=127.0.0.1:8848 ``` - 编写启动类和Controller ```java package com.neuedu.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/311:01 */ @SpringBootApplication @RestController @RefreshScope public class ConfigApp { //获取 application.properties 或者是 application.yaml中的属性 @Value("${myname}") private String myname; @Value("${name}") private String name; @Value("${age}") private Integer age; public static void main(String[] args) { SpringApplication.run(ConfigApp.class , args ); } /** * http://127.0.0.1:8080/ * @return */ @RequestMapping("/") public String hello(){ return "helloworld: myname="+myname; } @RequestMapping("/getNacosConfig") public String getNacosConfig(){ return String.format("name:%s , age: %d",name,age); } } ``` - 测试 http://127.0.0.1:8080/getNacosConfig ![image-20250603111207848](imgs/image-20250603111207848.png) # RabbitMQ ## 安装 使用自定义的用户名、密码 neuedu 123456 ```shell docker run \ -e RABBITMQ_DEFAULT_USER=neuedu \ -e RABBITMQ_DEFAULT_PASS=123456 \ -v mq-plugins:/plugins \ --name rabbitmq \ --hostname mq \ -p 15672:15672 \ -p 5672:5672 \ -d rabbitmq ``` 也可以使用默认的用户名 ,默认为 guest ```shell docker run -d \ --name rabbitmq \ --hostname mq \ -p 5672:5672 \ -p 15672:15672 \ rabbitmq ``` 启动webUI客户端 ``` docker exec -it rabbitmq rabbitmq-plugins enable rabbitmq_management ``` 登录 ![image-20250605092607930](imgs/image-20250605092607930.png) ![image-20250605092432516](imgs/image-20250605092432516.png) ## 使用UI界面测试发送消息和接受消息 ### 创建两个队列 ![image.png](imgs/1747467083740-0e370d52-3b72-438f-8dc9-755fe1c05afb.webp) ![image.png](imgs/1747467115111-b89811c9-77d8-45ea-ad36-51cc6663db8a.webp) ### **绑定关系** 点击 amp.fanout 交换机,即可进入交换机详情页面。点击菜单 Bindings 菜单,绑定两个队列。 ![img](imgs/1747467211986-48fc1864-31d7-441f-a37b-5d91c002efc5.png) ![img](imgs/1747467278006-30245e79-7a50-4153-a8c9-a1d3f48433cc.png) 设置给两个队列发消息2个队列与交换机绑定关系设置完成: ![img](imgs/1747467300658-554a5329-192a-4829-8b0b-9b8fa35b55b8.png) 在队列中可以看到对应的 交换机 : ![img](imgs/1747467373273-4b7de014-2970-4ce5-bc91-9f8d0e58cdab.png) ### **发送消息** 控制台中的 publish message 发送一条消息: ![img](imgs/1747467413428-6f710e5a-da23-410f-9ebc-4374e7cb2489.png) 2个队列接收到消息: ![img](imgs/1747467462618-09fd3931-5746-4338-a0e5-24bdfc1f8a78.png) 队列 hello.queue2 同样可以收到消息 ![img](imgs/1747467443265-d0acfaa8-a97c-46e0-ae97-f61238dfd62d.png) 这里是由控制台模拟了生产者发送的消息。由于没有消费者存在, 最终消息丢失了,这样说明交换机没有存储消息的能力。 ## **管理控制台页面操作 - 数据隔离** 用户管理 点击 Admin 选项卡,首先会看到 RabbitMQ 控制台的用户管理界面 ![image.png](imgs/1747469620473-681e555c-2ecd-4c05-893e-3c53741b941c.webp) 这里的用户都是RabbitMQ的管理或运维人员。表格中字段: 1 Name: mqadmin,用户名 2 Tags: administrator,说明 mqadmin 用户是超级管理员, 拥有所有权限 3Can access virtual host: /,可以访问的virtual host,这里 的/是默认的virtual host 于小型企业而言,出于成本考虑,我们通常只会搭建一套MQ集群, 公司内的多个不同项目同时使用。这个时候为了避免互相干扰, 我 们会利用 virtual host 的隔离特性,将不同项目隔离。一般会做两 件事情: ●给每个项目创建独立的运维账号,将管理权限分离。 ●给每个项目创建不同的virtual host,将每个项目的数据隔 离。 创建用户 yyzx ![image.png](imgs/1747469686497-82c2a8fa-8fd0-4fd7-bdd0-4d1ff75619c5.webp) 此时创建的用户没有任何 virtual host 的访问权限: ![image.png](imgs/1747469702240-351d7f10-ce08-42ce-bd38-4a2ac9d5ed37.webp) 创建 virtual host 退出登录,切换到 刚创建的 yyzx 用户登录,然后点击 Virtual Hosts 菜单 ,进入 Virtual Hosts 管理页面 ![image.png](imgs/1747469724983-de2b25f0-968f-42d5-bb6a-442a88461447.webp) 目前只有一个默认的virtual host,名字为 /。 ![image.png](imgs/1747469766545-433693eb-d990-4557-84ee-16dda1e659a7.webp) 给“颐养中心”项目 创建一个单独的 virtual host,而不是使用默认的/ ![image.png](imgs/1747469796302-570527c8-07dc-4907-9d22-a99e99009d7b.webp) 创建完成后: ![image.png](imgs/1747469837801-303c5cb8-cd3a-4a72-a74a-6829aba988ba.webp) 由于是登录 yyzx 账户后创建的 virtual host ,因此回到 users 菜 单,当前用户已经具备了对 /yyzx 这个virtual host的访问权限: ![image.png](imgs/1747469907276-e4d7f5b9-4665-4899-9d88-1984bdf4a9f6.webp) 右上角切换 virtual host :/yyzx 再次查看 queues 选项卡,这时已经看不到 之前创建的队列了,这就是基于virtual host 的隔离效果 ![image.png](imgs/1747469966335-d9a3de8d-ee80-42f6-a704-5f40a727757f.webp) ## 使用Spring-AMQP ### 创建虚拟主机 ![image-20250605104248606](imgs/image-20250605104248606.png) ### 创建一个项目(发送) - Boot项目 - spring-boot-starter-amqp - spring-boot-starter-web - spring-boot-starter-test ![image-20250605104527837](imgs/image-20250605104527837.png) ```xml sca-all-in-one com.neuedu.sca 1.0-SNAPSHOT 4.0.0 sca-p09-rabbitmq 17 17 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-amqp org.springframework.boot spring-boot-starter-test org.projectlombok lombok ``` ### 配置MQ信息 在application.yaml中配置 , **host** 需要根据自己的地址修改 ```yaml spring: application: name: publisher # 配置 rabbitmq rabbitmq: # 你的虚拟机IP 每个人需要按照自己 虚拟机 的 ip 地址修改 ******** host: 192.168.49.100 # 端口 port: 5672 # 虚拟主机 virtual-host: /yyzx # 用户名 username: yyzx # 密码 password: yyzx ``` ### 编写启动类 @SpringBootApplication 能自动链接到 MQ服务器 ```java package com.neuedu.amqp.publisher; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 作者: 张金山 * 创建时间:2025/5/17 19:29 星期六 * 描述: * 项目: springboot-amqp-demo - PACKAGE_NAME * 作者的博客: https://blog.fulfill.com.cn */ @SpringBootApplication public class PublisherBusinessApp { public static void main(String[] args) { SpringApplication.run(PublisherBusinessApp.class, args); } } ``` ### 使用 RabbitTemplate 发送消息 - 单元测试 ```java package com.neuedu.amqp.publisher; import io.micrometer.common.util.StringUtils; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/510:52 */ // @SpringBootTest(classes = PublisherBusinessApp.class) @SpringBootTest public class PublisherTest { @Autowired public RabbitTemplate rabbitTemplate; @Test public void testSend() { // 通过MQTT 委托 另外的服务 发送 手机短信、微信消息等。 // 队列名称 String queueName = "simple.queue"; // 消息 String message = "send phone msg ,phonenum:13888888888"; //发送消息 rabbitTemplate.convertAndSend(queueName, message); } } ``` - Controller ```java package com.neuedu.amqp.publisher; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/510:57 */ @RestController public class PublisherController { @Autowired public RabbitTemplate rabbitTemplate ; @RequestMapping("/send") public String send(String msg ){ // 队列名称 String queueName = "simple.queue" ; // 消息 String message = !StringUtils.isEmpty(msg)? msg:"send phone msg ,phonenum:13888888888"; //发送消息 rabbitTemplate.convertAndSend(queueName , message); return "success"; } } ``` ### 在浏览器中发送 http://127.0.0.1:8080/send?msg=66666666666 ![image-20250605110929528](imgs/image-20250605110929528.png) ### 接收(使用WebUI) ![image-20250605111006537](imgs/image-20250605111006537.png) ## 使用程序接受 消息 - 创建项目 - 添加依赖 - 配置连接信息(application.yaml) - 编写主类 - 自定义一个Component - @RabbitMQListener注解放到一个方法上用于接受消息处理 ### 创建项目 ### 添加依赖 ```xml sca-all-in-one com.neuedu.sca 1.0-SNAPSHOT 4.0.0 sca-p10-rabbitmq-consumer 17 17 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-amqp org.springframework.boot spring-boot-starter-test org.projectlombok lombok ``` ### 配置连接信息 ```yaml spring: application: name: consumer # 配置 rabbitmq rabbitmq: # 你的虚拟机IP host: 192.168.49.100 # 端口 port: 5672 # 虚拟主机 virtual-host: /yyzx # 用户名 username: yyzx # 密码 password: yyzx # 设置端口号 避免冲突 server: port: 9090 ``` ### 编写主类 ```java package com.neuedu.amqp.consumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/511:36 */ @SpringBootApplication public class ConsumerApp { public static void main(String[] args) { SpringApplication.run(ConsumerApp.class , args); } } ``` ### 自定义一个Component - @RabbitMQListener注解放到一个方法上用于接受消息处理 ```java package com.neuedu.amqp.consumer.listener; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/511:37 */ @Component public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") public void listenSimpleQueueMessage(String msg){ //定义处理消息的逻辑 System.out.println("Spring 消费者接收到的 消息:【" + msg+"】"); } } ``` ### 测试 - 启动 publisher的主类 - 启动Consumer的主类 - 调用浏览器发送MQ消息 - 观察 Consumer控制条 ![image-20250605114424455](imgs/image-20250605114424455.png) # WorkQueues 模型 ## 介绍 Work queues,任务模型。简单来说就是**让多个消费者绑定到一个队列,共同消费队列中的消息** ![img](imgs/1747465309979-732357fc-08de-4fdd-86d0-8e93f3fe0dbe.jpeg) 当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。 此时就可以使用work 模型,多个消费者共同处理消息处理,消息处理的速度就能大大提高了。 ![image-20250605134215566](imgs/image-20250605134215566.png) ## 使用单元测试在Publisher工程中发送多个消息 ```java package com.neuedu.amqp.publisher; import com.neuedu.amqp.publisher.PublisherBusinessApp; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; /** * 作者: 张金山 * 创建时间:2025/5/17 19:59 星期六 * 描述: 测试高并发 下的发送消息 * 项目: springboot-amqp-demo - com.neuedu.hc * 作者的博客: https://blog.fulfill.com.cn */ @SpringBootTest(classes = PublisherBusinessApp.class) public class HighConcurrencySendTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testHighConcurrencySend() throws InterruptedException { // 队列名称 String queueName = "work.queue"; // 消息 String message = "hello, message_"; for (int i = 0; i < 50; i++) { // 发送消息,每20毫秒发送一次,相当于每秒发送50条消息 rabbitTemplate.convertAndSend(queueName, message + i); Thread.sleep(20); } } } ``` ## 使用多个接收者接受数据 ![image-20250605135554997](imgs/image-20250605135554997.png) ![image-20250605135639236](imgs/image-20250605135639236.png) 也就是说消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。导致1个消费者空闲,另一个消费者忙的不可开交。没有充分利用每一个消费者的能力,最终消息处理的耗时远远超过了1秒。这样显然是有问题的。 在spring中有一个简单的配置,可以解决这个问题。修改consumer服务的application.yml文件,添加配置: prefetch: 每次处理只允许拉取一个消息. ```yaml spring: application: name: consumer # 配置 rabbitmq rabbitmq: # 你的虚拟机IP host: 192.168.49.100 # 端口 port: 5672 # 虚拟主机 virtual-host: /yyzx # 用户名 username: yyzx # 密码 password: yyzx ## 设置监听 listener: simple: # 每次只能获取一条消息,处理完成才能获取下一 个消息 prefetch: 1 # 设置端口号 避免冲突 server: port: 9090 ``` # 交换机(Exchange) 真正生产环境都会经过Exchange来发送消息,而不是直接发送到队列模型: ![img](imgs/1747465321945-859c1693-58af-44b2-af81-5f1b701127db.jpeg) ![img](imgs/1747465322131-787d80d1-c880-475e-98f6-0cfa2464f424.png)在订阅模型中,多了一个exchange 角色,而且过程略有变化: Publisher:生产者,不再发送消息到队列中,而是发给交换机 Exchange: 交换机,一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。 Queue:消息队列也与以前一样,接收消息、缓存消息。不过队列一定要与交换机绑定。 Consumer:消费者,与以前一样,订阅队列,没有变化 **Exchange(交换机)只负责转发消息,不具备存储消息的能力**,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失! 交换机的类型有四种: Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机 Direct:订阅(定向),基于RoutingKey(路由key)发送给订阅了消息的队列 Topic:通配符订阅(话题),与Direct类似,只不过RoutingKey可以使用通配符 Headers:头匹配,基于MQ的消息头匹配,用的较少。 # Fanout Fanout,英文翻译是扇出,在MQ中叫广播也可以。在广播模式下,消息发送流程: ![img](imgs/1747465323720-935f010e-6d2b-4a3d-890e-e21672b4bf38.jpeg) 1. 可以有多个队列 2. 每个队列都要绑定到Exchange(交换机) 3. 生产者发送的消息,只能发送到交换机 4. 交换机把消息发送给绑定过的所有队列 5. 订阅队列的消费者都能拿到消息 案例: ![img](imgs/1747465323897-fd7a85ea-c292-4e8c-8ba0-6cce49dfadba.jpeg)![img](imgs/1747465324111-3f5bc724-7964-40e7-ad5c-f4226d11fbc1.png) 流程: 创建一个名为 yyzx.fanout的交换机,类型是 Fanout 创建两个队列 fanout.queue1 和 fanout.queue2 ,绑定到交换机 yyzx.fanout ## 使用Fanout - 创建交换机 ![image-20250605142617557](imgs/image-20250605142617557.png) - 创建队列 ![image-20250605142820513](imgs/image-20250605142820513.png) - 绑定队列和交换机 ![image-20250605142748624](imgs/image-20250605142748624.png) ## 实现通过fanout交换机发送消息 发送方 ```java package com.neuedu.amqp.publisher; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest(classes = PublisherBusinessApp.class) public class FanoutExchangeTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void fanoutsend() throws InterruptedException { // 队列名称 String exchangeName = "yyzx.fanout"; // 消息 String message = "hello, everyone"; rabbitTemplate.convertAndSend(exchangeName, null ,message); } } ``` 接收方 ```java @RabbitListener(queues = "fanout.queue1") public void listenFanoutQueueMessage1(String msg) throws InterruptedException { // 定义处理消息的逻辑 System.out.println("消费者1接收到fanout消息:【" + msg + "】" + LocalTime.now()); } @RabbitListener(queues = "fanout.queue2") public void listenFanoutQueueMessage2(String msg) throws InterruptedException { // 定义处理消息的逻辑 System.err.println("消费者2接收到fanout消息:【" + msg + "】" + LocalTime.now()); } ``` # Direct 交换机 在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此称为定向路由。 模型: ![img](imgs/1747465325950-c6ccb540-3281-45c8-98d3-94789c7d869d.jpeg) 在Direct模型下: ![img](imgs/1747465326119-874a7f21-5d60-49a5-92d8-2f643648b309.png)队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key) ![img](imgs/1747465326300-3c2fe23e-748a-4172-b8c2-1b3a4abefe98.png)![img](imgs/1747465326469-157184c0-c593-4c24-b880-7d56287b2aff.png) 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。 Exchange不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息 案例: ![img](imgs/1747465326624-ae9fed73-9e13-43c1-a61d-9d5e5305edd1.png)流程: ![img](imgs/1747465326768-ac033799-ed6b-4d48-921d-8a05b343944a.jpeg) ## 声明交换机需求 声明一个名为 yyzx.direct 的交换机 声明队列 direct.queue1,绑定 yyzx.direct,bindingKey为 blud和 red 声明队列 direct.queue2,绑定 yyzx.direct,bindingKey为 yellow和 red 在 consumer 服务中,编写两个消费者方法,分别监听 direct.queue1 和 direct.queue2 在 publisher 中编写测试方法,向 yyzx.direct 发送消息 ## 定义交换机 ![image-20250606084444555](imgs/image-20250606084444555.png) ## 新建队列 ![image-20250606084518647](imgs/image-20250606084518647.png) ![image-20250606084538761](imgs/image-20250606084538761.png) ## 绑定交换机和队列 ![image-20250606084759720](imgs/image-20250606084759720.png) ## 发送消息的代码(publisher) ```java package com.neuedu.amqp.publisher; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest(classes = PublisherBusinessApp.class) public class DirectExchangeTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendDirectExchange(){ // 交换机名称 String exchangeName = "yyzx.direct" ; // 消息 // String message = "红色警报!!!" ; // // 发送消息 // rabbitTemplate.convertAndSend(exchangeName,"red" ,message); // String message = "黄色警报!!!" ; // // 发送消息 // rabbitTemplate.convertAndSend(exchangeName,"yello" ,message); String message = "蓝色警报!!!" ; // 发送消息 rabbitTemplate.convertAndSend(exchangeName,"blue" ,message); } } ``` ## 接受消息的代码(consumer) ```java // Direct 交换机模式 接收第一个队列 /** * routingKey ; blue ,red * @param msg */ @RabbitListener(queues = "direct.queue1") public void listenDirectQueue1(String msg) { System.out.println("消费者1接收到direct.queue1消息:【" + msg + "】"); } // Direct 交换机模式 接收第二个队列、 /*** * routingKey ; yellow ,red * @param msg */ @RabbitListener(queues = "direct.queue2") public void listenDirectQueue2(String msg) { System.err.println("消费者2接收到direct.queue2消息:【" + msg + "】"); } ``` ## 测试结果 ![image-20250606085525928](imgs/image-20250606085525928.png) # Topic 交换机 说明: ![img](imgs/1747465328952-a5aabc56-8418-4746-9ee2-b7373dcd8e41.png)Topic类型的 Exchange 与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange可以让队列在绑定BindingKey 的时候使用通配符! BindingKey 一般都是有一个或多个单词组成,多个单词之间以.分割,例如: item.insert ![img](imgs/1747465329321-0b436965-b52a-4b30-94b8-dbca29f0b78d.png)通配符规则: - \#:匹配一个或多个词 - *:匹配不多不少恰好1个词举例: - item.# :能够匹配 item.spu.insert 或者 item.spu item.* :只能匹配 item.spu ![img](imgs/1747465329472-800b92ff-b0c1-4465-bf6e-93f2ae77d263.png)图示: ![img](imgs/1747465329607-6a57555d-a6c4-46f2-a105-b3e7feee5618.jpeg) 假如此时 publisher 发送的消息使用的 RoutingKey 共有四种: shanghai.news 代表有上海的新闻消息; shanghai.weather 代表上海的天气消息; beijing.news 则代表北京新闻 beijing.weather 代表北京的天气消息 ## 案例: ![img](imgs/1747465329819-a1f43256-6a8e-4b39-a2c9-3d8f0b302c51.png)解释: topic.queue1:绑定的是 shanghai.# ,凡是以 shanghai.开头的 routing key 都会被匹配到,包括:shanghai.news shanghai.weather topic.queue2:绑定的是 #.news ,凡是以 .news 结尾的 routing key 都会被匹配。包括:shanghai.news beijing.news ## 声明交换机 ![image-20250606091501890](imgs/image-20250606091501890.png) ## 创建队列 ![image-20250606091439035](imgs/image-20250606091439035.png) ## 绑定队列和交换机的关系 ![image-20250606092517375](imgs/image-20250606092517375.png) ## 发送消息的代码(publisher) ```java package com.neuedu.amqp.publisher; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest(classes = PublisherBusinessApp.class) public class TopicExchangeTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendTopicExchange(){ // 交换机名称 String exchangeName = "yyzx.topic" ; // 消息 String message = "上海新闻 :为上海加快建成科创高地提供人才保障,以人才之“风”鼓科创之“帆”,AI“北斗七星”带动“群星闪耀” " ; // 发送消息 rabbitTemplate.convertAndSend(exchangeName,"shanghai.news" ,message); // 消息 message = "北京新闻 :北京面向无车家庭再次增发2万个新能源小客车指标" ; // 发送消息 rabbitTemplate.convertAndSend(exchangeName,"beijing.news" ,message); } } ``` ## 接受消息的代码(consumer) ```java @RabbitListener(queues = "topic.queue1") public void listenTopicQueue1(String msg) { System.out.println("消费者1接收到Topic跟shanghai 有关的消息 消息:【" + msg + "】"); } @RabbitListener(queues = "topic.queue2") public void listenTopicQueue2(String msg) { System.err.println("消费者2接收到Topic跟 news 有关的消息 消息:【" + msg + "】"); } ``` ## 测试结果 ![image-20250606092704112](imgs/image-20250606092704112.png) # 声明队列和交换机 之前都是基于RabbitMQ控制台来创建队列、交换机。但是在实际开发时,队列和交换机是程序员定义的,将来项目上线,又要交给运维去创 建。那么程序员就需要把程序中运行的所有队列和交换机都写下来,交给运维。在这个过程中是很容易出现错误的。 因此推荐的做法是由程序启动时检查队列和交换机是否存在,如果不存在自动创建。 基于API SpringAMQP提供了一个Queue类,用来创建队列: ![img](imgs/1747465332190-cb4d10b8-0d48-4493-af70-31f86f439e1c.jpeg) SpringAMQP还提供了一个Exchange接口,来表示所有不同类型的交换机: ![img](imgs/1747465332377-3b5c173e-72fc-410d-a24d-e3c09d4a7cb0.jpeg) ![img](imgs/1747465332592-201932ec-311c-4c44-a18e-5d784eac2748.png)我们可以自己创建队列和交换机,不过 SpringAMQP 还提供了 ExchangeBuilder 来简化这个过程: ![img](imgs/1747465332712-4b7afb52-8432-475d-aec8-1d02bebd616d.png) ![img](imgs/1747465332925-bb5120a4-9084-44b7-80cb-5aa45b508db2.png)而在绑定队列和交换机时,则需要使用 BindingBuilder 来创建 Binding对象: ![img](imgs/1747465333076-ece2ffa4-3210-424f-8d7a-c28ef47445de.png) ## ## Fanout ### 声明 - 声明交换机 - 声明队列 - 绑定交换机和队列 在**Consumer项目**项目中添加声明 ```java package com.neuedu.amqp.consumer.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.FanoutExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/610:30 */ @Configuration public class FanoutExchangeConfig { // 声明对交换机 @Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange("yyzx.fanout1"); } // 声明对队列 1 // @Bean("fanoutQueue1") @Bean public Queue fanoutQueue1(){ return new Queue("fanout1.queue1") ; } // 绑定关系 交换机 和队列1 @Bean public Binding bindingQueue1(Queue fanoutQueue1 , FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange); } // 声明对队列 2 // @Bean("fanoutQueue2") @Bean public Queue fanoutQueue2(){ return new Queue("fanout1.queue2") ; } // 绑定关系 交换机 和队列2 @Bean public Binding bindingQueue2(Queue fanoutQueue2 , FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange); } } ``` ### 发送消息(Publisher项目) ```java /** * 通过代码声明的 交换机 * @throws InterruptedException */ @Test public void fanout1send() throws InterruptedException { // 队列名称 String exchangeName = "yyzx.fanout1"; // 消息 String message = "hello, everyone"; rabbitTemplate.convertAndSend(exchangeName, null, message); } ``` ### 接收消息(Consumer项目) ```java package com.neuedu.amqp.consumer.listener; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import java.time.LocalTime; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/511:37 */ @Component public class SpringRabbitListenerCode { @RabbitListener(queues = "fanout1.queue1") public void listenFanoutQueueMessage1(String msg) throws InterruptedException { // 定义处理消息的逻辑 System.out.println("通过代码声明的队列和交换机 消费者1接收到fanout消息:【" + msg + "】" + LocalTime.now()); } @RabbitListener(queues = "fanout1.queue2") public void listenFanoutQueueMessage2(String msg) throws InterruptedException { // 定义处理消息的逻辑 System.err.println("通过代码声明的队列和交换机 消费者2接收到fanout消息:【" + msg + "】" + LocalTime.now()); } // // Direct 交换机模式 接收第一个队列 // // /** // * routingKey ; blue ,red // * // * @param msg // */ // @RabbitListener(queues = "direct.queue1") // public void listenDirectQueue1(String msg) { // System.out.println("消费者1接收到direct.queue1消息:【" + msg + "】"); // } // // // Direct 交换机模式 接收第二个队列、 // // /*** // * routingKey ; yellow ,red // * @param msg // */ // @RabbitListener(queues = "direct.queue2") // public void listenDirectQueue2(String msg) { // System.err.println("消费者2接收到direct.queue2消息:【" + msg + "】"); // } // // // @RabbitListener(queues = "topic.queue1") // public void listenTopicQueue1(String msg) { // System.out.println("消费者1接收到Topic跟shanghai 有关的消息 消息:【" + msg + "】"); // } // // @RabbitListener(queues = "topic.queue2") // public void listenTopicQueue2(String msg) { // System.err.println("消费者2接收到Topic跟 news 有关的消息 消息:【" + msg + "】"); // } } ``` ### 测试 ![image-20250606105032538](imgs/image-20250606105032538.png) ## Direct ### 声明 ```java package com.neuedu.amqp.consumer.config; import org.springframework.amqp.core.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class DirectConfig { /** * ● 声明交换机 * ● @return Direct 类型交换机 */ @Bean public DirectExchange directExchange() { return ExchangeBuilder.directExchange("yyzx.direct1").build(); } /** * ● 第一个队列 */ @Bean public Queue directQueue1() { return new Queue("direct1.queue1"); } /** * ● 绑定队列和交换机 * ● @param directQueue1 : 队列 * ● @param directExchange :交换机 * ● 需要设置 routingKey :red */ @Bean public Binding bindingQueue1WithRed(Queue directQueue1, DirectExchange directExchange) { return BindingBuilder.bind(directQueue1).to(directExchange).with("red"); } /** * ● 绑定队列和交换机 * ● @param directQueue1 : 队列 * ● @param directExchange :交换机 * ● 需要设置 routingKey :blue */ @Bean public Binding bindingQueue1WithBlue(Queue directQueue1, DirectExchange directExchange) { return BindingBuilder.bind(directQueue1).to(directExchange).with("blue"); } /** * ● 第二个队列 */ @Bean public Queue directQueue2() { return new Queue("direct1.queue2"); } /** * ● 绑定队列和交换机 * ● @param directQueue2 : 队列 * ● @param directExchange :交换机 * ● 需要设置 routingKey :red */ @Bean public Binding bindingQueue2WithRed(Queue directQueue2, DirectExchange directExchange) { return BindingBuilder.bind(directQueue2).to(directExchange).with("red"); } /** * ● 绑定队列和交换机 * ● @param directQueue2 : 队列 * ● @param directExchange :交换机 * ● 需要设置 routingKey :yellow */ @Bean public Binding bindingQueue1WithYellow(Queue directQueue2, DirectExchange directExchange) { return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow"); } } ``` ### 接收消息 ```java package com.neuedu.amqp.consumer.listener; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import java.time.LocalTime; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/511:37 */ @Component public class SpringRabbitListenerCode { //。。。。。。。 // Direct 交换机模式 接收第一个队列 /** * routingKey ; blue ,red * * @param msg */ @RabbitListener(queues = "direct1.queue1") public void listenDirectQueue1(String msg) { System.out.println("通过代码声明的队列和交换机 消费者1接收到direct.queue1消息:【" + msg + "】"); } // Direct 交换机模式 接收第二个队列、 /*** * routingKey ; yellow ,red * @param msg */ @RabbitListener(queues = "direct1.queue2") public void listenDirectQueue2(String msg) { System.err.println("通过代码声明的队列和交换机 消费者2接收到direct.queue2消息:【" + msg + "】"); } } ``` ### 测试 ![image-20250606113120582](imgs/image-20250606113120582.png) ## Topic 参考Direct # 使用注解的方式在监听的时候声明交换机和队列 ```java package com.neuedu.amqp.consumer.listener; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/613:41 */ @Component public class SpringRabbitListenerBinding { // 消费者1 @RabbitListener(bindings = @QueueBinding( value = @Queue(name="direct2.queue1"), exchange = @Exchange(name = "yyzx.direct2" , type = ExchangeTypes.DIRECT) , //type = ExchangeTypes.DIRECT 交换机类型 key = {"red","blue"} )) public void listenDirect2Queue1(String msg){ System.out.println("消费者1接收到direct2.queue1的消息:【"+msg+"】"); } //消费者2 @RabbitListener(bindings = @QueueBinding( value = @Queue(name="direct2.queue2"), exchange = @Exchange(name = "yyzx.direct2" , type = ExchangeTypes.DIRECT) , //type = ExchangeTypes.DIRECT 交换机类型 key = {"red","yellow"} )) public void listenDirect2Queue2(String msg){ System.out.println("消费者2接收到direct2.queue2的消息:【"+msg+"】"); } } ``` ## 观察WebUI界面 ![image-20250606134623843](imgs/image-20250606134623843.png) ![image-20250606134635886](imgs/image-20250606134635886.png) ## 测试 ![image-20250606134730924](imgs/image-20250606134730924.png) ## Topic类型 ```java package com.neuedu.amqp.consumer.listener; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @author 金山 * 项目:sca-all-in-one * site: https://blog.fulfill.com.cn * 描述 * @data 2025/6/613:41 */ @Component public class SpringRabbitListenerBindingTopic { //消费者1 @RabbitListener(bindings = @QueueBinding( value = @Queue(name="topic1.queue1"), exchange = @Exchange(name = "yyzx.topic1" , type = ExchangeTypes.TOPIC) , key = "shanghai.#" )) public void listenTopic1Queue1(String msg){ System.out.println("消费者1接收到topic1.queue1的消息:【"+msg+"】"); } //消费者2 @RabbitListener(bindings = @QueueBinding( value = @Queue(name="topic1.queue2"), exchange = @Exchange(name = "yyzx.topic1" , type = ExchangeTypes.TOPIC) , key = "#.news" )) public void listenTopic1Queue2(String msg){ System.out.println("消费者2接收到topic1.queue2的消息:【"+msg+"】"); } } ``` ![image-20250606140431061](imgs/image-20250606140431061.png)