10. Hystrix断路器
- 虽然现在Hystrix服务降级框架SpringCloud官方停止更新了,但是它的设计理念非常优秀,服务降级 服务熔断 服务限流等等它的一些列思想是后面框架借鉴的必备良药。所以我们需要深入地了解一下Hystrix。
- 虽然现在官网推荐使用resilience4j,但是它在国外用的比较多。
- 在国内主要是使用Hystrix,或者sentienl(阿里的)
- 消费侧服务侧都可以使用,一般在消费侧使用。
10.1 概述
10.1.1 分布式系统面临的问题
-
分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
-
服务雪崩
- 多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的
“扇出”
。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”. - 对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
- 多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的
-
eg:80服务调用了8001,8001调用8002,8002调用8004,8004调用8006,这样挨个调用使得链路越来越长,只要其中一个出事了就会导致所有的服务出现问题。
10.1.2 是什么
- Hystrix是一个用于处理分布式系统的
延迟
和容错
的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性
。 - “断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),
向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常
,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
10.1.3 能干嘛
- 服务降级
- 服务熔断
- 接近实时的监控
- …
10.1.4 官网资料
- 官网资料:https://github.com/Netflix/Hystrix/wiki/How-To-Use
10.1.5 Hystrix官宣,停更进维
- https://github.com/Netflix/Hystrix
- 被动修复bugs
- 不再接受合并请求
- 不再发布新版本
10.2 Hystrix重要概念
10.2.1 服务降级fallback
- 服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback
- 即:类似于if-else结构,对方系统不可用了,你需要给我一个兜底的解决方法
- 哪些情况会出发降级
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
10.2.2 服务熔断break
- 类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
- 就是保险丝:服务的降级->进而熔断->恢复调用链路
- 服务熔断也可以看成是降级的一种
10.2.3 服务限流flowlimit
- 秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
10.3 hystrix案例
- 因为启动多个服务比较麻烦,所以改为单机版进行测试
- 7001恢复为单机版
- 7001恢复为单机版
10.3.1 构建
1)新建cloud-provider-hystrix-payment8001
- 创建一个带熔断hystrix的8001
2)POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-provider-hystrix-payment8001</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3)YML
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
4)主启动
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient
@SpringBootApplication
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
5)业务类
- service:正常情况下需要写一个接口和接口实现类,为了节约时间直接写实现类了。
package com.angenin.springcloud.service;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class PaymentService {
//正常访问方法
public String paymentInfo_OK(Integer id){
//如果正常访问则,返回当前线程池的名字、传入的id、表情包
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O哈哈~";
}
//超时访问方法
public String paymentInfo_TimeOut(Integer id){
//前面学过时会导致服务降级,模拟错误
int timeNumber = 3;
try {
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
//超时返回的提示信息
return "线程池:" + Thread.currentThread().getName() +
"paymentInfo_TimeOut,id:" +id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒):"+timeNumber;
}
}
- controller
package com.angenin.springcloud.controller;
import com.angenin.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Slf4j
@RestController
public class PaymentController {
@Resource
PaymentService paymentService;
@Value("${server.port}") //spring的@Value注解
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){
String result = paymentService.paymentInfo_TimeOut(id);
log.info("******result:" + result);
return result;
}
}
6)正常测试
-
启动eureka7001
-
启动cloud-provider-hystrix-payment8001
-
访问:都能正确访问并返回结果。
-
success的方法:http://localhost:8001/payment/hystrix/ok/4
-
每次调用耗费3秒钟:http://localhost:8001/payment/hystrix/timeout/4
-
-
上述module均OK:以上述为根基平台,从正确->错误->降级熔断->恢复
10.3.2 高并发测试
- 上述在非高并发情形下,还能勉强满足 but…
- JMeter安装及环境配置
1)Jmeter压测测试
-
开启Jmeter,来2万个并发压死8001,20000个请求都去访问paymentInfo_TimeOut服务
保存:
http://localhost:8001/payment/hystrix/timeout/4
-
启动eureka7001
-
启动cloud-provider-hystrix-payment8001
-
分别访问2个方法,查看演示结果(自测)
- success的方法:http://localhost:8001/payment/hystrix/ok/4
- 每次调用耗费3秒钟:http://localhost:8001/payment/hystrix/timeout/4
-
结果:
- 两个都在自己转圈圈
- 原来:访问成功的方法立刻返回结果,访问耗时方法3秒钟后返回结果(线程量少,只有2个线程)
- 现在:2个方法都会转圈圈,说明这个成功的方法也被拖累变慢了(线程多,2万+2个线程)
- 为什么会被卡死
- tomcat的默认的工作线程数被打满 了,没有多余的线程来分解压力和处理。(Springboot默认集成的是tomact,里面有一个tomact容器的线程池)
- 访问同一个微服务的2个方法,微服务集中资源处理高并发的请求,由于重压之下将资源抽空了,所以会导致访问成功的方法也会出现卡顿 延迟的效果。
- 两个都在自己转圈圈
2)Jmeter压测结论
- 上面还是服务
提供者8001自己测试
,假如此时外部的消费者80也来访问,那消费者
只能干等,最终导致消费端80不满意,服务端8001直接被拖死
3)看热闹不嫌弃事大,80新建加入
-
新建cloud-consumer-feign-hystrix-order80
-
POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-hystrix-order80</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般基础通用配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
- YML
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
- 主启动
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients //激活feifn
public class OrderHystrixMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
- 业务类(PaymentHystrixService、OrderHystirxController)
package com.angenin.springcloud.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Component
//调用的微服务名
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" )
public interface PaymentHystrixService
{
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
-----------------------------------------------------------
package com.angenin.springcloud.controller;
import com.angenin.springcloud.service.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderHystirxController {
@Resource
private PaymentHystrixService paymentHystrixService;
//调用ok方法
@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;
}
}
-
正常测试
-
启动7001,8001,80
-
访问ok:http://localhost/consumer/payment/hystrix/ok/4(速度同样很快)
-
-
高并发测试
- 2W个线程压8001
- 消费端80微服务再去访问正常的Ok微服务8001地址
- http://localhost/consumer/payment/hystrix/ok/32
- 效果:
- 要么转圈圈等待,过一段时间才能返回结果。
- 要么消费端报超时错误
10.3.3 故障现象和导致原因
- 8001同一层次的其它接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完毕
- 80此时调用8001,客户端访问响应缓慢,转圈圈
10.3.4 上诉结论
- 正因为有上述故障或不佳表现,才有我们的降级/容错/限流等技术诞生
10.3.5 如何解决?解决的要求
- 超时导致服务器变慢(转圈):超时不再等待
- 出错(宕机或程序运行出错):出错要有兜底
- 解决:
- 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级
- 对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级
- 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级
10.3.6 服务降级
1)降级配置
- @HystrixCommand
- 现在基本上都是实用配置代替编码
2)8001先从自身找问题
- 设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作服务降级fallback
3)8001fallback
生产者降级:超时方法默认峰值为3秒,现在睡眠了5秒所以要进行降级,执行兜底的方法。
- 业务类启用
- @HystrixCommand报异常后如何处理
- 一旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法
package com.angenin.springcloud.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class PaymentService {
//正常访问方法
public String paymentInfo_OK(Integer id){
//如果正常访问则,返回当前线程池的名字、传入的id、表情包
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O哈哈~";
}
/**
* @HystrixCommand:启用
* 超时访问方法
* fallbackMethod:此方法出现问题了,执行哪个兜底的方法
* HystrixProperty:此方法线程的超时时间为3秒钟,我们现在睡眠5秒说明超时了,就走兜底的方法
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
})
public String paymentInfo_TimeOut(Integer id){
//前面学过时会导致服务降级,模拟错误
int timeNumber = 5;
//int age = 10/0; //程序出现异常,同样会走兜底的方法
try {
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
//超时返回的提示信息
return "线程池:" + Thread.currentThread().getName() +
"paymentInfo_TimeOut,id:" +id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒):"+timeNumber;
}
// 兜底方法
public String paymentInfo_TimeOutHandler(Integer id){ // 回调函数向调用方返回一个符合预期的、可处理的备选响应
return "线程池: "+Thread.currentThread().getName()+" 8001系统繁忙或者运行报错,请稍后再试,id: "+id+"\t"+"o(╥﹏╥)o";
}
}
- 主启动类激活
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient
@SpringBootApplication
@EnableCircuitBreaker //激活
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
-
自测:启动7001,8001
-
http://localhost:8001/payment/hystrix/timeout/1
4)80fallback
消费者降级:生产者的超时峰值为5秒睡眠了3秒,现在消费者调用要求的时间为1.5秒等不了3秒,所以服务降级调用兜底的方法。
- 80订单微服务,也可以更好的保护自己,自己也依样画葫芦进行客户端降级保护
- 题外话,切记:我们自己配置过的热部署方式对java代码的改动明显,但对
@HystrixCommand
内属性的修改建议重启微服务 - YML
feign:
hystrix:
enabled: true
- 主启动
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients //激活feifn
@EnableHystrix //激活
public class OrderHystrixMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
- 业务类
package com.angenin.springcloud.controller;
import com.angenin.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderHystirxController {
@Resource
private PaymentHystrixService paymentHystrixService;
//调用ok方法
@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) {
int age = 10/0; //测试出现异常同样要执行兜底的方法
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
}
-
把生产者端修改正常:生产者方峰值为5秒,睡眠3秒,此时消费方峰值为1.5秒等不了3秒,所以执行兜底方法。
-
测试:启动7001,8001,80
-
http://localhost/consumer/payment/hystrix/timeout/1
5)目前问题
-
问题1:每个业务方法对应一个兜底的方法,代码膨胀
-
问题2:业务逻辑方法和兜底的方法混在了一块,代码的耦合度极高。
-
解决:统一和自定义的分开
- 100个业务方法,其中97个普通的方法使用全局配置,只有3个特殊的走定制的(写3个兜底方法),这样就可以减轻fallback服务降级方法的生成。
6)解决问题演示
解决问题1:每个方法配置一个???膨胀
-
feign接口系列
-
@DefaultProperties(defaultFallback = “”)
- 1:1 每个方法配置一个服务降级方法,技术上可以,实际上傻X
- 1:N 除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback = “”) 统一跳转到统一处理结果页面
通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量,O(∩_∩)O哈哈~
-
controller配置
package com.angenin.springcloud.controller;
import com.angenin.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
/**
* 没有配置过定制的@HystrixCommand(fallbackMethod)方法就走这个全局定义的@DefaultProperties(defaultFallback)方法,
* 如果配置了就走自定义的@HystrixCommand(fallbackMethod)兜底方法。
*/
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystirxController {
@Resource
private PaymentHystrixService paymentHystrixService;
//调用ok方法
@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")
})*/
/**
* 注意:需要注释的是这个@HystrixCommand注解中的属性而不是整个注解
* 注释掉注解:代表不使用服务降级,正确就正确,错误就错误
* 使用@HystrixProperty注解:不指定fallbackMethod属性表示使用全局的,指定代表使用定制的。
*/
@HystrixCommand
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
int age = 10/0; //测试出现异常同样要执行兜底的方法
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
//下面是全局fallback兜底方法
public String payment_Global_FallbackMethod() {
return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
}
}
- 重启:7001,8001,80测试
- 效果:
http://localhost/consumer/payment/hystrix/timeout/1
解决问题2:和业务逻辑混一起???混乱
- 思路:消费者只要是使用Feign调用就一定有一个服务接口,那么把这个接口中的全部方法统一的来进行定义和调度,这样就达到了解耦的目的。
- 服务降级,客户端去调用服务端,碰上服务端宕机或关闭(前面已经测试过超时和运行时异常)
- 本次案例服务降级处理是
在客户端80实现完成的
,与服务端8001没有关系只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦 - 未来我们要面对的异常
- 运行
- 超时
- 宕机
- 再看我们的业务类PaymentController
- 修改cloud-consumer-feign-hystrix-order80
- 根据cloud-consumer-feign-hystrix-order80已经有的PaymentHystrixService接口,重新新建一个类(PaymentFallbackService)实现该接口,统一为接口里面的方法进行异常处理
- PaymentFallbackService类实现PaymentFeignClientService接口
package com.angenin.springcloud.service;
import org.springframework.stereotype.Component;
@Component
public class PaymentFallbackService implements PaymentHystrixService{
@Override
public String paymentInfo_OK(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";
}
}
- YML(前面已经设置过)
- PaymentFeignClientService接口
package com.angenin.springcloud.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Component
/**
* value:调用的微服务名
* 过程:fallback:相当于去找CLOUD-PROVIDER-HYSTRIX-PAYMENT这个微服务的名字,去调用下面已有的方法,
* 假如出事了去调用PaymentFallbackService里面的方法
*/
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class )
public interface PaymentHystrixService
{
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
- 测试
- 单个eureka先启动7001
- PaymentHystrixMain8001启动,OrderHystrixMain80
- 正常访问测试:
http://localhost/consumer/payment/hystrix/ok/31
- 故意关闭微服务8001
- 客户端自己调用提示:此时服务端provider已经down了,但是我们做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器
10.3.7 服务熔断
1)理论
-
断路器:一句话就是家里的保险丝
-
熔断机制概述
- 熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,
会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。 - 当检测到该节点微服务调用响应正常后,恢复调用链路。
- 在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,
当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解还是@HystrixCommand。 - 大神论文:https://martinfowler.com/bliki/CircuitBreaker.html
- 熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,
2)实操
修改
cloud-provider-hystrix-payment8001
- PaymentService
//=========服务熔断
/**
* commandProperties中配置的4个注解的含义:
* true使用断路器,假设在时间窗口期10秒钟内,10次请求有
* 超过60%都是失败的,那么这个断路器将起作用。
*
*/
@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) {
/* 业务逻辑:
* 如果输入的id大于等于0,则输出流水号,如果输入的id是个负数抛出异常,
* 那么根据上面注解配置的熔断机制执行降级的兜底方法paymentCircuitBreaker_fallback
* */
if(id<0){
throw new RuntimeException("******id 不能负数");
}
/**
* IdUtil.simpleUUID()类似于 UUID.randomUUID().toString().replaceAll("-", "")
* 它来自于之前在父项目中引入的hutool依赖
* hutool是个功能强大的JAVA工具包(中国人编写的),官网:https://hutool.cn/
*/
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
//降级的兜底方法
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id;
}
- why配置这些参数,查看官网:
https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker
- PaymentController
//=========服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result: "+result);
return result;
}
测试:
-
启动:cloud-eureka-server7001、cloud-provider-hystrix-payment8001
-
自测cloud-provider-hystrix-payment8001
-
正确:http://localhost:8001/payment/circuit/31
-
错误:http://localhost:8001/payment/circuit/-31
-
一次正确一次错误trytry
-
重点测试:多次错误,然后慢慢正确,发现刚开始不满足条件,就算是正确的访问地址也不能进行
- 解释:多次输入负数id返回错误页面(设置的是超过60%会熔断断路器),此时即使在输入正数id返回的仍然是错误页面,之后多次输入正数id,正确率上升了慢慢恢复为正确页面。
- 多次输入负数id返回错误页面
- 此时即使输入正数id返回的仍是错误页面
- 之后在多次输入正数id,正确率提高错误率降低,慢慢的恢复为正确页面。
3)原理(小总结)
-
大神结论
-
熔断类型
- 熔断打开
- 请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
- 熔断关闭
- 熔断关闭不会对服务进行熔断
- 熔断半开
- 部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
- 熔断打开
-
官网断路器流程图
-
官网步骤
-
断路器在什么情况下开始起作用
涉及到断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值
。- 1):快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
- 2):请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
- 3):错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
-
断路器开启或者关闭的条件
- 当满足一定的阀值的时候(默认10秒内超过20个请求次数)
- 当失败率达到一定的时候(默认10秒内超过50%的请求失败)
- 到达以上阀值,断路器将会开启
- 当开启的时候,所有请求都不会进行转发
- 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。重复4和5
-
断路器打开之后
- 再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
- 原来的主逻辑要如何恢复呢?
对于这一问题,hystrix也为我们实现了自动恢复功能。
当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
-
All配置:如下
-
//========================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";
}
10.3.8 服务限流
- 后面高级篇讲解alibaba的Sentinel说明
10.4 hystrix工作流程
-
https://github.com/Netflix/Hystrix/wiki/How-it-Works
-
Hystrix工作流程
-
官网图例
-
步骤说明
-
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()方法将引起命令失败的异常发送给调用者。
-
-
10.5 服务监控hystrixDashboard
10.5.1 概述
- 除了隔离依赖服务的调用以外,Hystrix还提供了
准实时的调用监控(Hystrix Dashboard)
,Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。- 即:hystrix的仪表盘监控界面的图形化展示
10.5.2 仪表盘9001
1)新建cloud-consumer-hystrix-dashboard9001
2)POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-hystrix-dashboard9001</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3)YML
server:
port: 9001
4)主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableHystrixDashboard //启用Hystrix仪表盘
public class HystrixDashboardMain9001
{
public static void main(String[] args)
{
SpringApplication.run(HystrixDashboardMain9001.class,args);
}
}
5)查看所有的生产者pom文件
- 所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置
6)测试
- 启动cloud-consumer-hystrix-dashboard9001该微服务后续将监控微服务8001
- http://localhost:9001/hystrix,出现此界面说明hystrixDashboard监控平台已经搭建好
10.5.3 断路器演示(服务监控hystrixDashboard)
-
演示:以之前的案例8001服务熔断为例,查看9001监控8001会产生哪些图标。
-
它需要自己搭建一个9001的监控测试平台比较麻烦,Hystrix被国内阿里的sentienl替代后直接可以登陆一个网站进行监控。
1)修改cloud-provider-hystrix-payment8001
注意事项:
- 被监控的生产者服务一般需要添加2个依赖图形化监控才能生效。
- 新版本Hystrix需要在主启动类MainAppHystrix8001中指定监控路径,否则会报Unable to connect to Command Metric Stream 404
package com.angenin.springcloud;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
@EnableEurekaClient
@SpringBootApplication
@EnableCircuitBreaker //激活
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.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;
}
}
2)监控测试
启动1个eureka或者3个eureka集群均可
- 启动cloud-eureka-server7001,cloud-provider-hystrix-payment8001,cloud-consumer-hystrix-dashboard9001
观察监控窗口
-
9001监控8001
- 填写监控地址:http://localhost:8001/hystrix.stream
- 填写监控地址:http://localhost:8001/hystrix.stream
-
测试地址(这是上面8001测试服务熔断的2个地址,10.3.7目录)
-
http://localhost:8001/payment/circuit/31 (输入正数id,返回正确页面)
-
http://localhost:8001/payment/circuit/-31 输入负数id,返回错误页面)
-
上述测试通过
-
ok
-
先访问正确地址,再访问错误地址,再正确地址,会发现图示断路器都是慢慢放开的。
-
监控结果,成功
-
监控结果,失败
-
-
-
如何看?
-
7色
-
1圈
- 实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现
故障实例和高压力实例
。
- 实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现
-
1线
- 曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。
- 曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。
-
整图说明
-
整图说明2
-
-
搞懂一个才能看懂复杂的
11. zuul路由网关
- zuul已经过时了所以这里不再讲解,直接开讲新一代网关Gateway,如果公司老项目用的zuul技术,笔记查看脑图。
12. Gateway新一代网关
12.1 概述简介
-
为什么需要网关
-
网关的技术实现
-
官网
-
上一代zuul 1.X:https://github.com/Netflix/zuul/wiki
-
当前gateway:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/
-
-
是什么
-
概述
-
一句话:
- SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。
- 源码架构
-
-
能干嘛
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
- …
-
微服务架构中网关在哪里
-
有Zuul了怎么又出来了gateway
-
我们为什么选择Gateway?
-
neflix不太靠谱,zuul2.0一直跳票,迟迟不发布 。
-
SpringCloud Gateway具有如下特性:
-
SpringCloud Gateway 与 Zuul的区别
-
-
Zuul1.x模型
-
GateWay模型
-
WebFlux是什么
-
https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-new-framework
-
说明:传统的Web框架,比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的。但是
在Servlet3.1之后有了异步非阻塞的支持
。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使用java8)Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖Servlet API,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。
-
-
12.2 三大核心概念
-
Route(路由)
- 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
-
Predicate(断言)
- 参考的是Java8的java.util.function.Predicate
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
- 参考的是Java8的java.util.function.Predicate
-
Filter(过滤)
- 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
-
总体:
- web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件; - 而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
- web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
12.3 Gateway工作流程
-
官网总结
-
核心逻辑:
路由转发+执行过滤器链
12.4 入门配置
12.4.1 Gateway9527搭建
1)新建Module
- cloud-gateway-gateway9527
2)POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-gateway-gateway9527</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.angenin.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--一般基础配置类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3)YML
- gateWay网关作为一种微服务,也要注册进服务中心。哪个注册中心都可以,如zk
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka #以单机版为例,不然集群启动太慢
4)无 业务类
- 网关没有业务类,他就是前面看大门的。
5)主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class, args);
}
}
6)9527网关如何做路由映射那???
-
以 cloud-provider-payment8001为例
-
cloud-provider-payment8001看看controller的访问地址
-
get方法:测试查询功能
-
lib:测试自定义负载均衡的规则
-
-
我们目前不想暴露8001端口,希望在8001外面套一层9527,这样比较安全,别人不能直接攻击8001外面有一层网关阻挡。
7)YML新增网关配置
server:
port: 9527
spring:
application:
name: cloud-gateway
#网关配置:
cloud:
gateway:
routes: #routes表示可以路由多个,可以为某个controller里面的所有rest接口都可以做路由
#第一个路由
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
#第二个路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka #以单机版为例,不然集群启动太慢
8)测试
-
启动cloud-eureka-server7001,cloud-provider-payment8001,cloud-gateway-gateway9527
-
引入jar包的小bug,gateway网关不需要引入web的相关依赖,如果引入启动会报错
-
访问说明
-
添加网关前:http://localhost:8001/payment/get/4
-
添加网关后:http://localhost:9527/payment/get/4,也能正确访问
-
9)Gateway配置路由的2种方式‘
方式一:在配置文件yml中配置,见前面的步骤
方式二:代码中注入RouteLocator的Bean
- 官网案例:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#spring-cloud-circuitbreaker-filter-factory
-
百度国内新闻网址,需要外网:http://news.baidu.com/guonei(现在好像没有这个网址了)
-
自己写一个
-
百度新闻
-
业务需求:通过9527网关访问到外网的百度新闻网址
-
编码
- cloud-gateway-gateway9527
- 业务实现,config,配置类内容详见下面。
-
测试:
- 重启9527项目
- http://localhost:9527/guonei,返回相同的新闻页面
-
配置类内容:
package com.angenin.springcloud.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GateWayConfig {
/**
* 配置了一个id为route-name的路由规则,
* 当访问地址 http://localhost:9527/guonei时会自动转发到地址:http://news.baidu.com/guonei
* @param
* @return
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder)
{
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route_atguigu",
r -> r.path("/guonei")
.uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
}
12.5 通过微服务名实现动态路由
-
当前存在的问题:地址写死了,服务的提供者可能有多个所以需要进行负载均衡
-
架构
-
默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建
动态路由进行转发,从而实现动态路由的功能
-
启动:一个eureka7001(cloud-eureka-server7001)+ 两个服务提供者8001/8002(cloud-provider-payment8001,cloud-provider-payment8002)
-
POM:之前已经添加过此依赖,把服务注册到注册中心
-
YML
- 需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能。
- lb://serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri
server:
port: 9527
spring:
application:
name: cloud-gateway
#网关配置:
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes: #routes表示可以路由多个,可以为某个controller里面的所有rest接口都可以做路由
#第一个路由
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
uri: lb://cloud-payment-service #匹配后提供服务的路由地址(8001 8002集群组成的微服务名称) 替换为动态的
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
#第二个路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
uri: lb://cloud-payment-service #匹配后提供服务的路由地址 替换为动态的
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka #以单机版为例,不然集群启动太慢
- 测试
- 在启动9527
- http://localhost:9527/payment/lb
- 8001/8002两个端口切换
12.6 Predicate的使用
12.6.1 是什么
- 启动我们的gateway9527
- 即:predicates下的多种匹配规则
12.6.2 Route Predicate Factories这个是什么东东?
-
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gateway-request-predicates-factories
-
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多个Route Predicate工厂可以进行组合
-
Spring Cloud Gateway 创建 Route 对象时, 使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。 Spring Cloud Gateway 包含许多内置的Route Predicate Factories。
-
所有这些谓词都匹配HTTP请求的不同属性。多种谓词工厂可以组合,并通过逻辑and。
12.6.3 常用的Route Predicate
- After Route Predicate
- Before Route Predicate
- Between Route Predicate
- Cookie Route Predicate
- Header Route Predicate
- Host Route Predicate
- Method Route Predicate
- Path Route Predicate
- Query Route Predicate
12.6.4 演示常用的Route Predicate
1)After Route Predicate
官网案例:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
问题:使用直接复制这行配置即可,但是自己如何写出想要的这个时间格式呢,它的时区又怎么写呢???
自己测试:
- 测试类:目的是获得After后面写的日期时间格式和时区
import java.time.ZonedDateTime;
public class T2 {
public static void main(String[] args) {
//获取默认时区的当前时间
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
//2023-08-27T19:32:02.975+08:00[Asia/Shanghai]
System.out.println(zbj);
// 用指定时区获取当前时间(美国/纽约)
//ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York"));
//System.out.println(zny);
}
}
- yml:之后把这个时间格式复制到yml配置文件中,表示在指定的时间之后lb这个地址才生效,2个条件结合使用。
server:
port: 9527
spring:
application:
name: cloud-gateway
#网关配置:
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes: #routes表示可以路由多个,可以为某个controller里面的所有rest接口都可以做路由
#第一个路由
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
uri: lb://cloud-payment-service #匹配后提供服务的路由地址(8001 8002集群组成的微服务名称) 替换为动态的
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
#第二个路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
uri: lb://cloud-payment-service #匹配后提供服务的路由地址 替换为动态的
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
- After=2023-08-27T19:32:02.975+08:00[Asia/Shanghai] #这个匹配的请求要在指定的时间之后,路由地址lb才生效(亚洲/上海)
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka #以单机版为例,不然集群启动太慢
- 测试:
- 配置的当前时间,那么在测试的时候这个当前时间肯定已经过了,所以生效,重启9527访问成功,8001/8002两个端口切换。
http://localhost:9527/payment/lb
- 把这个时间修改到还没有到的时间:比如当前时间是2023.8 27.19:52,修改为2023.8 27.20:52,此时时间没有到达所以不会生效,访问错误
http://localhost:9527/payment/lb
- 配置的当前时间,那么在测试的时候这个当前时间肯定已经过了,所以生效,重启9527访问成功,8001/8002两个端口切换。
2)Before Route Predicate
- 用法同1)After Route Predicate
- predicates下面写了2项,表示在指定的时间之前这个lb地址才生效,它们2个是结合使用的。
# 在指定的时间之前生效
- Before=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] # 断言,路径相匹配的进行路由
3)Between Route Predicate
- 用法同1)After Route Predicate
- 2项结合使用
# 在指定的2个时间内生效
- Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai]
4)Cookie Route Predicate
- 官网案例:
Cookie Route Predicate需要两个参数,一个是 Cookie name ,一个是正则表达式。
路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=chocolate, ch.p
自己测试:
- 模拟发送get和post请求的方式???
- jmeter工具
- postman工具(图形化工具)
- curl命令(图形化工具底层使用的命令)
- 本次案例使用curl命令来进行微服务的调试。
- yml文件:表示在指定时间后,满足cookie的格式,这个lb路径才能生效
#第二个路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
uri: lb://cloud-payment-service #匹配后提供服务的路由地址 替换为动态的
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
- After=2023-08-27T19:32:02.975+08:00[Asia/Shanghai] #这个匹配的请求要在指定的时间之后,路由地址lb才生效(亚洲/上海)
#- Before=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] #在指定时间之前生效
#- Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai] #在指定的2个时间之内生效
- Cookie=username,zzyy #满足cookie名和正则表达式格式
-
不带cookies访问,发送get请求:
curl http://localhost:9527/payment/lb
- 此时不满足cookies所以不生效。
- 此时不满足cookies所以不生效。
-
带上cookies访问:
curl http://localhost:9527/payment/lb --cookie "username=zzyy"
- 此时满足指定时间之后,符合cookie格式,所以lb路径生效,效果8001和8002交替出现
- 如果加入curl返回中文乱码,查看此地址的博客解决
https://blog.csdn.net/leedee/article/details/82685636
- 此时满足指定时间之后,符合cookie格式,所以lb路径生效,效果8001和8002交替出现
5)Header Route Predicate
- 官网案例
两个参数:一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+
自己测试
- yml文件:满足请求头的匹配规则,lb才生效(看清那些注释掉了,那些没有注释)
#第二个路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
uri: lb://cloud-payment-service #匹配后提供服务的路由地址 替换为动态的
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
#- After=2023-08-27T19:32:02.975+08:00[Asia/Shanghai] #这个匹配的请求要在指定的时间之后,路由地址lb才生效(亚洲/上海)
#- Before=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] #在指定时间之前生效
#- Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai] #在指定的2个时间之内生效
#- Cookie=username,zzyy #满足cookie名和正则表达式格式
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
- curl测试:
curl http://localhost:9527/payment/lb -H "X-Request-Id:123"
6)Host Route Predicate
- 官网文档
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。
它通过参数中的主机地址作为匹配规则。
测试
- yml
- Host=**.atguigu.com #根据主机地址进行匹配
- 输入命令
- 正确:curl http://localhost:9527/payment/lb -H “Host: www.atguigu.com”
- 错误:curl http://localhost:9527/payment/lb -H “Host: java.atguigu.net”
7)Method Route Predicate
- 官方文档
自己测试:
- yml
- Method=GET #根据请求方式进行匹配
- 输入地址:啥也不写就是get请求
8)Path Route Predicate
- 官方文档
测试
- 就是之前的路径匹配,详情查看12.4 入门配置----7),8)
9)Query Route Predicate
- 官方文档
自己测试
- yml
- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
- 正确:http://localhost:9527/payment/lb?username=31
- 错误:http://localhost:9527/payment/lb?username=-31
10)小总结
- 总yml
server:
port: 9527
spring:
application:
name: cloud-gateway
#网关配置:
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes: #routes表示可以路由多个,可以为某个controller里面的所有rest接口都可以做路由
#第一个路由
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
uri: lb://cloud-payment-service #匹配后提供服务的路由地址(8001 8002集群组成的微服务名称) 替换为动态的
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
#第二个路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址 写死的
uri: lb://cloud-payment-service #匹配后提供服务的路由地址 替换为动态的
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
#- After=2023-08-27T19:32:02.975+08:00[Asia/Shanghai] #这个匹配的请求要在指定的时间之后,路由地址lb才生效(亚洲/上海)
#- Before=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] #在指定时间之前生效
#- Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai] #在指定的2个时间之内生效
#- Cookie=username,zzyy #满足cookie名和正则表达式格式
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
#- Host=**.atguigu.com
#- Method=GET #根据请求方式进行匹配
- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka #以单机版为例,不然集群启动太慢
- 说白了,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。
12.7 Filter的使用
- GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
12.7.1 是什么
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。
Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生
12.7.2 Spring Cloud Gateway的Filter
生命周期,Only Two
- pre:业务逻辑之前
- post:业务逻辑之后
种类,Only Two:
-
GatewayFilter :单一的
-
GlobalFilter:全局的
-
具体查看官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories
12.7.3 常用的GatewayFilter(单一过滤器)
- YML
filters:
- AddRequestParameter=X-Request-Id,1024 #过滤器工厂会在匹配的请求头加上一对请求头,名称为X-Request-Id值为1024
- 剩下的查看官网
12.7.4 自定义全局GlobalFilter(全局过滤器)
1)两个主要接口介绍
implements GlobalFilter,Ordered
2)能干嘛
- 全局日志记录
- 统一网关鉴权
- …
3)案例代码
package com.angenin.springcloud.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Date;
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("**************come in MyLogGateWayFilter:" + new Date());
//获取request中的uname参数
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if(uname == null){
log.info("*******用户名为null,非法用户!!");
//设置 response 状态码 因为在请求之前过滤的,so就算是返回NOT_FOUND 也不会返回错误页面
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
//完成请求调用
return exchange.getResponse().setComplete();
}
//过滤链放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
//返回值是过滤器的优先级,越小优先级越高(最小-2147483648,最大2147483648)
return 0;
}
}
4)测试
-
启动
-
正确:http://localhost:9527/payment/lb?uname=z3(8001 8002来回切换)
-
错误
- 没有参数uname:http://localhost:9527/payment/lb,无法正常使用转发
- 没有参数uname:http://localhost:9527/payment/lb,无法正常使用转发
-
9527控制台日志
13 SpringCloud Config分布式配置中心
13.1 概述
13.1.1 分布式系统面临的----配置问题
-
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
-
SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理…
/(ㄒoㄒ)/~~
13.1.2 是什么
-
是什么
- SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为
各个不同微服务应用
的所有环境提供了一个中心化的外部配置
。
- SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为
-
怎么玩
- SpringCloud Config分为
服务端和客户端两部分
。 - 服务端也称为
分布式配置中心,它是一个独立的微服务应用
,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口 - 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
- SpringCloud Config分为
-
举例:
- 问题:3个微服务项目的yml文件连接的都是同一个数据库,此时需要修改端口号那么这3个yml文件都需要修改。
- 解决:把公用的配置抽取到配置中心,私有的配置写在各自的yml文件,这样就比较方便管理。
- 如下图:运维工程师在GitHub上面修改配置,之后同步到本地,再从本地同步到这个共用的配置中心,然后让A B C一起下,这样就实现了运维工程师一次修改处处发榜。
13.1.3 能干嘛
- 集中管理配置文件
- 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
- 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
- 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
- 将配置信息以REST接口的形式暴露:post、curl访问刷新均可…
13.1.4 与GitHub整合配置
- 由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式
13.1.5 官网
- https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/
13.2 Config服务端配置与测试
13.2.1 搭建GitHub环境
-
用你自己的账号在GitHub上新建一个名为springcloud-config的新Repository远程仓库
-
由上一步获得刚新建的git地址:
https://github.com/123shuai-aa/springcloud-config.git
-
本地硬盘目录上新建git本地仓库,并clone远程仓库到本地
-
本地仓库位置:E:\Git-bendiku\SpringCloud2020
-
打开命令行终端
-
命令行终端输入克隆命令:
git clone https://github.com/123shuai-aa/springcloud-config.git
- 因为这个新创建的远程库里面没有内容,所以会报警告。
- 因为这个新创建的远程库里面没有内容,所以会报警告。
-
效果:
-
-
在springcloud-config目录下创建3个yml配置文件:保存格式必须为UTF-8(默认就是)
-
开发环境:config-dev.yml
-
生产环境:config-pro.yml
-
测试环境:config-test.yml
-
-
config-dev.yml:
config:
info: "master branch,springcloud-config/config-dev.yml version=1"
- config-pro.yml:
config:
info: "master branch,springcloud-config/config-prod.yml version=1"
- config-test.yml:
config:
info: "master branch,springcloud-config/config-test.yml version=1"
- 使用命令将这3个文件推送到远程仓库:(前提是进入到命令行终端)
-
查看本地库状态:git status,可以看到3个yml文件
-
添加到暂存区:git add 文件名
- git add config-dev.yml
- git add config-pro.yml
- git add config-test.yml
-
提交本地库:git commit -m “日志信息” 文件名(注意为英文符号)
- git commit -m “init yml” config-dev.yml
- git commit -m “init yml” config-pro.yml
- git commit -m “init yml” config-test.yml
- 再次查看状态,提交过后显示没有文件需要提交。
-
远程库链接地址太长了不好记忆,所以给链接起个别名,将来推送和拉取代码的时候更加方便(
语法:git remote add 别名 远程库链接地址
)- 查看当前所有远程地址别名:git remote -v
- 默认别名是:origin,所以不需要再另起别名了。
-
推送本地库分支到远程仓库:
-
语法:git push 别名 分支(默认分支名是主分支:main)
-
git push origin main
-
之后会弹出一下窗口,选择
浏览器账号登录
。
-
输入GitHub账号密码
-
添加到凭据管理器中
-
命令行效果:
-
-
刷新浏览器的git页面,此时发现已将我们main分支上的内容推送到GitHub 创建的远程仓库
-
13.2.2 创建配置中心模块Congig Server
- 新建Module模块cloud-config-center-3344
它即为Cloud的配置中心模块cloudConfig Center
1)模块名:cloud-config-center-3344
2)POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-config-center-3344</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--config server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!--eureka client(通过微服务名实现动态路由)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3)YML
- 前提是git,GitHub环境 密码 账号 命令以及和idea的整合已经做过了。
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://github.com/123shuai-aa/springcloud-config.git #git的仓库地址
search-paths: #搜索目录
- springcloud-config
label: main #读取的分支
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka #服务注册到的eureka地址
- 对应的配置
4)主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344
{
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class, args);
}
}
5)windows:修改hosts文件
- windows下修改hosts文件,增加映射
- 127.0.0.1 config-3344.com
6)测试
- 测试通过Config微服务是否可以从GitHub上获取配置内容
- 启动微服务7001,3344
- http://config-3344.com:3344/main/config-dev.yml(可以访问到GitHub上面的配置文件,成功实现了用SpringCloud Config通过GitHub获取配置信息)
- 此时完成了这部分的内容:
13.2.3 配置读取规则
官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/#_quick_start
- 即:浏览器输入什么样格式的地址,可以读取到GitHub仓库中的配置文件。(5种)
讲解其中的3种:
- 第一种:/{label}/{application}-{profile}.yml (分支名+文件名,文件名之间有间隔号,推荐,就是上面测试的哪一种写法)
- master分支
- http://config-3344.com:3344/main/config-dev.yml
- http://config-3344.com:3344/main/config-test.yml
- http://config-3344.com:3344/main/config-pro.yml
- dev分支
- http://config-3344.com:3344/dev/config-dev.yml
- http://config-3344.com:3344/dev/config-test.yml
- http://config-3344.com:3344/dev/config-pro.yml
- master分支
- 第二种:/{application}-{profile}.yml(只写文件名,文件名之间有间隔号)
- http://config-3344.com:3344/config-dev.yml
- http://config-3344.com:3344/config-test.yml
- http://config-3344.com:3344/config-pro.yml
- http://config-3344.com:3344/config-xxxx.yml(不存在的配置),结果为:
{}
- 第三种:/{application}/{profile}[/{label}] (第一种的逆操作,结果是个json格式)
-
http://config-3344.com:3344/config/dev/main
-
http://config-3344.com:3344/config/test/main
-
http://config-3344.com:3344/config/test/dev
-
名词解释:
- /{name}-{profiles}.yml
- /{label}-{name}-{profiles}.yml
- label:分支(branch)
- name:服务名
- profiles:环境(dev/test/prod)
13.3 Config客户端配置与测试
13.3.1 新建cloud-config-client-3355
13.3.2 POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-config-client-3355</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--config server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--eureka client(通过微服务名实现动态路由)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
13.3.3 bootstrap.yml
-
是什么:
- applicaiton.yml是用户级的资源配置项
- bootstrap.yml是系统级的,
优先级更加高
- Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的
Application Context
的父上下文。初始化的时候,Bootstrap Context
负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment
。 Bootstrap
属性有高优先级,默认情况下,它们不会被本地配置覆盖。Bootstrap context
和Application Context
有着不同的约定,所以新增了一个bootstrap.yml
文件,保证Bootstrap Context
和Application Context
配置的分离。- 要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的,因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml
- 总结:可以理解为客户端A服务携带了2份配置文件,一个是 bootstrap.yml和配置中心3344沟通用的,一份是application.yml是自己用的,这样先从3344配置中心读取公有的配置到自己的bootstrap.yml中,在加载自己的application.yml,合起来才是最完整的客户端文件。
-
内容:
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: main #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/main/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#过程:表示3355通过3344间接读取到配置的主分支下面的config-dev配置文件。
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
- 说明:
13.3.4 主启动
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class,args);
}
}
13.3.5 业务类
在前面13.1.3能干嘛中讲过:
- 分布式配置中心可以,将配置信息以REST接口的形式暴露:post、curl访问刷新均可…
- 解释:既然配置信息暴漏了,那么3355就可以通过REST风格读取到3344配置中心的消息和内容的配置。
package com.angenin.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo() {
return configInfo;
}
}
@Value("${config.info}")
读取到的是GitHub中配置文件的前缀
13.3.6 测试
-
启动7001服务注册中心
-
启动Config配置中心3344微服务并自测
-
http://config-3344.com:3344/main/config-pro.yml
-
http://config-3344.com:3344/main/config-dev.yml
-
-
启动3355作为Client准备访问
- http://localhost:3355/configInfo
- http://localhost:3355/configInfo
-
结论:成功实现了客户端3355访问SpringCloud Config3344通过GitHub获取配置信息
13.3.7 问题随时而来,分布式配置的动态刷新问题
-
Linux运维修改GitHub上的配置文件内容做调整
- 修改config-dev.yml配置并提交到GitHub中,比如加个变量age或者版本号version
- 修改config-dev.yml配置并提交到GitHub中,比如加个变量age或者版本号version
-
刷新3344浏览器页面,发现ConfigServer配置中心立刻响应
-
刷新3355,发现ConfigClient客户端没有任何响应
-
3355没有变化除非自己重启或者重新加载
-
难到每次运维修改配置文件,客户端都需要重启??噩梦
13.4 Config客户端之动态刷新(手动)
- 目的:避免每次更新配置都要重启客户端微服务3355
13.4.1 动态刷新-修改3355模块
1)POM引入actuator监控
- 上面已经添加过了图形化监控依赖,意思自身发生变化后可以被别人监控到。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2)修改YML,暴露监控端口
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: main #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/main/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#过程:表示3355通过3344间接读取到配置的主分支下面的config-dev配置文件。
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
3)@RefreshScope业务类Controller修改
package com.angenin.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RefreshScope //实现刷新功能
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo() {
return configInfo;
}
}
4)测试:3355没有生效
- 重启3355配置生效
- 再次修改GitHub内容版本为3
- 自测访问3344发现内容已经改变----生效
- http://config-3344.com:3344/main/config-dev.yml
- http://config-3344.com:3344/main/config-dev.yml
- 访问3355----明明修改了配置但是还是没有生效
5)解决:3355没有生效问题
-
需要运维人员发送Post请求刷新3355
- 必须是POST请求
curl -X POST "http://localhost:3355/actuator/refresh"
(命令行窗口)
-
再次访问3355,此时虽然没有重启3355,但是发现版本号已经发生变化,说明配置生效了。
- 好处:避免了服务重启
- http://localhost:3355/configInfo
13.4.2 想想还有什么问题?
-
假如有多个微服务客户端3355/3366/3377,每个微服务都要执行一次post请求,手动刷新?
- 解决:for循环,写一个批量的脚本。
-
可否广播,一次通知,处处生效?我们想大范围的自动刷新,求方法
- 问题1:100台机器,只需要广播通知一次全部都生效,也不需要发生100次post请求了。
- 问题2:100台机器,有98台需要生效,剩下的2台不需要生效保持原来的版本,这种精确通知,精确清除。
-
解决:以上一些列问题,批量进行通知、精确通知可以使用----SpringCloud Bus消息总线来解决
14 SpringCloud Bus消息总线
14.1 概述
-
上一讲解的加深和扩充,一言以蔽之
- 分布式自动刷新配置功能
- Spring Cloud Bus 配合 Spring Cloud Config 使用可以实现配置的动态刷新。
-
是什么
- Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,
它整合了Java的事件处理机制和消息中间件的功能。
- Spring Clud Bus支持两种消息代理:RabbitMQ 和 Kafka
- Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,
-
能干嘛
- Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。
- Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。
-
为何被称为总线
- 什么是总线
- 在微服务架构的系统中,通常会使用
轻量级的消息代理
来构建一个共用的消息主题
,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线
。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
- 在微服务架构的系统中,通常会使用
- 基本原理
- ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic(主题)中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
- 如果对于主题等名词含义不理解的查看:
杨哥B站ActiveMQ课程:https://www.bilibili.com/video/av55976700?from=search&seid=15010075915728605208
- 什么是总线
14.2 RabbitMQ环境配置
- 课程安装的是windows环境下的RabbitMQ,这里我以linux环境下安装RabbitMQ为例
- 安装步骤查看:https://blog.csdn.net/aa35434/article/details/126634371
- 登录RabbitMQ后台管理界面:
http://192.168.10.140:15672/
- 用户名:admin
- 密码:123456
- 效果:
14.3 SpringCloud Bus动态刷新全局广播
- 必须先具备良好的RabbitMQ环境
14.3.1 再以3355为模板再制作一个3366
- 演示广播效果,增加复杂度,再以3355为模板再制作一个3366
1)新建cloud-config-client-3366
1)POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-config-client-3366</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--config server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--eureka client(通过微服务名实现动态路由)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2)YML
server:
port: 3366
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: main #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/main/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#过程:表示3366通过3344间接读取到配置的主分支下面的config-dev配置文件。
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
3)主启动
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3366 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3366.class,args);
}
}
4)controller
package com.angenin.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${server.port}")
private String serverPort;
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String configInfo() {
return "serverPort: "+serverPort+"\t\n\n configInfo: "+configInfo;
}
}
14.3.2 设计思想&技术选型
-
利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置
- 即:触发其中一个客户端3355,由3355广播通知给3366
- 即:触发其中一个客户端3355,由3355广播通知给3366
-
利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置
- 即:触发给配置中心3344,由3344广播通知给3355、3366
- 即:触发给配置中心3344,由3344广播通知给3355、3366
-
图二的架构显然更加适合,图一不适合的原因如下
- 打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责。
- 破坏了微服务各节点的对等性。
- 有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改
14.3.3 修改cloud-config-center-3344
给cloud-config-center-3344配置中心服务端
添加消息总线支持:
- POM
<!-- 添加rabbitMQ的消息总线支持包 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!--凡事要暴露监控刷新的操作,3344的pom文件一定要引入actuator依赖,之前已经添加过了-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- YML
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://github.com/123shuai-aa/springcloud-config.git #git的仓库地址
search-paths: #搜索目录
- springcloud-config
label: main #读取的分支
#rabbitmq相关配置
rabbitmq:
host: 192.168.10.140 #linux上的主机ip
port: 5672 #15672是图形管理界面的端口,5672是client访问端口
username: admin
password: 123456
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka #服务注册到的eureka地址
#rabbitmq相关配置,暴露bus刷新配置的端点
#凡事要暴露监控刷新的操作,3344的pom文件一定要引入actuator依赖
management:
endpoints: #暴露bus刷新配置的端点
web:
exposure:
include: 'bus-refresh'
14.3.4 修改cloud-config-client-3355
给cloud-config-client-3355客户端
添加消息总线支持
- POM
<!-- 添加rabbitMQ的消息总线支持包 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!--凡事要暴露监控刷新的操作,3344的pom文件一定要引入actuator依赖,之前已经添加过了-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- YML
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: main #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/main/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#过程:表示3355通过3344间接读取到配置的主分支下面的config-dev配置文件。
#rabbitmq相关配置
rabbitmq:
host: 192.168.10.140 #linux上的主机ip
port: 5672 #15672是图形管理界面的端口,5672是client访问端口
username: admin
password: 123456
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
14.3.5 修改cloud-config-client-3366
给cloud-config-client-3366客户端
添加消息总线支持
- POM
<!-- 添加rabbitMQ的消息总线支持包 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!--凡事要暴露监控刷新的操作,3344的pom文件一定要引入actuator依赖,之前已经添加过了-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- YML
server:
port: 3366
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: main #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/main/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#过程:表示3366通过3344间接读取到配置的主分支下面的config-dev配置文件。
#rabbitmq相关配置
rabbitmq:
host: 192.168.10.140 #linux上的主机ip
port: 5672 #15672是图形管理界面的端口,5672是client访问端口
username: admin
password: 123456
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
14.3.6 测试
-
启动7001、3344、3355、3366
-
运维工程师
-
修改Github上配置文件增加版本号
-
发送POST请求(
刷新的是3344配置中心
)-
curl -X POST “http://localhost:3344/actuator/bus-refresh”
-
一次发送,处处生效
-
-
-
配置中心
- http://config-3344.com:3344/main/config-dev.yml
- http://config-3344.com:3344/main/config-dev.yml
-
客户端
-
http://localhost:3355/configInfo
-
http://localhost:3366/configInfo
-
没有重启服务,再次获取配置信息,发现都已经刷新了
-
-
一次修改,广播通知,处处生效
-
登录RabbitMQ后台管理界面:
- 之前讲过的概念:
- 之前讲过的概念:
14.4 SpringCloud Bus动态刷新定点通知
14.1 业务需求
- 不想全部通知,只想定点通知
- 只通知3355
- 不通知3366
- 简单一句话
- 指定具体某一个实例生效而不是全部
- 公式:
http://localhost:配置中心的端口号/actuator/bus-refresh/{destination}
- /bus/refresh请求不再发送到具体的服务实例上,而是发给config server并通过destination参数类指定需要更新配置的服务或实例
14.2 测试案例
案例
-
我们这里以刷新运行在3355端口上的config-client为例
- 只通知3355
- 不通知3366
-
修改GitHub配置文件
-
刷新:
curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"
-
即:使用刷新3344通知指定的服务3355
-
bus-refresh
-
config-client:3355:
-
-
效果:3344,3355刷新了,3366没有刷新。(不重启)
http://config-3344.com:3344/main/config-dev.yml
http://localhost:3355/configInfo
http://localhost:3366/configInfo
14.3 总结
- 通知总结All
15 SpringCloud Stream消息驱动
15.1 消息驱动概述
15.1.1 Stream为什么被引入
-
场景:
- 现在的系统可以分成三部分,前端----》java后端----》大数据平台,现在java后端使用的是RabbitMQ而大数据平台使用的是Kafka,这样切换、维护、开发落地起来比较麻烦,
- 现在的系统可以分成三部分,前端----》java后端----》大数据平台,现在java后端使用的是RabbitMQ而大数据平台使用的是Kafka,这样切换、维护、开发落地起来比较麻烦,
-
目的:
- 有没有一种新的技术诞生,让我们不再关注具体MQ的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换-----SpringCloudStream
-
常用的消息中间件:
- ActiveMQ
- RabbitMQ
- RocketMQ
- Kafka(大数据常用)
15.1.1 是什么
-
什么是SpringCloudStream
- 官方定义 Spring Cloud Stream 是一个构建消息驱动微服务的框架。
- 应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream中binder对象交互。
- 通过我们配置来binding(绑定) ,而 Spring Cloud Stream 的 binder对象负责与消息中间件交互。
- 所以,我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。
- 通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。
- Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。
-
目前仅支持RabbitMQ、Kafka。
-
一句话:屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
-
官网:
-
https://spring.io/projects/spring-cloud-stream#overview
- Spring Cloud Stream是用于构建与共享消息传递系统连接的高度可伸缩的事件驱动微服务框架,该框架提供了一个灵活的编程模型,它建立在已经建立和熟悉的Spring熟语和最佳实践上,包括支持持久化的发布/订阅、消费组以及消息分区这三个核心概念
-
官方文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/3.0.1.RELEASE/reference/html/
-
Spring Cloud Stream中文指导手册
- https://m.wang1314.com/doc/webapp/topic/20971999.html
- https://m.wang1314.com/doc/webapp/topic/20971999.html
-
15.1.2 设计思想
-
标准MQ
- Message:生产者/消费者之间靠
消息
媒介传递信息内容 - 消息通道MessageChannel:消息必须走特定的
通道
- 消息通道里的消息如何被消费呢,谁负责收发处理
- 消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅
- Message:生产者/消费者之间靠
-
为什么用Cloud Stream
- 比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic和Partitions分区,这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,
一大堆东西都要重新推倒重新做
,因为它跟我们的系统耦合了,这时候springcloud Stream给我们提供了一种解耦合的方式。
- stream凭什么可以统一底层差异?
- 在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性通过定义绑定器作为中间层,完美地实现了
应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。 通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
- 在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性通过定义绑定器作为中间层,完美地实现了
Binder
-
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了
应用程序与消息中间件细节之间的隔离
。Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程 -
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
-
Binder可以生成Binding,Binding用来绑定消息容器的生产者和消费者,它有两种类型,INPUT和OUTPUT,INPUT对应于消费者,OUTPUT对应于生产者。
-
INPUT对应于消费者
-
OUTPUT对应于生产者
-
- 比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic和Partitions分区,这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,
-
Stream中的消息通信方式遵循了发布-订阅模式
- Topic主题进行广播
- 在RabbitMQ就是Exchange交换机
- 在Kakfa中就是Topic
- Topic主题进行广播
15.1.3 Spring Cloud Stream标准流程套路
- Binder
- 很方便的连接中间件,屏蔽差异
- Channel
- 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
- Source和Sink
- 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出(生产者),接受消息就是输入(消费者)。
15.1.4 编码API和常用注解
组成 | 说明 |
---|---|
Middleware | 中间件,目前只支FRabbitMQ和Kafka |
Binder | Binder是应用与消息中间件之间的封装,目前实行了KafKa和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现 |
@Input | 注解标识输入通道,通过该输入通接收到的消息息进入应用程序 |
@Output | 注解标识输出通道,发布的消息将通过该通道离开应用程序 |
@StreamListener | 监听队列,用于消费者的队列的消息接收 |
@EnableBinding | 指信道channel和exchange绑定在一起 |
15.2 案例说明
- RabbitMQ环境已经OK
- 工程中新建三个子模块
- cloud-stream-rabbitmq-provider8801, 作为生产者进行发消息模块
- cloud-stream-rabbitmq-consumer8802,作为消息接收模块
- cloud-stream-rabbitmq-consumer8803 作为消息接收模块
15.3 消息驱动之生产者
15.3.1 新建Module:cloud-stream-rabbitmq-provider8801
15.3.2 POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-stream-rabbitmq-provider8801</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!--基础配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
15.3.3 YML
- 爆红不影响使用
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.10.140
port: 5672
username: admin
password: 123456
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称 生产者使用output,消费者使用input
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: send-8801.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
15.3.4 主启动类StreamMQMain8801
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}
15.3.5 业务类
1)发送消息接口
package com.angenin.springcloud.service;
public interface IMessageProvider {
String send() ;
}
2)发送消息接口实现类
package com.angenin.springcloud.service.impl;
import com.angenin.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import javax.annotation.Resource;
import java.util.UUID;
/**
* 不再是以前crud操作数据库了:业务类上使用@service了,类里面注入dao层对象
* 现在操作的是消息中间件Rabbitmq:使用的是 SpringCloud Stream消息驱动中的注解
*/
@EnableBinding(Source.class) // 可以理解为定义消息的推送管道
public class MessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; // 消息的发送管道
@Override
public String send() {
//定义发送的消息
String serial = UUID.randomUUID().toString();
//使用绑定器将消息绑定起来
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("***serial: "+serial);
return null;
}
}
3)Controller
package com.angenin.springcloud.controller;
import com.angenin.springcloud.service.IMessageProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
//业务:每调用一次就发送一次流水号到Rabbitmq
@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
15.3.6 测试
- 启动7001eureka
- 启动rabbitmq
- 启动8801
- 访问Rabbitmq管理界面: http://192.168.10.140:15672
- 对应的配置:
- 访问Rabbitmq管理界面: http://192.168.10.140:15672
- 访问
- http://localhost:8801/sendMessage
- 多次发生请求:
- 后台日志的system.out…输出信息:
- 在Rabbitmq管理界面可以看到发送消息的峰值,说明以上的整合配置成功。
15.4 消息驱动之消费者
15.4.1 新建Module:cloud-stream-rabbitmq-consumer8802
15.4.2 POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-stream-rabbitmq-consumer8802</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--基础配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
15.4.3 YML
- 和生产者8801唯一的区别,8801生产者使用output,消费者8802使用input。当然起的名字不一样。
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.10.140
port: 5672
username: admin
password: 123456
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称 生产者使用output,消费者使用input
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: receive-8802.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
15.4.4 主启动类StreamMQMain8802
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StreamMQMain8802
{
public static void main(String[] args)
{
SpringApplication.run(StreamMQMain8802.class,args);
}
}
15.4.5 业务类
package com.angenin.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(Sink.class) //指信道channel和exchange绑定在一起
public class ReceiveMessageListener {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT) //监听队列,用于消费者的队列的消息接收
public void input(Message<String> message) {
System.out.println("消费者1号,----->接受到的消息: "+message.getPayload()+"\t port: "+serverPort);
}
}
15.4.6 测试8801发送8802接收消息
- 启动7001,Rabbitmq,8801,8802
- http://localhost:8801/sendMessage
- 查看Rabbitmq管理界面:
- 消息生产者发送消息
- 消息消费者接收到消息
15.5 分组消费与持久化
15.1 依照8802消费者,clone出来一份运行8803消费者
-
cloud-stream-rabbitmq-consumer8803
-
目录结构:
-
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.angenin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-stream-rabbitmq-consumer8803</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--基础配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
- YML
server:
port: 8803
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.10.140
port: 5672
username: admin
password: 123456
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称 生产者使用output,消费者使用input
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: receive-8803.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
- 主启动类
package com.angenin.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StreamMQMain8803 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8803.class,args);
}
}
- 业务类
package com.angenin.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListener
{
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message message)
{
System.out.println("消费者2号,------->接收到的消息:" + message.getPayload()+"\t port: "+serverPort);
}
}
15.2 启动
- 启动RabbitMQ
- 启动7001服务注册
- 启动8801消息生产
- 启动8802消息消费
- 启动8803消息消费
15.3 运行后有两个问题
- 有重复消费问题
- 消息持久化问题
15.4 消费
目前是8802/8803同时都收到了,存在重复消费问题
-
http://localhost:8801/sendMessage:8801生产者发送消息,发现消费者8802和8803都接收到了消息。
-
如何解决
分组和持久化属性group
-
生产实际案例
-
默认8802,8803是不同的分组所以可以重复消费。
15.5 分组
原理
- 微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。
不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
8802/8803都变成不同组,group两个不同
-
group: atguiguA、atguiguB
-
8802修改YML:
group: atguiguA
-
8803修改YML:
group: atguiguB
-
重启8802,8803生效
-
我们自己配置
- 分布式微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例,本例阳哥启动了两个消费微服务(8802/8803)多数情况,生产者发送消息给某个具体微服务时只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在Spring Cloud Stream中提供了
消费组
的概念。
- 分布式微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例,本例阳哥启动了两个消费微服务(8802/8803)多数情况,生产者发送消息给某个具体微服务时只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在Spring Cloud Stream中提供了
-
结论:还是重复消费
-
生产者发送消息,点击3次
-
生产者控制台8801
-
消费者控制台8802
-
消费者控制台8803
-
8802/8803都变成相同组,group两个相同
-
group: atguiguA
-
8802修改YML:atguiguA
-
8803修改YML:atguiguA
-
重启生效:
-
我们自己配置:
-
查看Rabbitmq管理界面发现2个分组:atguiguA,atguiguB。atguiguB是之前的历史记录和它没有关系。
-
atguiguA组里面有8802和8803两个消费者,这个时候消息发到A组里面每次产生竞争关系,只允许同一组下面的一个实例拿到,这样就避免了重复消费。
-
点击atguiguA:可以看到里面有2个消费者
-
点击atguiguB:消费者数量为0
-
-
结论:同一个组的多个微服务实例,每次只会有一个拿到
-
生产者发送消息,点击2次
-
生产者控制台8801
-
消费者控制台8802
-
消费者控制台8803
-
-
8802/8803实现了轮询分组,每次只有一个消费者
8801模块的发的消息只能被8802或8803其中一个接收到,这样避免了重复消费。
15.6 持久化
-
通过上述,解决了重复消费问题,再看看持久化
-
停止8802/8803并去除掉8802的分组group: atguiguA
- 8803的分组group: atguiguA没有去掉
- 8803的分组group: atguiguA没有去掉
-
8801先发送4条消息到rabbitmq:
http://localhost:8801/sendMessage
-
先启动8802,无分组属性配置,后台没有打出来消息
-
再启动8803,有分组属性配置,后台打出来了MQ上的消息
-
总结:消费者因为各种问题导致宕机,此时生产者持续发送消息,之后重新启动消费者,如果消费者
配置了分组
则会
消费Rabbitmq中未曾消费的消息,如果消费者没有配置分组
则不会
消费因为故障问题未曾被消费的消息,这样就导致了消息丢失。
16 SpringCloud Sleuth分布式请求链路跟踪
16.1 概述
16.1 为什么会出现这个技术?
- 为什么会出现这个技术?需要解决哪些问题?
- 在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
- 所以我们在微服务架构里面,由于调用的链路越来越多,我们有必要知道:从服务的A开始走了多少步,每一步的耗时是多少,完成了一次链路调用后走过了多少微服务,走过了那些节点。
- 分布式请求链路跟踪,超大型系统。需要在微服务模块极其多的情况下,比如80调用8001的,8001调用8002的,这样就形成了一个链路,如果链路中某环节出现了故障,我们可以使用Sleuth进行链路跟踪,从而找到出现故障的环节。
15.2 是什么
-
https://github.com/spring-cloud/spring-cloud-sleuth
-
Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案
-
在分布式系统中提供追踪解决方案并且兼容支持了zipkin
-
sleuth 负责跟踪,而zipkin负责展示
16.3 解决
- 发送链路数据后,它会以网页形式展现调用效果。
16.2 搭建链路监控步骤
16.2.1 zipkin
1)下载
-
SpringCloud从F版起已不需要自己构建Zipkin Server服务端了,只需调用jar包即可
-
https://repo1.maven.org/maven2/io/zipkin/zipkin-server/
-
zipkin-server-2.24.3-exec.jar
2)运行jar
- cmd进入
- 输入:java -jar zipkin-server-2.24.3-exec.jar
3)运行控制台
-
http://localhost:9411/zipkin/
-
术语
- 完整的调用链路
- 表示一请求链路,一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来
- 表示一请求链路,一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来
- 上图what
- 一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来
- 整个链路的依赖关系如下
- 一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来
- 名词解释
- Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识
- span:表示调用链路来源,通俗的理解span就是一次请求信息
- 完整的调用链路
16.2.2 服务提供者
1)cloud-provider-payment8001
- 考虑到新建项目的麻烦,所以这里不再新建项目了,直接使用以前的
2)POM
- 添加依赖
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
3)YML
spring:
application:
#微服务名称,将此服务项目入住到注册中心,那么就需要给此项目取个名字
name: cloud-payment-service
zipkin:
base-url: http://localhost:9411 # zipkin 地址
sleuth:
sampler:
probability: 1 #采样率值介于0到1之间,1则表示全部采集(一般不为1,不然高并发性能会有影响)
4)业务类PaymentController
- 在之前写的控制层中添加测试的方法
@GetMapping("/payment/zipkin")
public String paymentZipkin() {
return "hi ,i'am paymentzipkin server fall back,welcome to atguigu,O(∩_∩)O哈哈~";
}
16.2.3 服务消费者(调用方)
1)cloud-consumer-order80
- 考虑到新建项目的麻烦,所以这里不再新建项目了,直接使用以前的
2)POM
- 同样引入此依赖
<!-- 引入sleuth + zipkin -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
3)YML
spring:
application:
name: cloud-order-service
zipkin:
base-url: http://localhost:9411 # zipkin 地址
sleuth:
sampler:
# 采样率值 介于0-1之间 ,1表示全部采集
probability: 1
4)业务类OrderController
- 在之前写的控制层中添加测试的方法
// ====================> zipkin+sleuth
@GetMapping("/consumer/payment/zipkin")
public String paymentZipkin() {
String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
return result;
}
16.2.4 测试
-
依次启动eureka7001/8001/80
-
80调用8001多次测试下:
http://localhost/consumer/payment/zipkin
-
打开浏览器访问:http://localhost:9411
-
会出现以下界面
-
查找痕迹
-
查看依赖关系
-