# cloud2020 **Repository Path**: zhudunfeng/cloud2020 ## Basic Information - **Project Name**: cloud2020 - **Description**: cloud2020是对2020SpringCloud组件大批量进入维护,探索新组件与旧组件的变化过程的的测试集合,里面包含了所有热门组件相关的技术与demo - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2022-03-08 - **Last Updated**: 2023-04-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringCloud SpringCloud2020年的技术变更 ## 关于Cloud各种组件的停更/升级/替换 SpringCloudNetflix组件大部分进入维护状态,从而Spring官方推荐进行一些相应组件的升级与替换 维护前的主要cloud组件 image-20220308163814442 推荐替换的相应组件 ![image-20220308163942294](https://cloudimgs-1301504220.cos.ap-nanjing.myqcloud.com/image/202203081639345.png) ## 微服务架构编码构建 ### 未引入服务注册与发现组件实现服务调用(RestTemplate) ![image](https://user-images.githubusercontent.com/48040850/155866502-5e2966be-eae3-40c0-bbf6-b9aa3939d798.png) > 此时,项目未引入SpringCloud的服务注册与发现组件,服务之间的调用使用restTemplate进行相互调用 (1)项目架构 ```txt cloud2020 cloud-api-commons 服务提供与消费共同使用的相关类 cloud-consumer-oreder80 服务消费80 cloud-provider-payment8001 服务提供8001 ``` ![image-20220227111214723](https://cloudimgs-1301504220.cos.ap-nanjing.myqcloud.com/image/202202271112780.png) (2)核心代码 cloud-provider-payment8001 ```java package com.adun.springcloud.service.impl; @Service public class PaymentServiceImpl implements PaymentService { @Resource private PaymentDao paymentDao; @Override public int create(Payment payment) { return paymentDao.create(payment); } @Override public Payment getPaymentById(Long id) { return paymentDao.getPaymentById(id); } } ``` cloud-consumer-oreder80 ```java package com.adun.springcloud.controller; @RestController @Slf4j public class OrderController { public static final String PAYMENT_URL="http://localhost:8001"; //引入服务注册中心后可以使用注释替换上方代码 //public static final String PAYMENT_URL="http://CLOUD-PAYMENT-SERVICE"; @Resource private RestTemplate restTemplate; @Resource private DiscoveryClient discoveryClient; @Resource private LoadBalancer loadBalancer; @GetMapping(value = "/consumer/payment/create") public CommonResult create(Payment payment){ log.info("消费端create:{}",payment); return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class); } @GetMapping(value = "/consumer/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id){ log.info("消费端getPaymentById:{}",id); Map map = new HashMap<>(); map.put("id", id); return restTemplate.getForObject(PAYMENT_URL + "/payment/get/{id}", CommonResult.class,map); } } ``` ## 微服务注册中心 1. 什么是服务治理  ​ Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理 ​ 在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。 2. 什么是服务注册与发现 Eureka采用了CS的设计架构,Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用 Eureka的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。 ​ 在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息 比如 服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者|服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何rpc远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址)) ​ 下左图是Eureka系统架构,右图是Dubbo的架构,请对比 ![1660210993916](README.assets/1660210993916.png) ### Eureka项目相关 ![1660215822047](README.assets/1660215822047.png) Eureka包含两个组件:Eureka ServerEureka Client Eureka Server提供服务注册服务 各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。 EurekaClient通过注册中心进行访问 是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒) #### 单机Eureka构建步骤 > 此时,项目未引入SpringCloud的服务注册与发现组件,服务之间的调用使用restTemplate进行相互调用 ##### 1、项目架构 ``` cloud2020 cloud-api-commons 服务提供与消费共同使用的相关类 cloud-eureka-server7001 服务注册中心7001 cloud-consumer-oreder80 服务消费80 cloud-provider-payment8001 服务提供8001 ``` ##### 2、构建cloud-eureka-server7001 pom引入 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` yml修改 ```yaml server: port: 7001 eureka: instance: hostname: localhost #eureka服务端的实例名称 client: #false表示不向注册中心注册自己。 register-with-eureka: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 fetch-registry: false service-url: #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ ``` 主启动类添加注解`@EnableEurekaServer` ```java @SpringBootApplication @EnableEurekaServer public class EurekaMain7001 { public static void main(String[] args) { SpringApplication.run(EurekaMain7001.class,args); } } ``` 测试 http://localhost:7001/ ![1660211707392](README.assets/1660211707392.png) ##### 3、EurekaClient端cloud-provider-payment8001 将注册进EurekaServer成为服务提供者provider 改POM ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` 修改yml ```yaml server: port: 8001 spring: application: name: cloud-payment-service datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包 url: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 ##eureka-client注册自己到eureka-server eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://localhost:7001/eureka ``` 修改主启动类添加注解`@EnableEurekaClient` ```java @SpringBootApplication @EnableEurekaClient public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class,args); } } ``` ##### 4、EurekaClient端cloud-consumer-order80 将注册进EurekaServer成为服务消费者consumer 改POM ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` 修改yml ```yaml server: port: 80 spring: application: name: cloud-order-service eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://localhost:7001/eurek ``` 修改主启动类添加注解`@EnableEurekaClient` ```java @SpringBootApplication @EnableEurekaClient public class OrderMain80 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class,args); } } ``` ##### 5、测试 ![1660212263255](README.assets/1660212263255.png) #### 集群Eureka构建步骤 ##### 1、基本项目架构 ``` cloud2020 cloud-api-commons 服务提供与消费共同使用的相关类 cloud-eureka-server7001 服务注册中心7001 cloud-eureka-server7002 服务注册中线7002 cloud-consumer-oreder80 服务消费80 cloud-provider-payment8001 服务提供8001 cloud-provider-payment8002 服务提供8002 ``` image-20220227134225873 ##### 2、eureka server集群基本原理与实现 ![1660212472671](README.assets/1660212472671.png) - [ ] 问题:微服务RPC远程服务调用最核心的是什么 ? 高可用,试想你的注册中心只有一个only one, 它出故障了那就呵呵( ̄▽ ̄)"了,会导致整个为服务环境不可用,所以 解决办法:搭建Eureka注册中心集群 ,实现负载均衡+故障容错 image-202202271725855 image-202202271419150 ##### 3、EurekaServer集群环境构建步骤 cloud-eureka-server7001 cloud-eureka-server7002 改POM ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` 写yml【7001与7002相互注册】 7001 ```yaml server: port: 7001 eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: register-with-eureka: false #false表示不向注册中心注册自己。 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: defaultZone: http://eureka7002.com:7002/eureka/ ``` 7002 ```yaml server: port: 7002 eureka: instance: hostname: eureka7002.com #eureka服务端的实例名称 client: register-with-eureka: false #false表示不向注册中心注册自己。 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: defaultZone: http://eureka7001.com:7001/eureka/ ``` 主启动 ```java @SpringBootApplication @EnableEurekaServer public class EurekaMain7002 { public static void main(String[] args) { SpringApplication.run(EurekaMain7002.class,args); } } ``` ##### 4、将支付服务8001微服务发布到上面2台Eureka集群配置中 改yml ```yaml server: port: 8001 spring: application: name: cloud-payment-service datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包 url: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: #defaultZone: http://localhost:7001/eureka defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版 mybatis: mapperLocations: classpath:mapper/*.xml type-aliases-package: com.atguigu.springcloud.entities # 所有Entity别名类所在包 ``` ##### 5、将订单服务80微服务发布到上面2台Eureka集群配置中 改yml ```yaml server: port: 80 spring: application: name: cloud-order-service eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: #defaultZone: http://localhost:7001/eureka defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版 ``` ##### 6、测试01 ![1660213332102](README.assets/1660213332102.png) ##### 7、负载均衡环境搭建测试 ###### (1)支付服务提供者8001/8002集群环境构建 修改yml【注册中心为集群需要特别配置】 > 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 > fetchRegistry: true ```yaml server: port: 8002 spring: application: name: cloud-payment-service datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包 url: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版 #defaultZone: http://localhost:7001/eureka # 单机版 mybatis: mapperLocations: classpath:mapper/*.xml type-aliases-package: com.atguigu.springcloud.entities # 所有Entity别名类所在包 ``` 修改8001/8002的Controller,返回当前服务的端口名 ```java @RestController @Slf4j public class PaymentController { @Value("${server.port}") private String serverPort; @Resource private PaymentService paymentService; @PostMapping(value = "/payment/create") public CommonResult create(@RequestBody Payment payment) { int result = paymentService.create(payment); log.info("*****插入操作返回结果:" + result); if(result > 0) { return new CommonResult(200,"插入成功,返回结果"+result+"\t 服务端口:"+serverPort,payment); }else{ return new CommonResult(444,"插入失败",null); } } @GetMapping(value = "/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id) { Payment payment = paymentService.getPaymentById(id); log.info("*****查询结果:{}",payment); if (payment != null) { return new CommonResult(200,"查询成功"+"\t 服务端口:"+serverPort,payment); }else{ return new CommonResult(444,"没有对应记录,查询ID: "+id,null); } } } ``` ###### (2)订单服务访问地址不能写死 ```java //public static final String PAYMENT_SRV = "http://localhost:8001"; // 通过在eureka上注册过的微服务名称调用 public static final String PAYMENT_SRV = "http://CLOUD-PAYMENT-SERVICE"; ``` ###### (3)使用@LoadBalanced注解赋予RestTemplate负载均衡的能力 > 提前说一下Ribbon的负载均衡功能: LoadBalanced底层与ribbon相似,使用方式一致 ```java @Configuration public class ApplicationContextBean { @Bean @LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力 public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` ###### (4)测试02 ![1660214046026](README.assets/1660214046026.png) #### actuator微服务信息完善 ###### (1)主机名称:服务名称修改 问题 没有主机名 ![1660214298878](README.assets/1660214298878.png) 修改yml ```yaml eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版 #defaultZone: http://localhost:7001/eureka # 单机版 # instance: instance-id: payment8001 ``` ![1660214366193](README.assets/1660214366193.png) ###### (2)访问信息有IP信息提示 问题 没有IP提示 修改yml ```yaml eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版 #defaultZone: http://localhost:7001/eureka # 单机版 instance: instance-id: payment8001 prefer-ip-address: true #访问路径可以显示IP地址 ``` ![1660214472156](README.assets/1660214472156.png) #### 服务发现Discovery > 对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息 ##### (1)修改cloud-provider-payment8001的Controller ```java package com.atguigu.springcloud.controller; import com.atguigu.springcloud.entities.CommonResult; import com.atguigu.springcloud.entities.Payment; import com.atguigu.springcloud.service.PaymentService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.List; import java.util.concurrent.TimeUnit; /** * @auther zzyy * @create 2020-01-27 21:17 */ @RestController @Slf4j public class PaymentController { @Value("${server.port}") private String serverPort; @Resource private PaymentService paymentService; //服务发现相关 @Resource private DiscoveryClient discoveryClient; @PostMapping(value = "/payment/create") public CommonResult create(@RequestBody Payment payment) { int result = paymentService.create(payment); log.info("*****插入操作返回结果:" + result); if(result > 0) { return new CommonResult(200,"插入成功,返回结果"+result+"\t 服务端口:"+serverPort,payment); }else{ return new CommonResult(444,"插入失败",null); } } @GetMapping(value = "/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id) { Payment payment = paymentService.getPaymentById(id); log.info("*****查询结果:{}",payment); if (payment != null) { return new CommonResult(200,"查询成功"+"\t 服务端口:"+serverPort,payment); }else{ return new CommonResult(444,"没有对应记录,查询ID: "+id,null); } } //服务发现相关 @GetMapping(value = "/payment/discovery") public Object discovery() { List services = discoveryClient.getServices(); for (String element : services) { System.out.println(element); } List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); for (ServiceInstance element : instances) { System.out.println(element.getServiceId() + "\t" + element.getHost() + "\t" + element.getPort() + "\t" + element.getUri()); } return this.discoveryClient; } } ``` ##### (2)8001主启动类添加注解`@EnableDiscoveryClient` ```java @SpringBootApplication @EnableEurekaClient @EnableDiscoveryClient //服务发现【可以平替@EnableEurekaClient】 public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class,args); } } ``` ##### (3)自测 ![1660214912458](README.assets/1660214912458.png) #### Eureka自我保护 ##### 1、故障现象 概述 保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式, Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。 如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式: EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE ![1660215009699](README.assets/1660215009699.png) ##### 2、导致原因 > 总结: > > 一句话:某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存 > > 属于CAP里面的AP分支 为什么会产生Eureka自我保护机制? 为了防止EurekaClient可以正常运行,但是 与 EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除 什么是自我保护模式? 默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。 在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。 它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着 综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。 ![1660215089163](README.assets/1660215089163.png) ##### 3、怎么禁止自我保护 ###### (1)注册中心eureakeServer端7001 ![1660215199235](README.assets/1660215199235.png) 修改yml ```yaml server: port: 7001 spring: application: name: eureka-cluster-server eureka: instance: hostname: eureka7001.com client: register-with-eureka: false fetch-registry: false service-url: #defaultZone: http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka defaultZone: http://eureka7001.com:7001/eureka server: #关闭自我保护机制,保证不可用服务被及时踢除 enable-self-preservation: false eviction-interval-timer-in-ms: 2000 ``` 关闭效果 ![1660215254444](README.assets/1660215254444.png) ###### ###### (2)生产者客户端eureakeClient端8001 ![1660215323695](README.assets/1660215323695.png) yml ```yaml server: port: 8001 ###服务名称(服务注册到eureka名称) spring: application: name: cloud-provider-payment eureka: client: #服务提供者provider注册进eureka服务列表内 service-url: register-with-eureka: true fetch-registry: true # cluster version #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka # singleton version defaultZone: http://eureka7001.com:7001/eureka #心跳检测与续约时间 #开发时设置小些,保证服务关闭后注册中心能即使剔除服务 instance: #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒) lease-renewal-interval-in-seconds: 1 #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务 lease-expiration-duration-in-seconds: 2 ``` ### zookeeper相关项目 ![1660215864730](README.assets/1660215864730.png) #### 1、基本项目架构 注册中心zookeeper需要安装到linux ![1660215992738](README.assets/1660215992738.png) ```txt cloud2020 cloud-provider-payment8004 服务提供者8004 cloud-consumerzk-order80 服务消费者80 ``` image-20220227225729931 #### 2、项目搭建 ##### (1)新建cloud-provider-payment8004 改pom【zookeeper版本必须与自己安装的一致】 ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloud-provider-payment8004 org.springframework.boot spring-boot-starter-web com.adun.springcloud cloud-api-commons ${project.version} org.springframework.cloud spring-cloud-starter-zookeeper-discovery org.apache.zookeeper zookeeper org.apache.zookeeper zookeeper 3.4.9 org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 写yml ```yaml #8004表示注册到zookeeper服务器的支付服务提供者端口号 server: port: 8004 #服务别名----注册zookeeper到注册中心名称 spring: application: name: cloud-provider-payment cloud: zookeeper: connect-string: 192.168.111.144:2181 ``` 主启动 ```java @SpringBootApplication @EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务 public class PaymentMain8004 { public static void main(String[] args) { SpringApplication.run(PaymentMain8004.class,args); } } ``` 业务类 ```java @RestController public class PaymentController { @Value("${server.port}") private String serverPort; @RequestMapping(value = "/payment/zk") public String paymentzk() { return "springcloud with zookeeper: "+serverPort+"\t"+ UUID.randomUUID().toString(); } } ``` 验证测试 http://localhost:8004/payment/zk ![1660216388381](README.assets/1660216388381.png) 验证测试2 ![1660216470623](README.assets/1660216470623.png) 思考:服务节点是临时节点还是持久节点 临时节点 ##### (2)新建cloud-consumerzk-order80 改pom ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloud-consumerzk-order81 org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-zookeeper-discovery org.apache.zookeeper zookeeper org.apache.zookeeper zookeeper 3.4.9 org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 写yml ```yaml server: port: 80 spring: application: name: cloud-consumer-order cloud: #注册到zookeeper地址 zookeeper: connect-string: 192.168.111.144:2181 ``` 主启动 ```java @SpringBootApplication @EnableDiscoveryClient public class OrderZK80 { public static void main(String[] args) { SpringApplication.run(OrderZK80.class, args); } } ``` 业务类 配置bean ```java @Configuration public class ApplicationContextBean { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` controller ```java @RestController public class OrderZKController { public static final String INVOKE_URL = "http://cloud-provider-payment"; @Autowired private RestTemplate restTemplate; @RequestMapping(value = "/consumer/payment/zk") public String paymentInfo() { String result = restTemplate.getForObject(INVOKE_URL+"/payment/zk", String.class); System.out.println("消费者调用支付服务(zookeeper)--->result:" + result); return result; } } ``` 验证测试 ![1660216702504](README.assets/1660216702504.png) 访问测试地址 http://localhost/consumer/payment/zk ### consul相关项目 ![1660216987745](README.assets/1660216987745.png) #### 基本项目架构 ```txt cloud2020 cloud-providerconsul-payment8006 服务提供者8006 cloud-consumerconsul-order80 服务消费者80 ``` image-20220227225915255 #### 1、原理 > https://www.consul.io/intro/index.html ##### 是什么 ![1660217067343](README.assets/1660217067343.png) Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发。 提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。 它具有很多优点。包括: 基于 raft 协议,比较简洁; 支持健康检查, 同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持 Linux、Mac、Windows ##### 做什么 ![1660217240530](README.assets/1660217240530.png) Spring Cloud Consul 具有如下特性: ![1660217221161](README.assets/1660217221161.png) ##### 怎么用 https://www.springcloud.cc/spring-cloud-consul.html #### 2、安装并运行Consul ![1660217391535](README.assets/1660217391535.png) 查看版本 ![1660217406773](README.assets/1660217406773.png) 结果页面 ![1660217425565](README.assets/1660217425565.png) #### 3、项目搭建 (1)新建Module支付服务provider8006 改pom ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloud-providerconsul-payment8006 org.springframework.cloud spring-cloud-starter-consul-discovery org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 写yml ```yaml ###consul服务端口号 server: port: 8006 spring: application: name: consul-provider-payment ####consul注册中心地址 cloud: consul: host: localhost port: 8500 discovery: #hostname: 127.0.0.1 service-name: ${spring.application.name} ``` 主启动 ```java @SpringBootApplication @EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务 public class PaymentMain8006 { public static void main(String[] args) { SpringApplication.run(PaymentMain8006.class, args); } } ``` 业务类 ```java @RestController public class PaymentController { @Value("${server.port}") private String serverPort; @RequestMapping(value = "/payment/consul") public String paymentzk() { return "springcloud with consul: "+serverPort+"\t"+ UUID.randomUUID().toString(); } } ``` 验证测试 http://localhost:8006/payment/consul ![1660217820544](README.assets/1660217820544.png) (2)新建Module消费服务order80 改pom ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloud-consumerconsul-order80 com.adun.springcloud cloud-api-commons ${project.version} org.springframework.cloud spring-cloud-starter-consul-discovery org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 写yml ```yaml ###consul服务端口号 server: port: 80 spring: application: name: cloud-consumer-order ####consul注册中心地址 cloud: consul: host: localhost port: 8500 discovery: #hostname: 127.0.0.1 service-name: ${spring.application.name} ``` 主启动 ```java @SpringBootApplication @EnableDiscoveryClient public class OrderConsulMain80 { public static void main(String[] args) { SpringApplication.run(OrderConsulMain80.class, args); } } ``` 业务类 配置bean ```java @Configuration public class ApplicationContextBean { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` controller ```java @RestController public class OrderConsulController { public static final String INVOKE_URL = "http://cloud-provider-payment"; //consul-provider-payment @Autowired private RestTemplate restTemplate; @GetMapping(value = "/consumer/payment/consul") public String paymentInfo() { String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul", String.class); System.out.println("消费者调用支付服务(consule)--->result:" + result); return result; } } ``` 验证测试 http://localhost/consumer/payment/consul ![1660217979128](README.assets/1660217979128.png) ### 三个注册中心的异同点 ![1660218033204](README.assets/1660218033204.png) ##### 经典CAP 最多只能同时较好的满足两个。 CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求, 因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类: CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。 CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。 AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。 image-20220227230802462 image-20220227230944151 #### AP(Eureka) AP架构 当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。 结论:违背了一致性C的要求,只满足可用性和分区容错,即AP ![1660218227773](README.assets/1660218227773.png) #### CP(Zookeeper/Consul) CP架构 当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性 结论:违背了可用性A的要求,只满足一致性和分区容错,即CP ![1660218259751](README.assets/1660218259751.png) ## 微服务负载均衡组件 ### Ribbon负载均衡服务调用 ![1660218861061](README.assets/1660218861061.png) #### 基本项目架构 ``` cloud2020 cloud-api-commons 服务提供与消费共同使用的相关类 cloud-eureka-server7001 服务注册中心7001 cloud-eureka-server7002 服务注册中线7002 cloud-consumer-oreder80 服务消费80 cloud-provider-payment8001 服务提供8001 cloud-provider-payment8002 服务提供8002 ``` #### 1、概述 ##### (1)是什么 Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具。 简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。 ##### (2)官网资料 https://github.com/Netflix/ribbon/wiki/Getting-Started Ribbon目前也进入维护模式 ![1660219009331](README.assets/1660219009331.png) 未来替换方案 ![](README.assets/图片.jpeg) ##### (3)能做什么 ![1660219258563](README.assets/1660219258563.png) 集中式LB 即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx), 由该设施负责把访问请求通过某种策略转发至服务的提供方; 进程内LB 将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。 Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。 #### 2、Ribbon负载均衡演示 ##### (1)架构说明 ![1660228668284](README.assets/1660228668284.png) Ribbon在工作时分成两步 第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server. 第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。 其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。 总结:Ribbon其实就是一个软负载均衡的客户端组件, 他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例 ##### (2)pom说明【springboot的eureka-client的starter集成了ribbon】 之前写样例时候没有引入spring-cloud-starter-ribbon也可以使用ribbon, ```xml org.springframework.cloud spring-cloud-starter-netflix-ribbon ``` 猜测spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用, 证明如下: 可以看到spring-cloud-starter-netflix-eureka-client 确实引入了Ribbon ![1660228926991](README.assets/1660228926991.png) ##### (3)使用RestTemplate+Eurka+Ribbon实现负载均衡 > 查看: > > Eureka项目相关 》集群Eureka构建步骤》负载均衡环境搭建测试 #### 3、Ribbon核心组件IRule ##### (1)IRule:根据特定算法中从服务列表中选取一个要访问的服务 ![1660229915271](README.assets/1660229915271.png) ![1660229958538](README.assets/1660229958538.png) ```java com.netflix.loadbalancer.RoundRobinRule 轮询 com.netflix.loadbalancer.RandomRule 随机 com.netflix.loadbalancer.RetryRule 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务 WeightedResponseTimeRule 对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择 BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 AvailabilityFilteringRule 先过滤掉故障实例,再选择并发较小的实例 ZoneAvoidanceRule 默认规则,复合判断server所在区域的性能和server的可用性选择服务器 ``` ##### (2)如何替换(自定义负载均衡算法) ![1660230286826](README.assets/1660230286826.png) ###### (1)注意配置细节 > 官方文档明确给出了警告: > 这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下, > 否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了 ![1660230309168](README.assets/1660230309168.png) ###### (2)新建package【注意位置】 ![1660230452595](README.assets/1660230452595.png) ###### (3)上面包下新建MySelfRule规则类 ```java @Configuration public class MySelfRule { @Bean public IRule myRule() { return new RandomRule();//定义为随机 } } ``` ###### (4)主启动类添加`@RibbonClient` ```java //自定义ribbon负载均衡算法 //分别指定多个服务调用负载均衡算法 //@RibbonClients(value = { // @RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration= MySelfRule.class), // @RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration= MySelfRule.class)}) //指定单个服务调用负载均衡算法 @RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration= MySelfRule.class) public class OrderMain80 { public static void main(String[] args) { SpringApplication.run(OrderMain80.class, args); } } ``` ###### (5)测试 http://localhost/consumer/payment/get/31 #### 4、Ribbon负载均衡算法 ![1660230777089](README.assets/1660230777089.png) ##### (1)原理 负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。 `List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");` 如: List [0] instances = 127.0.0.1:8002    List [1] instances = 127.0.0.1:8001 8001+ 8002 组合成为集群,它们共计2台机器,集群总数为2, 按照轮询算法原理: 当总请求数为1时: 1 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001 当总请求数位2时: 2 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002 当总请求数位3时: 3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001 当总请求数位4时: 4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002 如此类推...... ##### (2)RoundRobinRule源码 ```java // // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.netflix.loadbalancer; import com.netflix.client.config.IClientConfig; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class RoundRobinRule extends AbstractLoadBalancerRule { private AtomicInteger nextServerCyclicCounter; private static final boolean AVAILABLE_ONLY_SERVERS = true; private static final boolean ALL_SERVERS = false; private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class); public RoundRobinRule() { this.nextServerCyclicCounter = new AtomicInteger(0); } public RoundRobinRule(ILoadBalancer lb) { this(); this.setLoadBalancer(lb); } public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { log.warn("no load balancer"); return null; } else { Server server = null; int count = 0; while(true) { if (server == null && count++ < 10) { //获取所有启用并可访问的服务器 List reachableServers = lb.getReachableServers(); //获取所有已知服务器(可达和不可达的) List allServers = lb.getAllServers(); //可达服务器总数 int upCount = reachableServers.size(); //所有服务器总数 int serverCount = allServers.size(); if (upCount != 0 && serverCount != 0) { int nextServerIndex = this.incrementAndGetModulo(serverCount); server = (Server)allServers.get(nextServerIndex); if (server == null) { Thread.yield(); } else { if (server.isAlive() && server.isReadyToServe()) { return server; } server = null; } continue; } log.warn("No up servers available from load balancer: " + lb); return null; } if (count >= 10) { log.warn("No available alive servers after 10 tries from load balancer: " + lb); } return server; } } } private int incrementAndGetModulo(int modulo) { int current; int next; do { current = this.nextServerCyclicCounter.get(); //核心就是取模 next = (current + 1) % modulo; } while(!this.nextServerCyclicCounter.compareAndSet(current, next)); return next; } public Server choose(Object key) { return this.choose(this.getLoadBalancer(), key); } public void initWithNiwsConfig(IClientConfig clientConfig) { } } ``` ##### (3)手写 > 自己试着写一个本地负载均衡器试试 ![1660232423375](README.assets/1660232423375.png) - eureka-server 7001/7002集群启动 - 8001/8002微服务改造 controller ```java @RestController @Slf4j @RequestMapping(value ="/payment") public class PaymentController { @Resource private PaymentService paymentService; @Value("${server.port}") private String port; @Resource private DiscoveryClient discoveryClient; @PostMapping(value = "/create") public CommonResult create(@RequestBody Payment payment){ int result = paymentService.create(payment); log.info("*****插入操作返回结果:" + result); if(result>0){ return new CommonResult(200,"插入数据库成功:ServerPort:"+port,result); }else{ return new CommonResult(444,"插入数据库失败",null); } } @GetMapping(value = "/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id){ Payment payment = paymentService.getPaymentById(id); log.info("*****查询结果:{}",payment); if (payment != null) { return new CommonResult(200,"查询成功:ServerPort:"+port,payment); }else { return new CommonResult(444,"没有对应记录,查询ID: "+id,null); } } @GetMapping(value = "/discovery") public Object discovery(){ List services = discoveryClient.getServices(); for (String service : services) { log.info("****service:{}",service); } List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); for (ServiceInstance instance : instances) { log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri()); } return this.discoveryClient; } ``` //负载均衡测试 @GetMapping(value = "/lb") public String getPaymentLB(){ return port; } } ``` - 80订单微服务改造 (i)ApplicationContextBean去掉注解@LoadBalanced) ​```java @Configuration public class ApplicationContextBean { @Bean //@LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` (ii)LoadBalancer接口 ```java public interface LoadBalancer { ServiceInstance instances(List serviceInstances); } ``` (iii)MyLB ```java @Component public class MyLB implements LoadBalancer { private AtomicInteger atomicInteger = new AtomicInteger(0); public final int getAndIncrement() { int current; int next; do { current = this.atomicInteger.get(); next = current >= 2147483647 ? 0 : current + 1; } while(!this.atomicInteger.compareAndSet(current, next)); System.out.println("*****next: "+next); return next; } @Override public ServiceInstance instances(List serviceInstances) { int index = getAndIncrement() % serviceInstances.size(); return serviceInstances.get(index); } } ``` (iv)OrderController ```java @RestController public class OrderController { //public static final String PAYMENT_SRV = "http://localhost:8001"; public static final String PAYMENT_SRV = "http://CLOUD-PAYMENT-SERVICE"; @Resource private RestTemplate restTemplate; //可以获取注册中心上的服务列表 @Resource private DiscoveryClient discoveryClient; @Resource private LoadBalancer loadBalancer; @GetMapping("/consumer/payment/create") public CommonResult create(Payment payment) { return restTemplate.postForObject(PAYMENT_SRV+"/payment/create",payment,CommonResult.class); } @GetMapping("/consumer/payment/get/{id}") public CommonResult getPayment(@PathVariable("id") Long id) { return restTemplate.getForObject(PAYMENT_SRV+"/payment/get/"+id,CommonResult.class); } @GetMapping("/consumer/payment/getForEntity/{id}") public CommonResult getPayment2(@PathVariable("id") Long id) { ResponseEntity entity = restTemplate.getForEntity(PAYMENT_SRV+"/payment/get/"+id, CommonResult.class); if(entity.getStatusCode().is2xxSuccessful()){ return entity.getBody(); }else { return new CommonResult(444, "操作失败"); } } //自定义负载均衡算法 @Resource private LoadBalancer loadBalancer; @GetMapping("/consumer/payment/lb") public String getPaymentLB() { List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); if(instances == null || instances.size()<=0) { return null; } ServiceInstance serviceInstance = loadBalancer.instances(instances); URI uri = serviceInstance.getUri(); return restTemplate.getForObject(uri+"/payment/lb",String.class); } } ``` (v)测试 http://localhost/consumer/payment/lb ## 微服务调用 ### restTemplate 进行简单的服务调用 image-20220308142650801 image-20220308142726146 ### ribbon+restTemplate 使用restTemplate进行服务调用 使用ribbon进行负载均衡加强 ```java package com.adun.springcloud.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class ApplicationContextConfig { @Bean //restTemplate启用ribbon的负载均衡 @LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } } ``` 相关项目结构 image-20220308162636836 ### openFeign > 底层封装ribbon,使用ribbon来进行负载均衡 ![1660274713426](README.assets/1660274713426.png) #### 基本项目架构 image-20220308162730725 ```txt cloud2020 cloud-api-commons 服务提供与消费共同使用的相关类 cloud-eureka-server7001 服务注册中心7001 cloud-eureka-server7002 服务注册中线7002 cloud-consumer-feign-order80 服务消费80 cloud-provider-payment8001 服务提供8001 cloud-provider-payment8002 服务提供8002 ``` #### 1、概述 ![1660274839994](README.assets/1660274839994.png) (1)OpenFeign是什么 > Feign是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可 GitHub: https://github.com/spring-cloud/spring-cloud-openfeign 官网解释: https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/#spring-cloud-openfeign Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。 它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡 ![1660274982128](README.assets/1660274982128.png) (2)能干嘛 Feign能干什么 Feign旨在使编写Java Http客户端变得更容易。 前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。 Feign集成了Ribbon 利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用 (3)Feign和OpenFeign的区别 | Feign | OpenFeign | |:-------------------------------------------------------------------------------------------- |:-------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端 | | |
Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 | OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。 | | | | | org.springframework.cloud | | spring-cloud-starter-feign | org.springframework.cloud spring-cloud-starter-openfeign | #### 2、OpenFeign使用步骤 ![1660276272832](README.assets/1660276272832.png) ##### 项目搭建 新建cloud-consumer-feign-order80 > Feign在消费端使用 ![1660276360859](README.assets/1660276360859.png) ##### (1)改pom ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloud-consumer-feign-order80 org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-netflix-eureka-client com.adun.springcloud cloud-api-commons ${project.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` ##### (2)写yml ```yaml server: port: 80 eureka: client: register-with-eureka: false service-url: #服务注册进eureka-server集群 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ ``` ##### (3)主启动 ```java @SpringBootApplication //激活feign @EnableFeignClients public class OrderFeignMain80 { public static void main(String[] args) { SpringApplication.run(OrderFeignMain80.class,args); } } ``` ##### (4)业务类 ![1660276624515](README.assets/1660276624515.png) PaymentFeignService ```java @Component @FeignClient(value = "CLOUD-PAYMENT-SERVICE") public interface PaymentFeignService { @GetMapping(value = "/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id); @GetMapping(value = "/payment/feign/timeout") public String paymentFeignTimeOut(); } ``` controller ```java @RestController public class OrderFeignController { @Resource private PaymentFeignService paymentFeignService; @GetMapping(value = "/consumer/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id) { return paymentFeignService.getPaymentById(id); } } ``` ##### (5)测试 ![1660276746110](README.assets/1660276746110.png) #### 3、OpenFeign超时控制 ![1660277035123](README.assets/1660277035123.png) ##### 项目结构 ```txt cloud2020 cloud-api-commons 服务提供与消费共同使用的相关类 cloud-eureka-server7001 服务注册中心7001 cloud-eureka-server7002 服务注册中线7002 cloud-consumer-feign-order80 服务消费80 cloud-provider-payment8001 服务提供8001 ``` ##### 是什么 > OpenFeign默认等待1秒钟,超过后报错 ```txt 默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。 为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。 yml文件中开启配置 ``` OpenFeign默认支持Ribbon ![1660277569410](README.assets/1660277569410.png) YML文件里需要开启OpenFeign客户端超时控制 ```yaml server: port: 80 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ #设置feign客户端超时时间(OpenFeign默认支持ribbon) ribbon: #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 ReadTimeout: 5000 #指的是建立连接后从服务器读取到可用资源所用的时间 ConnectTimeout: 5000 ``` ##### bug复现修改 ##### (1)服务提供方8001故意写暂停程序 ```java @RestController @Slf4j public class PaymentController { @Value("${server.port}") private String serverPort; @Resource private PaymentService paymentService; @Resource private DiscoveryClient discoveryClient; @PostMapping(value = "/payment/create") public CommonResult create(@RequestBody Payment payment) { int result = paymentService.create(payment); log.info("*****插入操作返回结果:" + result); if(result > 0) { return new CommonResult(200,"插入成功,返回结果"+result+"\t 服务端口:"+serverPort,payment); }else{ return new CommonResult(444,"插入失败",null); } } @GetMapping(value = "/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id) { Payment payment = paymentService.getPaymentById(id); log.info("*****查询结果:{}",payment); if (payment != null) { return new CommonResult(200,"查询成功"+"\t 服务端口:"+serverPort,payment); }else{ return new CommonResult(444,"没有对应记录,查询ID: "+id,null); } } @GetMapping(value = "/payment/discovery") public Object discovery() { List services = discoveryClient.getServices(); for (String element : services) { System.out.println(element); } List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); for (ServiceInstance element : instances) { System.out.println(element.getServiceId() + "\t" + element.getHost() + "\t" + element.getPort() + "\t" + element.getUri()); } return this.discoveryClient; } @GetMapping(value = "/payment/lb") public String getPaymentLB() { System.out.println("*****lb from port: "+serverPort); return serverPort; } //Feign超时演示 @GetMapping(value = "/payment/feign/timeout") public String paymentFeignTimeOut() { System.out.println("*****paymentFeignTimeOut from port: "+serverPort); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return serverPort; } } ``` ##### (2)服务消费方80添加超时方法PaymentFeignService ```java @Component @FeignClient(value = "CLOUD-PAYMENT-SERVICE") public interface PaymentFeignService { @GetMapping(value = "/payment/get/{id}") CommonResult getPaymentById(@PathVariable("id") Long id); //Feign超时演示 @GetMapping(value = "/payment/feign/timeout") String paymentFeignTimeOut(); } ``` ##### (3)服务消费方80添加超时方法OrderFeignController ```java @RestController public class OrderFeignController { @Resource private PaymentFeignService paymentFeignService; @GetMapping(value = "/consumer/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id) { return paymentFeignService.getPaymentById(id); } //Feign超时演示 @GetMapping(value = "/consumer/payment/feign/timeout") public String paymentFeignTimeOut() { return paymentFeignService.paymentFeignTimeOut(); } } ``` ##### (4)测试 ![1660277349675](README.assets/1660277349675.png) ![1660277361703](README.assets/1660277361703.png) #### 4、OpenFeign日志打印功能 ![1660277713102](README.assets/1660277713102.png) ##### (1)是什么 Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。 说白了就是对Feign接口的调用情况进行监控和输出 ##### (2)日志级别 ```shell NONE:默认的,不显示任何日志; BASIC:仅记录请求方法、URL、响应状态码及执行时间; HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息; FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。 ``` ##### (3)配置日志Bean ```java @Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } } ``` ##### (4)YML文件里需要开启日志的Feign客户端 ```yaml server: port: 80 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ #设置feign客户端超时时间 #springCloud默认开启支持ribbon ribbon: #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 ReadTimeout: 5000 #指的是建立连接后从服务器读取到可用资源所用的时间 ConnectTimeout: 5000 logging: level: # feign日志以什么级别监控哪个接口 com.atguigu.springcloud.service.PaymentFeignService: debug ``` ##### (5)后台日志查看 ![1660277876931](README.assets/1660277876931.png) ## 服务熔断降级与限流 ### Hystrix断路器 > 注意: > > 1. 生产者配置降级与熔断【熔断器,降级处理,有明显区别】 > 2. 消费者配置降级【降级推荐配置到客户】 > 3. 监控面板需要配合actuator进行使用 ![1660293406450](README.assets/1660293406450.png) #### 基本项目架构 ```txt cloud2020 cloud-api-commons 服务提供与消费共同使用的相关类 cloud-eureka-server7001 服务注册中心7001 cloud-eureka-server7002 服务注册中线7002 cloud-consumer-feign-hystrix-order80 服务消费80 cloud-provider-hystrix-payment8001 服务提供8001 ``` image-20220308164134879 #### 1、概述 ![1660293695331](README.assets/1660293695331.png) ##### (1)分布式系统面临的问题 分布式系统面临的问题 ==复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。==【可能引发服务雪崩】 ![1660293780285](README.assets/1660293780285.png) ##### (2)Hystrix是什么 Hystrix是一个用于处理分布式系统的延迟容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。 “断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。 #### 2、Hystrix重要概念 ![1660295592280](README.assets/1660295592280.png) #### 3、hystrix案例 ![1660299510294](README.assets/1660299510294.png) ##### 构建项目 ##### (1)新建cloud-provider-hystrix-payment8001 写pom ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloud-provider-hystrix-payment8001 org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator com.adun.springcloud cloud-api-commons ${project.version} org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 改yml ```yaml server: port: 8001 spring: application: name: cloud-provider-hystrix-payment eureka: client: register-with-eureka: true fetch-registry: true service-url: #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka defaultZone: http://eureka7001.com:7001/eureka ``` 主启动 ```java @SpringBootApplication @EnableEurekaClient //本服务启动后会自动注册进eureka服务中 public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class,args); } } ``` 业务类 service ```java @Service public class PaymentService { /** * 正常访问,一切OK * @param id * @return */ public String paymentInfo_OK(Integer id) { return "线程池:"+Thread.currentThread().getName()+"paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O"; } /** * 超时访问,演示降级 * @param id * @return */ public String paymentInfo_TimeOut(Integer id) { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池:"+Thread.currentThread().getName()+"paymentInfo_TimeOut,id: "+id+"\t"+"O(∩_∩)O,耗费3秒"; } } ``` controller ```java @RestController @Slf4j public class PaymentController { @Autowired private PaymentService paymentService; @Value("${server.port}") private String serverPort; @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentService.paymentInfo_OK(id); log.info("****result: "+result); return result; } @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException { String result = paymentService.paymentInfo_TimeOut(id); log.info("****result: "+result); return result; } } ``` ##### (2)测试【基本+高并发】 ![1660299822361](README.assets/1660299822361.png) ![1660299871643](README.assets/1660299871643.png) 正常测试OK 高并发测试GG 配置jmeter ![1660300024729](README.assets/1660300024729.png) 为什么会卡死 > tomcat的默认的工作线程数被打满 了,没有多余的线程来分解压力和处理。 ##### (3)新建cloud-consumer-feign-hystrix-order80 改pom ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloud-consumer-feign-hystrix-order80 org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-netflix-eureka-client com.adun.springcloud cloud-api-commons ${project.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 写yml ```yaml server: port: 80 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/ ``` 主启动 ```java @SpringBootApplication @EnableFeignClients public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class,args); } } ``` 业务类 PaymentHystrixService ```java @Component @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT") public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") String paymentInfo_OK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") String paymentInfo_TimeOut(@PathVariable("id") Integer id); } ``` OrderHystirxController ```java @RestController @Slf4j public class OrderHystirxController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_OK(id); return result; } @GetMapping("/consumer/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; } } ``` 测试 ![1660300499181](README.assets/1660300499181.png) ![1660300513844](README.assets/1660300513844.png) ##### 上述问题产生原因 ![1660308215761](README.assets/1660308215761.png) ##### 优化 ##### 服务降级 ![1660308837873](README.assets/1660308837873.png) ###### (1)8001进行fallback修改升级 ![1660309619933](README.assets/1660309619933.png) ```java @Service public class PaymentService { /** * 正常访问,一切OK * @param id * @return */ public String paymentInfo_OK(Integer id) { return "线程池:"+Thread.currentThread().getName()+"paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O"; } /** * 超时访问,演示降级 * @param id * @return */ @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000") }) public String paymentInfo_TimeOut(Integer id) { //现在只有超时 int second = 5; try { TimeUnit.SECONDS.sleep(second); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池:"+Thread.currentThread().getName()+"paymentInfo_TimeOut,id: "+id+"\t"+"O(∩_∩)O,耗费秒: "+second; } public String paymentInfo_TimeOutHandler(Integer id){ return "/(ㄒoㄒ)/调用支付接口超时或异常:\t"+ "\t当前线程池名字" + Thread.currentThread().getName(); } } ``` ###### (2)图示分析 ![1660309295534](README.assets/1660309295534.png) 上图故意制造两个异常: 1 int age = 10/0; 计算异常 2 我们能接受3秒钟,它运行5秒钟,超时异常。 当前服务不可用了,做服务降级,兜底的方案都是paymentInfo_TimeOutHandler ###### (3)主启动类激活Hystrix ```java 添加新注解@EnableCircuitBreaker //激活Hystrix断路器 ``` ###### (4)80fallback修改升级 ![1660309599390](README.assets/1660309599390.png) 修改yml ```yaml server: port: 80 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/ feign: hystrix: enabled: true ``` 主启动添加注解`@EnableHystrix` ```java @SpringBootApplication @EnableFeignClients //激活Hystrix @EnableHystrix public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class, args); } } ``` 业务类 PaymentHystirxController ```java @RestController @Slf4j public class PaymentHystirxController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_OK(id); return result; } @GetMapping("/consumer/payment/hystrix/timeout/{id}") @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500") }) public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; } public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) { return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出sh错请检查自己,o(╥﹏╥)o"; } } ``` ###### 此时出现代码膨胀与混乱的问题 ![1660310858462](README.assets/1660310858462.png) 去膨胀 ```txt @DefaultProperties(defaultFallback = "") 1:1 每个方法配置一个服务降级方法,技术上可以,实际上傻X 1:N 除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback = "") 统一跳转到统一处理结果页面 通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量,O(∩_∩)O哈哈~ ``` ![1660311040523](README.assets/1660311040523.png) controller配置 ```java @RestController @Slf4j @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") public class PaymentHystirxController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_OK(id); return result; } @GetMapping("/consumer/payment/hystrix/timeout/{id}") @HystrixCommand //加了@DefaultProperties属性注解,并且没有写具体方法名字,就用统一全局的 public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; } public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) { return "paymentTimeOutFallbackMethod,对方系统繁忙,请10秒钟后再次尝试/(ㄒoㄒ)/"; } public String payment_Global_FallbackMethod() { return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~"; } } ``` 解决代码混乱 服务降级,客户端去调用服务端,碰上服务端宕机或关闭 ![1660312212834](README.assets/1660312212834.png) controller代码混乱 ![1660312268702](README.assets/1660312268702.png) 混合在一块 ,每个业务方法都要提供一个。 修改cloud-consumer-feign-hystrix-order80 PaymentFeignClientService接口 ```java @Component @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class) public interface PaymentFeignClientService { @GetMapping("/payment/hystrix/{id}") public String getPaymentInfo(@PathVariable("id") Integer id); } ``` PaymentFallbackService类实现PaymentFeignClientService接口 ```java @Component //必须加 //必须加 //必须加 public class PaymentFallbackService implements PaymentFeignClientService { @Override public String getPaymentInfo(Integer id) { return "服务调用失败,提示来自:cloud-consumer-feign-order80"; } } ``` yml开启hystrix ```yml server: port: 80 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #logging: # level: # # feign日志以什么级别监控哪个接口 # com.atguigu.springcloud.service.PaymentFeignClientService: debug # 用于服务降级 在注解@FeignClient中添加fallbackFactory属性值 feign: hystrix: enabled: true #在Feign中开启Hystrix ``` 测试 ![1660312468450](README.assets/1660312468450.png) ##### 服务熔断 ![1660319145029](README.assets/1660319145029.png) ###### (1)熔断是什么 > 大神论文 https://martinfowler.com/bliki/CircuitBreaker.html 熔断机制概述 熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时, **会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。** 当检测到该节点微服务调用响应正常后,恢复调用链路。 在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况, 当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。**熔断机制的注解是@HystrixCommand**。 ###### (2)实操 修改cloud-provider-hystrix-payment8001 1. PaymentService why配置这些参数 ![1660319517753](README.assets/1660319517753.png) ![1660319637318](README.assets/1660319637318.png) ```java //=========服务熔断 @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = { @HystrixProperty(name = "circuitBreaker.enabled",value = "true"), @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"), }) public String paymentCircuitBreaker(@PathVariable("id") Integer id) { if(id < 0) { throw new RuntimeException("******id 不能负数"); } String serialNumber = IdUtil.simpleUUID(); return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber; } public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) { return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id; } ``` 2. PaymentController ```java @GetMapping("/payment/circuit/{id}") public String paymentCircuitBreaker(@PathVariable("id") Integer id) { String result = paymentService.paymentCircuitBreaker(id); log.info("****result: "+result); return result; } ``` 3. 测试 ![1660319697769](README.assets/1660319697769.png) ###### (3)原理(小总结) ![1660319771095](README.assets/1660319771095.png) (i)大神结论 ![1660319798025](README.assets/1660319798025.png) (ii)官网断路器流程图 ![1660319823375](README.assets/1660319823375.png) (iii)官网步骤 ![1660319847311](README.assets/1660319847311.png) (iv)断路器在什么情况下开始起作用 ![1660319866489](README.assets/1660319866489.png) 涉及到断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值。 1:快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。 2:请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。 3:错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。 (v)断路器开启或者关闭的条件 ![1660319949037](README.assets/1660319949037.png) (vi)断路器打开之后 ```txt 1:再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。 2:原来的主逻辑要如何恢复呢? 对于这一问题,hystrix也为我们实现了自动恢复功能。 当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑, 当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合, 主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时 ``` (vii)All配置 ```java //========================All @HystrixCommand(fallbackMethod = "str_fallbackMethod", groupKey = "strGroupCommand", commandKey = "strCommand", threadPoolKey = "strThreadPool", commandProperties = { // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离 @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"), // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数) @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"), // 配置命令执行的超时时间 @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"), // 是否启用超时时间 @HystrixProperty(name = "execution.timeout.enabled", value = "true"), // 执行超时的时候是否中断 @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"), // 执行被取消的时候是否中断 @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"), // 允许回调方法执行的最大并发数 @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"), // 服务降级是否启用,是否执行回调函数 @HystrixProperty(name = "fallback.enabled", value = "true"), // 是否启用断路器 @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候, // 如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"), // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 // circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50, // 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"), // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后, // 会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态, // 如果成功就设置为 "关闭" 状态。 @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"), // 断路器强制打开 @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"), // 断路器强制关闭 @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"), // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间 @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"), // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据 // 设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。 // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常 @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"), // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。 @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"), // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。 @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"), // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。 @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"), // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数, // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行, // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。 @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"), // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。 @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"), // 是否开启请求缓存 @HystrixProperty(name = "requestCache.enabled", value = "true"), // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中 @HystrixProperty(name = "requestLog.enabled", value = "true"), }, threadPoolProperties = { // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量 @HystrixProperty(name = "coreSize", value = "10"), // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列, // 否则将使用 LinkedBlockingQueue 实现的队列。 @HystrixProperty(name = "maxQueueSize", value = "-1"), // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。 // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue // 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"), } ) public String strConsumer() { return "hello 2020"; } public String str_fallbackMethod() { return "*****fall back str_fallbackMethod"; } ``` ##### 服务限流 > 后面高级篇讲解alibaba的Sentinel说明 #### 4、hystrix工作流程 > https://github.com/Netflix/Hystrix/wiki/How-it-Works 官网图例 ![1660320235200](README.assets/1660320235200.png) 步骤说明 | | | | ---- | ------------------------------------------------------------ | | 1 | 创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象。 | | 2 | 命令执行。其中 HystrixComand 实现了下面前两种执行方式;而 HystrixObservableCommand 实现了后两种执行方式:execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常。queue():异步执行, 直接返回 一个Future对象, 其中包含了服务执行结束时要返回的单一结果对象。observe():返回 Observable 对象,它代表了操作的多个结果,它是一个 Hot Obserable(不论 "事件源" 是否有 "订阅者",都会在创建后对事件进行发布,所以对于 Hot Observable 的每一个 "订阅者" 都有可能是从 "事件源" 的中途开始的,并可能只是看到了整个操作的局部过程)。toObservable(): 同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有 "订阅者" 的时候并不会发布事件,而是进行等待,直到有 "订阅者" 之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程)。 | | 3 | 若当前命令的请求缓存功能是被启用的, 并且该命令缓存命中, 那么缓存的结果会立即以 Observable 对象的形式 返回。 | | 4 | 检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到 fallback 处理逻辑(第 8 步);如果断路器是关闭的,检查是否有可用资源来执行命令(第 5 步)。 | | 5 | 线程池/请求队列/信号量是否占满。如果命令依赖服务的专有线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满, 那么 Hystrix 也不会执行命令, 而是转接到 fallback 处理逻辑(第8步)。 | | 6 | Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。HystrixCommand.run() :返回一个单一的结果,或者抛出异常。HystrixObservableCommand.construct(): 返回一个Observable 对象来发射多个结果,或通过 onError 发送错误通知。 | | 7 | Hystrix会将 "成功"、"失败"、"拒绝"、"超时" 等信息报告给断路器, 而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行 "熔断/短路"。 | | 8 | 当命令执行失败的时候, Hystrix 会进入 fallback 尝试回退处理, 我们通常也称该操作为 "服务降级"。而能够引起服务降级处理的情况有下面几种:第4步: 当前命令处于"熔断/短路"状态,断路器是打开的时候。第5步: 当前命令的线程池、 请求队列或 者信号量被占满的时候。第6步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候。 | | 9 | 当Hystrix命令执行成功之后, 它会将处理结果直接返回或是以Observable 的形式返回。
tips:如果我们没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常, Hystrix 依然会返回一个 Observable 对象, 但是它不会发射任何结果数据, 而是通过 onError 方法通知命令立即中断请求,并通过onError()方法将引起命令失败的异常发送给调用者。 | | | | > tips:如果我们没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常, Hystrix 依然会返回一个 Observable 对象, 但是它不会发射任何结果数据, 而是通过 onError 方法通知命令立即中断请求,并通过onError()方法将引起命令失败的异常发送给调用者。 #### 5、服务监控hystrixDashboard ![1660320599638](README.assets/1660320599638.png) ###### 基本项目结构 ``` cloud2020 cloud-consumer-hystrix-dashboard9001 cloud-provider-hystrix-payment8001 ``` ###### 概述 除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。 ###### 新建项目仪表盘9001 新建cloud-consumer-hystrix-dashboard9001 改pom ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloud-consumer-hystrix-dashboard9001 org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 写yml ```yaml server: port: 9001 ``` 主启动 HystrixDashboardMain9001+新注解@EnableHystrixDashboard ```java @SpringBootApplication @EnableHystrixDashboard public class HystrixDashboardMain9001 { public static void main(String[] args) { SpringApplication.run(MainApp9001.class,args); } } ``` 所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置 ```java org.springframework.boot spring-boot-starter-actuator ``` 启动cloud-consumer-hystrix-dashboard9001该微服务后续将监控微服务8001 http://localhost:9001/hystrix ![1660321028339](README.assets/1660321028339.png) ###### 断路器演示(服务监控hystrixDashboard) ![1660321075981](README.assets/1660321075981.png) 修改cloud-provider-hystrix-payment8001 ```java @SpringBootApplication @EnableEurekaClient //本服务启动后会自动注册进eureka服务中 @EnableCircuitBreaker//对hystrixR熔断机制的支持 public class MainAppHystrix8001 { public static void main(String[] args) { SpringApplication.run(MainAppHystrix8001.class,args); } /** *此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑 *ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream", *只要在自己的项目里配置上下面的servlet就可以了 */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } } ``` ![1660321167351](README.assets/1660321167351.png) 成功 ![1660321191477](README.assets/1660321191477.png) 失败 ![1660321207713](README.assets/1660321207713.png) 如何看 7色 1圈 > 实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色<黄色<橙色<红色递减。 > 该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现==故障实例和高压力实例。== 1线 > 曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。 整图说明 ![1660321306166](README.assets/1660321306166.png) 整图说明2 ![1660321324136](README.assets/1660321324136.png) 搞懂一个才能看懂复杂的 ![1660321338635](README.assets/1660321338635.png) ## 服务配置中心 image-20220313170052544 ### Spring Cloud Config #### configServer ##### pom.xml ```xml org.springframework.cloud spring-cloud-config-server org.springframework.boot spring-boot-starter-actuator ``` ##### application.yml ```yaml spring: application: name: cloud-config-center #注册进Eureka服务器的微服务名 cloud: config: server: git: uri: git@github.com:adun/springcloud-config.git #GitHub上面的git仓库名字 ####搜索目录 search-paths: - springcloud-config ####读取分支 label: master ``` ##### 主启动类 ```java @EnableConfigServer ``` #### 微服务客户实例 ##### pom.xml ```xml org.springframework.cloud spring-cloud-starter-config org.springframework.boot spring-boot-starter-actuator ``` ##### bootstrap.yml ```yaml server: port: 3355 spring: application: name: config-client cloud: #Config客户端配置 config: label: master #分支名称 name: config #配置文件名称 profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml uri: http://localhost:3344 #配置中心地址k #服务注册到eureka地址 eureka: client: service-url: defaultZone: http://localhost:7001/eureka # 暴露监控端点 management: endpoints: web: exposure: include: "*" ``` ##### 需要刷新的类,添加注解 ```java //可动态刷新配置 @RefreshScope ``` ### Spring Cloud Bus【底层使用消息中间件进行通知】 > Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。 Spring Cloud Bus 配合 Spring Cloud Config 使用可以实现配置的动态刷新。 ==Bus支持两种消息代理:RabbitMQ 和 Kafka== 可以通过引入不同的坐标进行选择 > `spring-cloud-starter-bus-amqp` or `spring-cloud-starter-bus-kafka` #### 什么是总线 在微服务架构的系统中,==通常会使用轻量级的消息代理来构建一个共用的消息主题==,并让系统中所有微服务实例都连接上来。==由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。==在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。 #### 基本原理 ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。 #### 基本架构 ![image-20220313164848797](https://cloudimgs-1301504220.cos.ap-nanjing.myqcloud.com/image/202203131648869.png) #### 配置中心服务端 ##### pom.xml ```xml org.springframework.cloud spring-cloud-starter-bus-amqp org.springframework.boot spring-boot-starter-actuator ``` ##### yml ```yaml #rabbitmq相关配置 rabbitmq: host: localhost port: 5672 username: guest password: guest ##rabbitmq相关配置,暴露bus刷新配置的端点 management: endpoints: #暴露bus刷新配置的端点 web: exposure: include: 'bus-refresh' ``` ##### 主启动类 ```java //激活分布式配置服务中心 @EnableConfigServer ``` #### 配置中心客户端 ##### pom.xml ```xml org.springframework.cloud spring-cloud-starter-bus-amqp org.springframework.boot spring-boot-starter-actuator ``` ##### bootstrap.yml ```yaml #rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口 rabbitmq: host: localhost port: 5672 username: guest password: guest # 暴露监控端点 management: endpoints: web: exposure: include: "*" # 'refresh' ``` ##### 需要动态刷新的类,添加注解 ```java //可动态刷新配置 @RefreshScope ``` ## Spring Cloud Stream > 引入目的:屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型 ## Spring Cloud Sleuth ## SpringCloudAlibaba ### SpringCloud Alibaba Sentinel实现熔断与限流 ![1660372973088](README.assets/1660372973088.png) #### 基本项目结构 ```txt cloud2020 cloudalibaba-sentinel-service8401 ``` #### 1、Sentinel ![1660373016902](README.assets/1660373016902.png) ##### (1)官网 https://github.com/alibaba/Sentinel 中文翻译版 https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D ##### (2)是什么 > 一句话解释,之前我们讲解过的Hystrix ![1660373091134](README.assets/1660373091134.png) ![1660373102586](README.assets/1660373102586.png) ##### (3)去哪下 https://github.com/alibaba/Sentinel/releases ![1660373135733](README.assets/1660373135733.png) ##### (4)能干嘛 ![1660373154450](README.assets/1660373154450.png) ##### (5)怎么玩 > 官网教程 > > https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel ![1660373242878](README.assets/1660373242878.png) #### 2、安装Sentinel控制台 ![1660373332320](README.assets/1660373332320.png) ##### (1)sentinel组件由2部分构成 ![1660373361382](README.assets/1660373361382.png) ##### (2)安装步骤 i. 下载 https://github.com/alibaba/Sentinel/releases 下载到本地sentinel-dashboard-1.7.0.jar ![1660373462790](README.assets/1660373462790.png) ii. 运行命令 > java -jar sentinel-dashboard-1.7.0.jar ![1660373483831](README.assets/1660373483831.png) iii .访问sentinel管理界面 > http://localhost:8080 > > 登录账号密码均为sentinel ![1660373511163](README.assets/1660373511163.png) #### 3、初始化演示工程 ![1660374462132](README.assets/1660374462132.png) ##### (1)启动Nacos8848成功 ##### (2)Moudle 改pom ```xml 4.0.0 cloud2020 com.adun.springcloud 1.0-SNAPSHOT cloudalibaba-sentinel-service8401 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.csp sentinel-datasource-nacos com.alibaba.cloud spring-cloud-starter-alibaba-sentinel org.springframework.cloud spring-cloud-starter-openfeign org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true cn.hutool hutool-all 4.6.3 org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 写yml ```yaml server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: #Nacos服务注册中心地址 server-addr: localhost:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: localhost:8080 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719 management: endpoints: web: exposure: include: '*' ``` 主启动 ```java @EnableDiscoveryClient @SpringBootApplication public class SentinelMain8401 { public static void main(String[] args) { SpringApplication.run(SentinelMain8401.class, args); } } ``` 业务类 controller ```java @RestController public class FlowLimitController { @GetMapping("/testA") public String testA() { return "------testA"; } @GetMapping("/testB") public String testB() { return "------testB"; } } ``` ##### (3)启动Sentinel8080 ##### (4)启动微服务8401 ##### (5)启动8401微服务后查看Sentinel控制台 ![1660376828810](README.assets/1660376828810.png) 刚开始sentinel控制台空空如也 ![1660376532043](README.assets/1660376532043.png) ![1660376797189](README.assets/1660376797189.png) ![1660376807002](README.assets/1660376807002.png) #### 4、流控规则 ![1660376953247](README.assets/1660376953247.png) ##### (1)基本介绍 ![1660376980378](README.assets/1660376980378.png) 进一步解释说明 ![1660377011445](README.assets/1660377011445.png) ##### (2)流控模式 ![1660379515302](README.assets/1660379515302.png) ###### 直接(默认) 配置及说明 > 表示1秒钟内查询1次就是OK,若超过次数1,就直接-快速失败,报默认错误 ![1660379566871](README.assets/1660379566871.png) 测试结果【1s中访问多次】 ![1660379694134](README.assets/1660379694134.png) ###### 关联 ![1660379743567](README.assets/1660379743567.png) 配置A > 设置效果 > 当关联资源/testB的qps阀值超过1时,就限流/testA的Rest访问地址,当关联资源到阈值后限制配置好的资源名 ![1660379845124](README.assets/1660379845124.png) postman模拟并发密集访问testB ![1660379883377](README.assets/1660379883377.png) 访问testB成功 ![1660379934258](README.assets/1660379934258.png) postman里新建多线程集合组 ![1660379959466](README.assets/1660379959466.png) 将访问地址添加进新新线程组 ![1660379980627](README.assets/1660379980627.png) Run > 大批量线程高并发访问B,导致A失效了 ![1660379994824](README.assets/1660379994824.png) 运行testA ![1660380039337](README.assets/1660380039337.png) ###### 链路 ![1660380380616](README.assets/1660380380616.png) ##### (3)流控效果 ###### i. 直接 ![1660381054308](README.assets/1660381054308.png) ###### ii. 预热 ![1660381111868](README.assets/1660381111868.png) (1)说明 > 公式:阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值 (2)官网 https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6 ![1660381207743](README.assets/1660381207743.png) > 默认coldFactor为3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。 > 限流 冷启动 > > https://github.com/alibaba/Sentinel/wiki/%E9%99%90%E6%B5%81---%E5%86%B7%E5%90%AF%E5%8A%A8 (3)源码分析 com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController ![1660381324886](README.assets/1660381324886.png) (4)WarmUp配置 | | | ------------------------------------------------------------ | | 默认 coldFactor 为 3,即请求QPS从(threshold / 3) 开始,经多少预热时长才逐渐升至设定的 QPS 阈值。 | | 案例,阀值为10+预热时长设置5秒。 系统初始化的阀值为10 / 3 约等于3,即阀值刚开始为3;然后过了5秒后阀值才慢慢升高恢复到10 | ![1660381397734](README.assets/1660381397734.png) (5)多次点击http://localhost:8401/testB,刚开始不行,后续慢慢OK (6)应用场景 > 如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值。 ###### iii. 排队等待 ![1660382340707](README.assets/1660382340707.png) 官网 > https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6 ![1660382357935](README.assets/1660382357935.png) 源码 > com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController ```java // // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.alibaba.csp.sentinel.slots.block.flow.controller; import com.alibaba.csp.sentinel.node.Node; import com.alibaba.csp.sentinel.slots.block.flow.TrafficShapingController; import com.alibaba.csp.sentinel.util.TimeUtil; import java.util.concurrent.atomic.AtomicLong; public class RateLimiterController implements TrafficShapingController { private final int maxQueueingTimeMs; private final double count; private final AtomicLong latestPassedTime = new AtomicLong(-1L); public RateLimiterController(int timeOut, double count) { this.maxQueueingTimeMs = timeOut; this.count = count; } public boolean canPass(Node node, int acquireCount) { return this.canPass(node, acquireCount, false); } public boolean canPass(Node node, int acquireCount, boolean prioritized) { if (acquireCount <= 0) { return true; } else if (this.count <= 0.0D) { return false; } else { long currentTime = TimeUtil.currentTimeMillis(); long costTime = Math.round(1.0D * (double)acquireCount / this.count * 1000.0D); long expectedTime = costTime + this.latestPassedTime.get(); if (expectedTime <= currentTime) { this.latestPassedTime.set(currentTime); return true; } else { long waitTime = costTime + this.latestPassedTime.get() - TimeUtil.currentTimeMillis(); if (waitTime > (long)this.maxQueueingTimeMs) { return false; } else { long oldTime = this.latestPassedTime.addAndGet(costTime); try { waitTime = oldTime - TimeUtil.currentTimeMillis(); if (waitTime > (long)this.maxQueueingTimeMs) { this.latestPassedTime.addAndGet(-costTime); return false; } else { if (waitTime > 0L) { Thread.sleep(waitTime); } return true; } } catch (InterruptedException var15) { return false; } } } } } } ``` 测试 ![1660382451187](README.assets/1660382451187.png) #### 5、降级【熔断】规则 > 高版本显示熔断规则 ![1660382818254](README.assets/1660382818254.png) ##### (1)官网 https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7 ##### (2)基本介绍 ![1660382862066](README.assets/1660382862066.png) >RT(平均响应时间,秒级) > 平均响应时间 超出阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级 > 窗口期过后关闭断路器 > RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效) > >异常比列(秒级) > QPS >= 5 且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级 > >异常数(分钟级) > 异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级 进一步说明 > > Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制, > 让请求快速失败,避免影响到其它的资源而导致级联错误。 > > 当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。 Sentinel的断路器是没有半开状态的 > 半开的状态系统自动去检测是否请求有异常, > 没有异常就关闭断路器恢复使用, > 有异常则继续打开断路器不可用。具体可以参考Hystrix 复习Hystrix【状态机】 ![1660383149482](README.assets/1660383149482.png) ##### (3)降级策略实战 ![1660383217654](README.assets/1660383217654.png) ###### i. RT 是什么 ![1660383288609](README.assets/1660383288609.png) ![1660383297403](README.assets/1660383297403.png) 测试 代码 ```java @GetMapping("/testD") public String testD() { //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.info("testD 测试RT"); return "------testD"; } ``` 配置 ![1660383419274](README.assets/1660383419274.png) jmeter压测 ![1660383454245](README.assets/1660383454245.png) 结论 ![1660383498067](README.assets/1660383498067.png) > 按照上述配置, > > 永远一秒钟打进来10个线程(大于5个了)调用testD,我们希望200毫秒处理完本次任务, > 如果超过200毫秒还没处理完,在未来1秒钟的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了 > > 后续我停止jmeter,没有这么大的访问量了,断路器关闭(保险丝恢复),微服务恢复OK ###### ii. 异常比例 是什么 ![1660385347633](README.assets/1660385347633.png) ![1660385360043](README.assets/1660385360043.png) 测试 代码 ```java @GetMapping("/testD") public String testD() { //测试异常比例 log.info("testD 测试异常比例"); int age = 10/0; return "------testD"; } ``` 配置 ![1660385472378](README.assets/1660385472378.png) jmeter ![1660385497021](README.assets/1660385497021.png) 结论 > 按照上述配置, > 单独访问一次,必然来一次报错一次(int age = 10/0),调一次错一次; ![1660385530330](README.assets/1660385530330.png) > 开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件了。 > 断路器开启(保险丝跳闸),微服务不可用了,不再报错error而是服务降级了。 ###### iii. 异常数 是什么 ![1660385620978](README.assets/1660385620978.png) > 时间窗口一定要大于等于60秒。 ![1660385636053](README.assets/1660385636053.png) > 异常数是按照分钟统计的 测试 代码 ```java @GetMapping("/testE") public String testE() { log.info("testE 测试异常比例"); int age = 10/0; return "------testE 测试异常比例"; } ``` 配置 ![1660385731854](README.assets/1660385731854.png) > http://localhost:8401/testE,第一次访问绝对报错,因为除数不能为零, > 我们看到error窗口,但是达到5次报错后,进入熔断后降级。 jmeter ![1660385806159](README.assets/1660385806159.png) #### 6、热点key限流 ![1660401538672](README.assets/1660401538672.png) ##### (1)基本介绍 > 何为热点 > 热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作 ![1660401733445](README.assets/1660401733445.png) ##### (2)官网 > https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81 ##### (3)承上启下复习start ```java @SentinelResource ``` > 兜底方法 > 分为系统默认和客户自定义,两种 > > 之前的case,限流出问题后,都是用sentinel系统默认的提示:Blocked by Sentinel (flow limiting) > > > 我们能不能自定?类似hystrix,某个方法出问题了,就找对应的兜底降级方法? > > 结论 > 从HystrixCommand 到@SentinelResource ##### (4)代码 > com.alibaba.csp.sentinel.slots.block.BlockException ```java @GetMapping("/testHotKey") @SentinelResource(value = "testHotKey",blockHandler = "dealHandler_testHotKey") public String testHotKey(@RequestParam(value = "p1",required = false) String p1, @RequestParam(value = "p2",required = false) String p2){ return "------testHotKey"; } public String dealHandler_testHotKey(String p1,String p2,BlockException exception) { return "-----dealHandler_testHotKey"; } sentinel系统默认的提示:Blocked by Sentinel (flow limiting) ``` ##### (5)配置 ![1660402099193](README.assets/1660402099193.png) ![1660402119409](README.assets/1660402119409.png) > 限流模式只支持QPS模式,固定写死了。(这才叫热点) > @SentinelResource注解的方法参数索引,0代表第一个参数,1代表第二个参数,以此类推 > 单机阀值以及统计窗口时长表示在此窗口时间超过阀值就限流。 > 上面的抓图就是第一个参数有值的话,1秒的QPS为1,超过就限流,限流后调用dealHandler_testHotKey支持方法。 > > 此时限流只对一个参数起作用 ##### (6)测试 ![1660402270697](README.assets/1660402270697.png) error: http://localhost:8401/testHotKey?p1=abc error: http://localhost:8401/testHotKey?p1=abc&p2=33 right: http://localhost:8401/testHotKey?p2=abc ##### (7)参数例外项 ![1660402614350](README.assets/1660402614350.png) 特例配置 ![1660402726831](README.assets/1660402726831.png) 特殊测试 ![1660403098400](README.assets/1660403098400.png) 前提条件 > 热点参数的注意点,参数必须是基本类型或者String ##### (8)其他 > 手贱添加异常看看....../(ㄒoㄒ)/~~ > @SentinelResource > 处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理; > > RuntimeException > int age = 10/0,这个是java运行时报出的运行时异常RunTimeException,@SentinelResource不管 > > 总结 > @SentinelResource主管配置出错,运行出错该走异常走异常 #### 7、系统规则 ![1660403846108](README.assets/1660403846108.png) ##### (1)是什么 > https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81 ##### (2)各项配置参数说明 ![1660403891641](README.assets/1660403891641.png) ##### (3)配置全局QPS #### 8、@SentinelResource ![1660404036425](README.assets/1660404036425.png) ##### (1)按资源名称限流+后续处理 ![1660404105879](README.assets/1660404105879.png) ###### i. Moudle cloudalibaba-sentinel-service8401 改pom ```xml 4.0.0 cloud2020 com.adun.springcloud 1.0-SNAPSHOT cloudalibaba-sentinel-service8401 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.adun.springcloud cloud-api-commons ${project.version} com.alibaba.csp sentinel-datasource-nacos com.alibaba.cloud spring-cloud-starter-alibaba-sentinel org.springframework.cloud spring-cloud-starter-openfeign org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true cn.hutool hutool-all 4.6.3 org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 写yaml ```yaml server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 sentinel: transport: dashboard: localhost:8080 #配置Sentinel dashboard地址 port: 8719 management: endpoints: web: exposure: include: '*' ``` 主启动 ```java @EnableDiscoveryClient @SpringBootApplication public class MainApp8401 { public static void main(String[] args) { SpringApplication.run(MainApp8401.class, args); } } ``` 业务类 ```java @RestController public class RateLimitController { @GetMapping("/byResource") @SentinelResource(value = "byResource",blockHandler = "handleException") public CommonResult byResource() { return new CommonResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001")); } public CommonResult handleException(BlockException exception) { return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用"); } } ``` ###### ii. 配置流控规则 配置步骤 ![1660404479260](README.assets/1660404479260.png) 图形配置和代码关系 ![1660404531015](README.assets/1660404531015.png) ###### iii. 测试 1秒钟点击1下,OK 超过上述,疯狂点击,返回了自己定义的限流处理信息,限流发生 ```json { "code": 444, "message": "com.alibaba.csp.sentinel.slots.block.flow.FlowException\t 服务不可用", "data": null } ``` ##### (2)按照Url地址限流+后续处理 ![1660404874896](README.assets/1660404874896.png) ###### i.修改业务类RateLimitController ```java @RestController public class RateLimitController { @GetMapping("/byResource") @SentinelResource(value = "byResource",blockHandler = "handleException") public CommonResult byResource() { return new CommonResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001")); } public CommonResult handleException(BlockException exception) { return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用"); } @GetMapping("/rateLimit/byUrl") @SentinelResource(value = "byUrl") public CommonResult byUrl() { return new CommonResult(200,"按url限流测试OK",new Payment(2020L,"serial002")); } } ``` ###### ii. Sentinel控制台配置 ![1660405000574](README.assets/1660405000574.png) ###### iii. 测试 ![1660405069950](README.assets/1660405069950.png) > 会返回Sentinel自带的限流处理结果 ![1660405090204](README.assets/1660405090204.png) ##### (3)上面兜底方案面临的问题 ```txt 1 系统默认的,没有体现我们自己的业务要求。 2 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。 3 每个业务方法都添加一个兜底的,那代码膨胀加剧。 4 全局统一的处理方法没有体现。 ``` ##### (4)客户自定义限流处理逻辑 ![1660405210831](README.assets/1660405210831.png) ###### i.自定义限流处理类 ![1660405351129](README.assets/1660405351129.png) ```java public class CustomerBlockHandler { public static CommonResult handleException(BlockException exception){ return new CommonResult(2020,"自定义的限流处理信息......CustomerBlockHandler"); } } ``` ###### ii.RateLimitController ```java /** * 自定义通用的限流处理逻辑, blockHandlerClass = CustomerBlockHandler.class blockHandler = handleException2 上述配置:找CustomerBlockHandler类里的handleException2方法进行兜底处理 */ /** * 自定义通用的限流处理逻辑 */ @GetMapping("/rateLimit/customerBlockHandler") @SentinelResource(value = "customerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2") public CommonResult customerBlockHandler() { return new CommonResult(200,"按客户自定义限流处理逻辑"); } ``` ###### iii.启动微服务后先调用一次 http://localhost:8401/rateLimit/customerBlockHandler ###### iv.Sentinel控制台配置 ![1660405475629](README.assets/1660405475629.png) ###### v.进一步说明 ![1660405528518](README.assets/1660405528518.png) ##### (5)更多注解属性说明 ![1660405595126](README.assets/1660405595126.png) ![1660405620940](README.assets/1660405620940.png) > 所有的代码都要用try-catch-finally方式进行处理,o(╥﹏╥)o Sentinel主要有三个核心Api ![1660405657172](README.assets/1660405657172.png) #### 9、服务熔断功能 ![1660405836800](README.assets/1660405836800.png) ##### (1)Ribbon系列 ![1660406216589](README.assets/1660406216589.png) ###### 基本项目架构 ```txt cloud2020 cloudalibaba-provider-payment9003 cloudalibaba-provider-payment9004 cloudalibaba-consumer-nacos-order84 ``` ###### i. 提供者9003/9004 新建cloudalibaba-provider-payment9003/9004两个一样的做法 改pom ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloudalibaba-provider-payment9003 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.adun.springcloud cloud-api-commons ${project.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 写yml ```yaml server: port: 9003 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 #配置Nacos地址 management: endpoints: web: exposure: include: '*' ``` 主启动 ```java @SpringBootApplication @EnableDiscoveryClient public class PaymentMain9003 { public static void main(String[] args) { SpringApplication.run(PaymentMain9003.class, args); } } ``` 业务类 ```java @RestController public class PaymentController { @Value("${server.port}") private String serverPort; public static HashMap hashMap = new HashMap<>(); static { hashMap.put(1L,new Payment(1L,"28a8c1e3bc2742d8848569891fb42181")); hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182")); hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183")); } @GetMapping(value = "/paymentSQL/{id}") public CommonResult paymentSQL(@PathVariable("id") Long id) { Payment payment = hashMap.get(id); CommonResult result = new CommonResult(200,"from mysql,serverPort: "+serverPort,payment); return result; } } ``` 测试地址 http://localhost:9003/paymentSQL/1 ###### ii.消费者84 新建cloudalibaba-consumer-nacos-order84 写pom ```xml cloud2020 com.adun.springcloud 1.0-SNAPSHOT 4.0.0 cloudalibaba-consumer-nacos-order84 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-sentinel com.adun.springcloud cloud-api-commons ${project.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 改yml ```yml server: port: 84 spring: application: name: nacos-order-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-payment-provider ``` 主启动 ```java @SpringBootApplication @EnableDiscoveryClient public class ConsumerNacosOrderMain84 { public static void main(String[] args) { SpringApplication.run(ConsumerNacosOrderMain84.class, args); } } ``` 业务类 ApplicationContextConfig ```java @Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } } ``` CircleBreakerController ![1660466889909](README.assets/1660466889909.png) [1]只配置fallback > fallback负责业务异常 > > @SentinelResource(value = "fallback",fallback = "handlerFallback") > > //fallback负责业务异常 编码 ```java @RestController @Slf4j public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @RequestMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback负责业务异常 public CommonResult fallback(@PathVariable Long id) { CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id); if (id == 4) { throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常...."); }else if (result.getData() == null) { throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常"); } return result; } public CommonResult handlerFallback(@PathVariable Long id,Throwable e) { Payment payment = new Payment(id,"null"); return new CommonResult<>(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment); } } ``` 图说 ![1660468266841](README.assets/1660468266841.png) 结果 ![1660468291524](README.assets/1660468291524.png) [2]只配置blockHandler > blockHandler负责在sentinel里面配置的降级限流【熔断】 > > @SentinelResource(value = "fallback",blockHandler = "blockHandler") > > //blockHandler负责在sentinel里面配置的降级限流 > 编码 ```java @RestController @Slf4j public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @RequestMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler负责在sentinel里面配置的降级限流 public CommonResult fallback(@PathVariable Long id) { CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id); if (id == 4) { throw new IllegalArgumentException ("非法参数异常...."); }else if (result.getData() == null) { throw new NullPointerException ("NullPointerException,该ID没有对应记录"); } return result; } public CommonResult handlerFallback(@PathVariable Long id,Throwable e) { Payment payment = new Payment(id,"null"); return new CommonResult<>(444,"fallback,无此流水,exception "+e.getMessage(),payment); } public CommonResult blockHandler(@PathVariable Long id,BlockException blockException) { Payment payment = new Payment(id,"null"); return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment); } } ``` 图说 ![1660468410702](README.assets/1660468410702.png) 本例sentinel需配置 > 异常超过2次后,断路器打开,断电跳闸,系统被保护 ![1660468432340](README.assets/1660468432340.png) 结果 ![1660468453960](README.assets/1660468453960.png) [3]fallback和blockHandler都配置 > 若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。 > > @SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler") > > //若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。 > 编码 ```java @RestController @Slf4j public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @RequestMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler") public CommonResult fallback(@PathVariable Long id) { CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id); if (id == 4) { throw new IllegalArgumentException ("非法参数异常...."); }else if (result.getData() == null) { throw new NullPointerException ("NullPointerException,该ID没有对应记录"); } return result; } public CommonResult handlerFallback(@PathVariable Long id,Throwable e) { Payment payment = new Payment(id,"null"); return new CommonResult<>(444,"fallback,无此流水,exception "+e.getMessage(),payment); } public CommonResult blockHandler(@PathVariable Long id,BlockException blockException) { Payment payment = new Payment(id,"null"); return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment); } } ``` 图说 ![1660468575606](README.assets/1660468575606.png) 本例sentinel需配置 ![1660468589536](README.assets/1660468589536.png) 结果 ![1660468601298](README.assets/1660468601298.png) ```java 若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。 ``` [4]忽略效果 >fallback不再兜底,没有降级 > > @SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler",exceptionsToIgnore = {IllegalArgumentException.class}) > > //fallback不再兜底,没有降级 > > 编码 ```java @RestController @Slf4j public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @RequestMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler", exceptionsToIgnore = {IllegalArgumentException.class}) public CommonResult fallback(@PathVariable Long id) { CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id); if (id == 4) { throw new IllegalArgumentException ("非法参数异常...."); }else if (result.getData() == null) { throw new NullPointerException ("NullPointerException,该ID没有对应记录"); } return result; } public CommonResult handlerFallback(@PathVariable Long id,Throwable e) { Payment payment = new Payment(id,"null"); return new CommonResult<>(444,"fallback,无此流水,exception "+e.getMessage(),payment); } public CommonResult blockHandler(@PathVariable Long id,BlockException blockException) { Payment payment = new Payment(id,"null"); return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment); } } ``` 图说 ![1660468682988](README.assets/1660468682988.png) 结果 ![1660468698968](README.assets/1660468698968.png) ##### (2)Feign系列 ![1660470265072](README.assets/1660470265072.png) ###### 基本项目架构 ```txt cloud2020 cloudalibaba-consumer-nacos-order84 cloudalibaba-provider-payment9003 ``` ###### 修改84模块 写pom ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` 改yml ```yml server: port: 84 spring: application: name: nacos-order-consumer cloud: nacos: discovery: #Nacos服务注册中心地址 server-addr: localhost:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: localhost:8080 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719 management: endpoints: web: exposure: include: '*' # 激活Sentinel对Feign的支持 feign: sentinel: enabled: true ``` 主启动 > 添加@EnableFeignClients启动Feign的功能 ```java @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class ConsumerNacosOrderMain84 { public static void main(String[] args) { SpringApplication.run(ConsumerNacosOrderMain84.class, args); } } ``` 业务类 ![1660470471726](README.assets/1660470471726.png) 带@FeignClient注解的业务接口 ```java /** * @auther ADun * @create 2019-12-10 17:17 * 使用 fallback 方式是无法获取异常信息的, * 如果想要获取异常信息,可以使用 fallbackFactory参数 */ @FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)//调用中关闭9003服务提供者 public interface PaymentService { @GetMapping(value = "/paymentSQL/{id}") public CommonResult paymentSQL(@PathVariable("id") Long id); } ``` fallback = PaymentFallbackService.class ```java @Component public class PaymentFallbackService implements PaymentService { @Override public CommonResult paymentSQL(Long id) { return new CommonResult<>(444,"服务降级返回,没有该流水信息",new Payment(id, "errorSerial......")); } } ``` Controller ```java //==================OpenFeign @Resource private PaymentService paymentService; @GetMapping(value = "/consumer/openfeign/{id}") public CommonResult paymentSQL(@PathVariable("id") Long id) { if(id == 4) { throw new RuntimeException("没有该id"); } return paymentService.paymentSQL(id); } ``` ##### (3)熔断框架比较 ![1660470619074](README.assets/1660470619074.png) #### 10、规则持久化 ![1660470736300](README.assets/1660470736300.png) ###### 基本项目结构 ```txt cloud2020 cloudalibaba-sentinel-service8401 ``` ###### (1)步骤 ![1660470777799](README.assets/1660470777799.png) 修改cloudalibaba-sentinel-service8401 改pom ```xml com.alibaba.csp sentinel-datasource-nacos ``` 写yml 添加Nacos数据源配置 ```yml spring: cloud: sentinel: datasource: ds1: nacos: server-addr: localhost:8848 dataId: ${spring.application.name} groupId: DEFAULT_GROUP data-type: json rule-type: flow ``` ```yml server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 sentinel: transport: dashboard: localhost:8080 #配置Sentinel dashboard地址 port: 8719 datasource: ds1: nacos: server-addr: localhost:8848 dataId: cloudalibaba-sentinel-service groupId: DEFAULT_GROUP data-type: json rule-type: flow management: endpoints: web: exposure: include: '*' feign: sentinel: enabled: true # 激活Sentinel对Feign的支持 ``` 添加Nacos业务规则配置 ![1660470912020](README.assets/1660470912020.png) 内容解析 ```json [ { "resource": "/rateLimit/byUrl", "limitApp": "default", "grade": 1, "count": 1, "strategy": 0, "controlBehavior": 0, "clusterMode": false } ] resource:资源名称; limitApp:来源应用; grade:阈值类型,0表示线程数,1表示QPS; count:单机阈值; ``` 启动8401后刷新sentinel发现业务规则有了 ![1660471440147](README.assets/1660471440147.png) ![1660471465459](README.assets/1660471465459.png) 停止8401再看 ![1660471489174](README.assets/1660471489174.png)