目录
8.2.1、Hystrix支付微服务构建(未加入熔断降级前)
(1)新建cloud-provider-hygtrix-payment8001
(1)新建cloud-consumer-feign-hystrix-order80
(1)将8001 Hystrix提供者的Service方法改为如下
8.2.8、Hystrix全局服务降级DefaultProperties
8.2.9、Hystrix之通配服务降级FeignFallBack
(1)修改cloud-consumer-feign-hystrix-order80
(1)修改cloud-provider-hystrix-payment8001
(1)新建cloud-consumer-hystrix-dashboard9001
(5)所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置
(1)修改cloud-provider-hystrix-payment8001主启动类
9.1.3、有Zuul了怎么又出来Gateway?我们为什么选择Gateway?
(1)新建cloud-gateway-gateway9527
(5)查看cloud-provider-payment8001看看controller的访问地址
9.4.2、第二种:代码中注入RouteLocator的Bean
The After Route Predicate Factory
The Before Route Predicate Factory
The Between Route Predicate Factory
The Cookie Route Predicate Factory
The Header Route Predicate Factory
(1)用你自己的账号在Github或者码云上新建一个名为springcloud-config的新Repository
(2)获取springcloud-config的Git地址(SSH)
(6)测试通过Config微服务是否可以从Github上获取配置内容
(3)@RefreshScope业务类Controller修改
(1)创建cloud-stream-rabbitmq-provider8801
(1)创建cloud-stream-rabbitmq-consumer8802
(1)依照8802,克隆出来一份运行8803 - cloud-stream-rabbitmq-consumer8803
十三、SpringCloud Sleuth(分布式请求链路跟踪)
服务提供者cloud-provider-payment8001
服务消费者cloud-consumer-order80(调用方)
八、Hystrix(服务降级)
8.1、Hystrix基本概念
8.1.1、分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
对于高流量的应用来说,单一的后避依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
8.1.2、Hystrix是什么?
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
"断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
8.1.3、服务降级
概念
服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback。
哪些情况会触发降级
程序运行导常(数组下标越界、除数分母为0)。
超时。
服务熔断触发服务降级。
线程池/信号量打满也会导致服务降级。
8.1.4、服务熔断
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。服务熔断也是一种服务降级方式。
服务降级是你暂时不可用,返回个提示,10秒钟后可以重写访问。但服务熔断直接就不可用了,直接拉闸限电。
8.1.5、服务限流
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
8.2、Hystrix案例
8.2.1、Hystrix支付微服务构建(未加入熔断降级前)
(1)新建cloud-provider-hygtrix-payment8001
(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.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-provider-hygtrix-payment8001</artifactId>
<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.atguigu.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)主启动类
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
(5)Service业务类
@Service
public class PaymentService {
/**
* 正常访问,肯定OK
*/
public String paymentInfo_OK(Integer id) {
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O哈哈~";
}
/**
* 模拟方法运行时间超时
*/
public String paymentInfo_TimeOut(Integer id) {
try {
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): 3";
}
}
(6)Controller
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentService.paymentInfo_OK(id);
log.info("*****result: "+result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
{
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*****result: "+result);
return result;
}
}
(7)测试
启动eureka7001
启动cloud-provider-hystrix-payment8001
访问
success的方法 - http://localhost:8001/payment/hystrix/ok/1
每次调用耗费5秒钟 - http://localhost:8001/payment/hystrix/timeout/1
都能正常访问,只不过第二个访问的慢一点,3秒钟之后才返回数据到页面中。
以上述为根基平台,从正确 -> 错误 -> 降级熔断 -> 恢复。
8.2.2、Jmeter并发压力测试
当我们使用Jmeter 20000个并发同时访问http://localhost:8001/payment/hystrix/timeout/1接口。这时我们再访问http://localhost:8001/payment/hystrix/ok/1接口,发现http://localhost:8001/payment/hystrix/ok/1接口也开始转圈了,也就是说此接口也被拖慢了,那么问题来了,我们之前测试此接口明明是秒回啊,怎么被第二个接口拖慢了呢?20000个并发访问的是第二个接口,怎么能影响到第一个呢?
原因是当20000个并发同时打到第二个接口时,整个微服务的资源都在为你服务。
8.2.3、Jmeter压测结论
上面还是服务提供者8001自己测试,假如此时外部的消费者80也来访问,那消费者只能干等,最终导致消费端80不满意,服务端8001直接被拖慢。
8.2.4、新建消费者80
(1)新建cloud-consumer-feign-hystrix-order80
(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.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-hystrix-order80</artifactId>
<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.atguigu.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>
(3)yml配置文件
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
(4)主启动类
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
(5)Service Feign接口
@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);
}
(6)Controller
@RestController
@Slf4j
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
}
(7)测试
我们发现一样的超时,2万个并发同时访问第二个接口一样会拖慢第一个接口。
8.2.5、如何解决
超时导致服务器变慢(转圈) - 超时不再等待。
出错(宕机或程序运行出错) - 出错要有兜底。
解决:
对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级。
对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级。
对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级。
8.2.6、服务降级(提供者)
(1)将8001 Hystrix提供者的Service方法改为如下
@Service
public class PaymentService {
/**
* 正常访问,肯定OK
*/
public String paymentInfo_OK(Integer id) {
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O哈哈~";
}
/**
* 模拟方法运行时间超时,设置峰值3秒钟,此方法运行超过3秒钟直接调用fallback方法
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler"/*指定善后方法名*/,commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
})
public String paymentInfo_TimeOut(Integer id)
{
//int age = 10/0;
try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): ";
}
/**
* 用来善后的方法
*/
public String paymentInfo_TimeOutHandler(Integer id)
{
return "线程池: "+Thread.currentThread().getName()+" 8001系统繁忙或者运行报错,请稍后再试,id: "+id+"\t"+"o(╥﹏╥)o";
}
}
(2)主启动类激活
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker //加入
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
(3)测试
8.2.7、服务降级(消费者)
(1)将80 Hystrix消费者者的yml加入下面代码
#开启
feign:
hystrix:
enabled: true
(2)修改业务类controller
@RestController
@Slf4j
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
//int age = 10/0;
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
//善后方法
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
}
(3)主启动类加注解
@SpringBootApplication
@EnableFeignClients
@EnableHystrix //添加到此处
public class OrderHystrixMain80{
public static void main(String[] args){
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
注意:假如你的提供者设置的方法运行时间峰值为5秒,而方法真正运行时间为3秒,这样当然不用触发降级。但消费者这边设置方法运行时间的峰值为1.5秒,那么结果就是会触发消费者这端的fallback。
8.2.8、Hystrix全局服务降级DefaultProperties
目前问题:每个业务方法对应一个兜底的方法,代码膨胀。
解决方法:
1:1每个方法配置一个服务降级方法,技术上可以,但是不聪明
1:N除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback = “”)统一跳转到统一处理结果页面
通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量
(1)修改消费者80的Controller
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
/*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})*/
@HystrixCommand//用全局的fallback方法
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ㄒ)/~~";
}
}
① 在类上加入注解@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod"),代表全局降级方法。
② 编写全局降级方法payment_Global_FallbackMethod()。
③ 将paymentInfo_TimeOut方法的@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod"注解注释掉,然后加入单独的@HystrixCommand注解,代表用全局的fallback方法。
(2)测试
8.2.9、Hystrix之通配服务降级FeignFallBack
目前问题:统一和自定义的分开,代码混乱。
服务降级,客户端去调用服务端,碰上服务端宕机或关闭。
本次案例服务降级处理是在客户端80实现完成的,与服务端8001没有关系,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦。
未来我们要面对的异常:
运行
超时
宕机
(1)修改cloud-consumer-feign-hystrix-order80
根据cloud-consumer-feign-hystrix-order80已经有的PaymentHystrixService接口,重新新建一个类(AaymentFallbackService)实现该接口,统一为接口里面的方法进行异常处理。
PaymentFallbackService类实现PaymentHystrixService接口
@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";
}
}
(2)yml
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
#开启
feign:
hystrix:
enabled: true
(3)PaymentHystrixService接口
@Component
@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);
}
(4)测试
依次启动7001、PaymentHystrixMain8001、OrderHystrixMain80。
访问:http://localhost/consumer/payment/hystrix/ok/1
客户端自己调用提示 - 此时服务端provider已经down了,但是我们做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器。
8.2.10、服务熔断理论
熔断机制概述
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。
1、调用失败会触发降级,而降级会调用fallback方法。
2、但无论如何降级的流程一定会先调用正常方法再调用fallback方法。
3、假如单位时间内调用失败次数过多,也就是降级次数过多,则触发熔断。
4、熔断以后就会跳过正常方法直接调用fallback方法。
5、所谓“熔断后服务不可用”就是因为跳过了正常方法直接执行fallback。
8.2.11、服务熔断案例
(1)修改cloud-provider-hystrix-payment8001
@Service
public class PaymentService {
/**
* 正常访问,肯定OK
*/
public String paymentInfo_OK(Integer id) {
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O哈哈~";
}
/**
* 模拟方法运行时间超时,设置峰值3秒钟,此方法运行超过3秒钟直接调用fallback方法
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler"/*指定善后方法名*/,commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
})
public String paymentInfo_TimeOut(Integer id){
//int age = 10/0;
try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): ";
}
/**
* 用来善后的方法
*/
public String paymentInfo_TimeOutHandler(Integer id){
return "线程池: "+Thread.currentThread().getName()+" 8001系统繁忙或者运行报错,请稍后再试,id: "+id+"\t"+"o(╥﹏╥)o";
}
//=====服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if(id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id;
}
}
(2)Controller
//====服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result: "+result);
return result;
}
(3)测试
自测cloud-provider-hystrix-payment8001
正确 - http://localhost:8001/payment/circuit/1
错误 - http://localhost:8001/payment/circuit/-1
多次错误,再来次正确,但是出现了错误的显示。
重点测试 - 多次错误,然后慢慢正确,发现刚开始不满足条件,就算是正确的访问地址也不能进行,这就是直接服务熔断了。
8.2.12、服务熔断总结
涉及到断路器的三个重要参数:
1、快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
2、请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
3、错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
8.2.13、Hystrix工作流程
1、创建HystrixCommand (用在依赖的服务返回单个操作结果的时候)或HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候)对象。
2、命令执行。
3、其中 HystrixCommand实现了下面前两种执行方式:
① execute():同步执行,从依赖的服务返回一个单一的结果对象或是在发生错误的时候抛出异常。
② queue():异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。
4、而 HystrixObservableCommand实现了后两种执行方式:
① obseve():返回Observable对象,它代表了操作的多个统果,它是一个Hot Observable (不论“事件源”是否有“订阅者”,都会在创建后对事件进行发布,所以对于Hot Observable的每一个“订阅者”都有可能是从“事件源”的中途开始的,并可能只是看到了整个操作的局部过程)。
② toObservable():同样会返回Observable对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有“订间者”的时候并不会发布事件,而是进行等待,直到有“订阅者"之后才发布事件,所以对于Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程)。
5、若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。
6、检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑(第8步);如果断路器是关闭的,检查是否有可用资源来执行命令(第5步)。
7、线程池/请求队列信号量是否占满。如果命令依赖服务的专有线程地和请求队列,或者信号量(不使用线程的时候)已经被占满,那么Hystrix也不会执行命令,而是转接到fallback处理理辑(第8步) 。
8、Hystrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。
HystrixCommand.run():返回一个单一的结果,或者抛出异常。
HystrixObservableCommand.construct():返回一个Observable对象来发射多个结果,或通过onError发送错误通知。
9、Hystix会将“成功”、“失败”、“拒绝”、“超时” 等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行"熔断/短路"。
10、当命令执行失败的时候,Hystix会进入fallback尝试回退处理,我们通常也称波操作为“服务降级”。而能够引起服务降级处理的情况有下面几种:
① 第4步∶当前命令处于“熔断/短路”状态,断洛器是打开的时候。
② 第5步∶当前命令的钱程池、请求队列或者信号量被占满的时候。
③ 第6步∶HystrixObsevableCommand.construct()或HytrixCommand.run()抛出异常的时候。
11、当Hystrix命令执行成功之后,它会将处理结果直接返回或是以Observable的形式返回。
tips:如果我们没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常,Hystrix依然会运回一个Obsevable对象,但是它不会发射任结果数惯,而是通过onError方法通知命令立即中断请求,并通过onError方法将引起命令失败的异常发送给调用者。
8.3、Hystrix图形化Dashboard
8.3.1、概述
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。
Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
8.3.2、图形化搭建
(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.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-hystrix-dashboard9001</artifactId>
<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)主启动类
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
(5)所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
(6)启动9001
浏览器输入http://localhost:9001/hystrix
8.3.3、监控实战
(1)修改cloud-provider-hystrix-payment8001主启动类
注意:新版本Hystrix需要在主启动类PaymentHystrixMain8001中指定监控路径。
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*否则,Unable to connect to Command Metric Stream 404
*/
@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个eureka7001
启动8001,9001
(3)观察监控窗口
9001监控8001 - 填写监控地址 - http://localhost:8001/hystrix.stream 到 http://localhost:9001/hystrix页面的输入框。
测试地址
http://localhost:8001/payment/circuit/1
http://localhost:8001/payment/circuit/-1
测试通过
先访问正确地址,再访问错误地址,再正确地址,会发现图示断路器都是慢慢放开的。
九、GateWay(服务网关)
9.1、GateWay基本概念
9.1.1、GateWay是什么?
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和Project Reactor等技术。
Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。
SpringCloud Gateway是Spring Cloud的一个全新项目,基于Spring 5.0+Spring Boot 2.0和Project Reactor等技术开发的网关,它旨在为微服务架构提供—种简单有效的统一的API路由管理方式。
SpringCloud Gateway作为Spring Cloud 生态系统中的网关,目标是替代Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
9.1.2、微服务架构中网关的位置
9.1.3、有Zuul了怎么又出来Gateway?我们为什么选择Gateway?
1、netflix不太靠谱,zuul2.0一直跳票,迟迟不发布。
① 一方面因为Zuul1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来也非常的简单便捷。
② Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的Zuul 2.x,但Spring Cloud貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?
③ 多方面综合考虑Gateway是很理想的网关选择。
2、SpringCloud Gateway具有如下特性:
① 基于Spring Framework 5,Project Reactor和Spring Boot 2.0进行构建;
② 动态路由:能够匹配任何请求属性;
③ 可以对路由指定Predicate (断言)和Filter(过滤器);
④ 集成Hystrix的断路器功能;
⑤ 集成Spring Cloud 服务发现功能;
⑥ 易于编写的Predicate (断言)和Filter (过滤器);
⑦ 请求限流功能;
⑧ 支持路径重写。
3、SpringCloud Gateway与Zuul的区别:
① 在SpringCloud Finchley正式版之前,Spring Cloud推荐的网关是Netflix提供的Zuul。
② Zuul 1.x,是一个基于阻塞I/O的API Gateway。
③ Zuul 1.x基于Servlet 2.5使用阻塞架构它不支持任何长连接(如WebSocket)Zuul的设计模式和Nginx较像,每次I/О操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第-次加载较慢的情况,使得Zuul的性能相对较差。
④ Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul .x的性能较Zuul 1.x有较大提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway的RPS(每秒请求数)是Zuul的1.6倍。
⑤ Spring Cloud Gateway建立在Spring Framework 5、Project Reactor和Spring Boot2之上,使用非阻塞API。
⑥ Spring Cloud Gateway还支持WebSocket,并且与Spring紧密集成拥有更好的开发体验
9.1.4、GateWay非阻塞异步模型
Zuul1.x模型
Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Serviet IO处理模型。
Servlet的生命周期?servlet由servlet container进行生命周期管理。
1、container启动时构造servlet对象并调用servlet init()进行初始化;
2、container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service);
3、container关闭时调用servlet destory()销毁servlet。
上述模式的缺点:
Servlet是一个简单的网络IO模型,当请求进入Servlet container时,Servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(如抽风用Jmeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势。
所以Zuul 1.X是基于servlet之上的一个阻塞式处理模型,即Spring实现了处理所有request请求的一个servlet (DispatcherServlet)并由该servlet阻塞式处理。所以SpringCloud Zuul无法摆脱servlet模型的弊端。
Gateway模型
传统的Web框架,比如说: Struts2,SpringMVC等都是基于Servlet APl与Servlet容器基础之上运行的。
但是在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring 5必须让你使用Java 8)。
Spring WebFlux是Spring 5.0 引入的新的响应式框架,区别于Spring MVC,它不需要依赖Servlet APl,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。
9.2、GateWay工作流程
9.2.1、三大核心概念
1、Route(路由) - 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由。
2、Predicate(断言) - 参考的是Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。
3、Filter(过滤) - 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件;而fliter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。
举例:我去医院看牙科(请求),门卫大爷(Filter过滤)说不管你要看什么,必须先做核酸。当我做完核酸后门卫大爷让我进去了。这时我拿着手机上的挂号信息与医院的挂号信息相匹配(Predicate断言),匹配上了我可以找大夫看病了(路由)。
9.2.2、工作流程
客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到GatewayWeb Handler。
Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post")执行业务逻辑。
Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
核心逻辑:路由转发 + 执行过滤器链。
9.3、GateWay入门配置
9.3.1、Gateway9527搭建
(1)新建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.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-gateway-gateway9527</artifactId>
<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.atguigu.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: 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)主启动类
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class, args);
}
}
(5)查看cloud-provider-payment8001看看controller的访问地址
get方法和lb方法
我们目前不想暴露8001端口,希望在8001外面套一层9527。
(6)修改yml
server:
port: 9527
spring:
application:
name: cloud-gateway
#############################新增网关配置###########################
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
#uri: lb://cloud-payment-service #匹配后提供服务的路由地址
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
(7)测试
启动7001、8001、9527。
添加网关前 - http://localhost:8001/payment/get/1
添加网关后 - http://localhost:9527/payment/get/1
两者访问成功,返回相同结果!
9.4、GateWay配置路由的两种方式
9.4.1、第一种:yml
详细步骤请看9.3.1。
9.4.2、第二种:代码中注入RouteLocator的Bean
业务需求:通过9527网关访问到外网的百度新闻网址。
(1)创建配置类
@Configuration
public class GateWayConfig {
@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();
}
}
(2)测试
浏览器输入http://localhost:9527/guonei,返回页面不存在_百度搜索相同的页面。
9.5、GateWay配置动态路由
9.5.1、实现
默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能(不写死一个地址)。
(1)启动
eureka7001
payment8001/8002
(2)pom
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
(3)yml
server:
port: 9527
spring:
application:
name: cloud-gateway
#############################新增网关配置###########################
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
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
(4)测试
浏览器输入 - http://localhost:9527/payment/lb
结果:不停刷新页面,8001/8002两个端口切换。
9.6、GateWay常用的Predicate
The After Route Predicate Factory
此时间可以使用ZonedDateTime打印。
这个After大多用在维护开服时间,比如游戏准备22年10月1日开服,那用After可以设置此时间之后的路由才好使。
The Before Route Predicate Factory
与After类似。
The Between Route Predicate Factory
与After类似,两个时间之间用逗号分隔。
The Cookie Route Predicate Factory
两个参数:一个是Cookie name,另一个是正则表达式。
路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
打开DOS命令行进行测试:
curl http://localhost:9527/payment/lb //不带Cookie测试,结果肯定不能访问
curl http://localhost:9527/payment/lb --cookie "username=jcy" //带上Cookie,匹配成功
The Header Route Predicate Factory
两个参数:一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
打开DOS命令行进行测试:
# 带指定请求头的参数的CURL命令
curl http://localhost:9527/payment/lb -H "X-Request-Id:123"
9.7、GateWay的Filter
9.7.1、是什么?
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。
9.7.2、常见的过滤器
总共过滤器有31种之多。
9.7.3、自定义过滤器
自定义全局GlobalFilter:
两个主要接口介绍:
- GlobalFilter
- Ordered
能干什么:
- 全局日志记录
- 统一网关鉴权
- …
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("***********come in MyLogGateWayFilter: "+new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if(uname == null) {
log.info("*******用户名为null,非法用户,o(╥﹏╥)o");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
测试,分别启动:
EurekaMain7001
PaymentMain8001
PaymentMain8002
GateWayMain9527
浏览器输入:
http://localhost:9527/payment/lb - 访问异常
http://localhost:9527/payment/lb?uname=abc - 正常访问
十、SpringCloud Config(分布式配置中心)
10.1、介绍
分布式系统面临的配置问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理.……
比如你现在有40个微服务都用的一个叫test的数据库,如果我们修改了数据库的名字,那还要每个微服务的配置文件都改一遍吗,所以我们需要Config进行集中式管理。
是什么?
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
怎么玩?
SpringCloud Config分为服务端和客户端两部分。
● 服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。
● 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
能干嘛?
● 集中管理配置文件。
● 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release。
● 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息。
● 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置。
● 将配置信息以REST接口的形式暴露 - post/crul访问刷新即可…
10.2、Config服务端配置与测试
(1)用你自己的账号在Github或者码云上新建一个名为springcloud-config的新Repository
(2)获取springcloud-config的Git地址(SSH)
(3)本地硬盘目录上新建Git仓库并Clone
在springcloud-config的文件夹种创建三个配置文件(为本次教学使用的)。随后git add .,git commit -m "sth",push等一系列上传操作上传到springcloud-config的新Repository。
● config-dev.yml
config:
info: "master branch,springcloud-config/config-dev.yml version=7"
● config-prod.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"
10.3、Config配置总控中心搭建
(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.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-config-center-3344</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<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配置文件
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://gitee.com/ak/springcloud-config.git #GitHub上面的git仓库名字
# username: xxx@qq.com
# password: 123456
####搜索目录
search-paths:
- springcloud-config
####读取分支
label: master
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
(4)主启动类
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344 {
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class, args);
}
}
(5)Windows下修改hosts文件,增加映射
127.0.0.1 config-3344.com
(6)测试通过Config微服务是否可以从Github上获取配置内容
依次启动7001、3344
访问:http://config-3344.com:3344/master/config-dev.yml
10.4、Config客户端配置与测试
(1)新建cloud-config-client-3355
(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.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-config-client-3355</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<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)创建bootstrap.yml
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
(4)主启动类
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class, args);
}
}
(5)业务类
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo(){
return configInfo;
}
}
(6)测试
启动7001、3344、3355
访问:http://localhost:3355/configInfo
成功的实现了客户端3355访问SpringCloud Config3344通过GitHub获取配置信息。
(7)随之而来的问题
- Linux运维修改GitHub上的配置文件内容做调整。
- 刷新3344,发现ConfigServer配置中心立刻响应。
- 刷新3355,发现ConfigClient客户端没有任何响应。
- 3355没有变化除非自己重启或者重新加载。
- 难到每次运维修改配置文件,客户端都需要重启??噩梦。
10.5、Config动态刷新之手动版
避免每次更新配置都要重启客户端微服务3355。
(1)修改3355模块,POM引入actuator监控
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
(2)修改YML,暴露监控端口
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
(3)@RefreshScope业务类Controller修改
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo() {
return configInfo;
}
}
(4)运维人员发送Post请求刷新3355
curl -X POST "http://localhost:3355/actuator/refresh"
(5)测试
启动7001、3344、3355。
修改GitHub上的版本号,比如现在是1版本,改为10。
刷新3344网址看版本号是否跟着改变:http://config-3344.com:3344/master/config-dev.yml
刷新3355网址看版本号是否跟着改变:http://localhost:3355/configInfo
(6)出现的问题
假如有多个微服务客户端3355、3366、3377.......,那每个服务都要运维通过curl执行一次post请求,手动刷新?很显然有点麻烦,那能不能用广播的形式,一次通知,处处生效?当然可以,下一章消息总线Bus见......
十一、SpringCloud Bus(消息总线)
11.1、介绍
Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态刷新。
Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。Spring Cloud Bus目前支持RabbitMQ和Kafka。
什么是总线?
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
基本原理
ConfigClient实例都监听MQ中同一个topic(默认是Spring Cloud Bus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
11.2、Bus之RabbitMQ环境配置
安装步骤省略.....
开启RabbitMQ服务
11.3、创建3366(跟3355一样)
(1)新建cloud-config-client-3366
(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.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-config-client-3366</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<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)bootstrap.yml配置文件
server:
port: 3366
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
(4)主启动类
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3366 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3366.class,args);
}
}
(5)业务类
@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;
}
}
(6)设计思想
1、利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置。
2、利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置。
第二种的架构显然更加适合,第一种不适合的原因如下:
- 打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责。
- 破坏了微服务各节点的对等性。
- 有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改。
11.4、Bus动态刷新全局广播配置实现
给3344配置中心服务端添加消息总线支持
(1)pom
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
(2)YML配置文件
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://gitee.com/ak/springcloud-config.git #GitHub上面的git仓库名字
# username: xxx@qq.com
# password: 123456
####搜索目录
search-paths:
- springcloud-config
####读取分支
label: master
#rabbitmq相关配置<--------------------------
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
##rabbitmq相关配置,暴露bus刷新配置的端点<--------------------------
management:
endpoints: #暴露bus刷新配置的端点
web:
exposure:
include: 'bus-refresh'
给3355客户端添加消息总线支持
(1)pom
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
(2)YML配置文件
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
#rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口<----------------------
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
给3366客户端添加消息总线支持
(1)pom
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
(2)YML配置文件
server:
port: 3366
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口<-----------------------
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
测试
(1)依次启动7001、3344、3355、3366
(2)依次访问下面的地址,看版本号是多少?
http://config-3344.com:3344/master/config-dev.yml
http://localhost:3355/configInfo
http://localhost:3366/configInfo
版本号得出都为1。
(3)运行在Github上修改版本为2,然后发布post请求
curl -X POST "http://localhost:3344/actuator/bus-refresh"
(4)再次访问那三个地址,发现版本已经全部变为了2
11.5、Bus动态刷新定点通知
需求:我们不想全部都通知,只想定点通知,比如只通知3355,不通知3366
公式:http://localhost:3344/actuator/bus-refresh/{destination}
/bus/refresh请求不再发送到具体的服务实例上,而是发给config server通过destination参数类指定需要更新配置的服务或实例。
实现:我们运维先将Github上的版本从2改为3,然后运行下面post请求
curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355
十二、SpringCloud Stream(消息驱动)
12.1、Stream为什么被引入?
常见MQ(消息中间件):
- ActiveMQ
- RabbitMQ
- RocketMQ
- Kafka
有没有一种新的技术诞生,让我们不再关注具体MQ的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换。(类似于Hibernate)
Cloud Stream是什么?屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。
其实Hibernate就是这个意思,我们不用关心它用的是oracle还是mysql,它给我们提供了session.create(),我们用就好了,不用关心具体的语法,我们也不可能所有数据库都精通。
12.2、Binder介绍
官方定义Spring Cloud Stream是一个构建消息驱动微服务的框架。
应用程序通过inputs或者outputs来与Spring Cloud Stream中binder对象交互。
通过我们配置来binding(绑定),而Spring Cloud Stream 的binder对象负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。
Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。
目前仅支持RabbitMQ、 Kafka。
12.3、Stream的设计思想
为什么用Cloud Stream?
比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic和Partitions分区。
这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候Spring Cloud Stream给我们提供了—种解耦合的方式。
Stream凭什么可以统一底层差异?
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Binder:
- INPUT对应于消费者
- OUTPUT对应于生产者
Stream编码常用注解简介
Spring Cloud Stream标准流程套路
Binder - 很方便的连接中间件,屏蔽差异。
Channel - 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置。
Source和Sink - 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。
编码API和常用注解
案例说明
(1)准备RabbitMQ环境
(2)工程中新建三个子模块
- cloud-stream-rabbitmq-provider8801,作为生产者进行发消息模块
- cloud-stream-rabbitmq-consumer8802,作为消息接收模块
- cloud-stream-rabbitmq-consumer8803,作为消息接收模块
12.4、Stream消息驱动之生产者(发送)
(1)创建cloud-stream-rabbitmq-provider8801
(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.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-stream-rabbitmq-provider8801</artifactId>
<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>
(3)YML配置文件
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
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地址
(4)主启动类
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}
(5)业务类
public interface IMessageProvider {
public String send();
}
@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;
}
}
(6)controller
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
(7)测试
依次启动7001、RabbitMQ服务、8801
访问:http://localhost:8801/sendMessage
12.5、Stream消息驱动之消费者(接收)
(1)创建cloud-stream-rabbitmq-consumer8802
(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.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-stream-rabbitmq-consumer8802</artifactId>
<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>
(3)YML配置文件
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
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地址
(4)主启动类
@SpringBootApplication
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class,args);
}
}
(5)业务类
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消费者1号,----->接受到的消息: "+message.getPayload()+"\t port: "+serverPort);
}
}
(6)测试
依次启动EurekaMain7001、StreamMQMain8801、StreamMQMain8802
8801发送8802接收消息。
12.6、消息重复消费
(1)依照8802,克隆出来一份运行8803 - cloud-stream-rabbitmq-consumer8803
(2)启动
依次启动7001、8801、8802、8803,别忘记启动RabbitMQ
我们访问http://localhost:8801/sendMessage,连续发送两次。
分别看8802和8803接收到的消息:发现消息是8802/8803同时都收到了,存在重复消费问题。
(3)如何解决消息重复消费
分组和持久化属性group(重要)
比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中的消息分组来解决。
注意在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。不同组是可以全面消费的(重复消费)。
(4)实现(不同分组)
8802修改YML
group: A_Group
8803修改YML
group: B_Group
结论:还是重复消费,因为两个在不同组。
(5)实现(相同分组)
把8802和8803都改成相同组A_Group。
12.7、消息持久化
通过上述,解决了重复消费问题,再看看持久化。
停止8802/8803并去除掉8802的分组group: A_Group,8803的分组group: A_Group没有去掉。
8801先发送4条消息到RabbitMQ。
先启动8802,无分组属性配置,后台没有打出来消息。
再启动8803,有分组属性配置,后台打出来了MQ上的消息。(消息持久化体现)。
十三、SpringCloud Sleuth(分布式请求链路跟踪)
13.1、Sleuth介绍
为什么会出现这个技术?要解决哪些问题?
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
是什么?
Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案。
在分布式系统中提供追踪解决方案并且兼容支持了zipkin。
13.2、Sleuth之zipkin搭建安装
下载
SpringCloud从F版起已不需要自己构建Zipkin Server了,只需调用jar包即可。
Central Repository: io/zipkin/zipkin-server
zipkin-server-2.12.9-exec.jar
运行jar
java -jar zipkin-server-2.12.9-exec.jar
运行控制台
完整的调用链路
表示一请求链路,一条链路通过Trace ld唯一标识,Span标识发起的请求信息,各span通过parent id关联起来。
—条链路通过Trace ld唯一标识,Span标识发起的请求信息,各span通过parent id关联起来。
Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识。
span:表示调用链路来源,通俗的理解span就是一次请求信息。
13.3、Sleuth链路监控展现
服务提供者cloud-provider-payment8001
(1)pom
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
(2)YML
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采样率值介于 0 到 1 之间,1 则表示全部采集
probability: 1
(3)controller
@GetMapping("/payment/zipkin")
public String paymentZipkin() {
return "hi ,i'am paymentzipkin server fall back,welcome to here, O(∩_∩)O哈哈~";
}
服务消费者cloud-consumer-order80(调用方)
(1)pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
(2)YML
spring:
application:
name: cloud-order-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1
(3)OrderController
@GetMapping("/consumer/payment/zipkin")
public String paymentZipkin() {
String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
return result;
}
(4)测试
依次启动7001、8001、80