Spring Cloud 中级篇

1.Hystrix断路器

目前,Spring Cloud官方Hystrix停止更新了,但是它的设计理念非常的优秀,出道就是巅峰,服务降级、服务熔断、服务限流、服务隔离等等,一系列的设计思想是后面框架借鉴或者说抄作业的必备良药;所以说有必要深入了解Hystrix,虽然官方上推荐使用resilence4j,但在国内用的少,结合中国特色,主要讲解Hystrix,和阿里巴巴的sentinel。

1.1.概述

1.1.1.分布式系统面临的问题

首先,随着系统拆分了以后,分布式会面临一堆问题,在软件开发当中有一句话叫做:高内聚、低耦合,现在,工程一个一个分开了,耦合度一定是降低了;但是,现在我们经常会出现80调用8001,假设8002又调用8004,8004又调用8006,链路会越来越长,一条绳上的蚂蚱,一个出事了一定会全体连坐;

分布式系统面临的问题
复杂分布式系统结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。

服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其它系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其它的模块,这样就会发生级联故障,或者叫雪崩。

1.1.2.是什么

Hystrix是一个用于处理分布式系统的延迟容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

1.1.3.能干嘛

a. 服务降级
b. 服务熔断
c. 接近实时的监控

1.1.4.官网资料

https://github.com/Netflix/Hystrix/wiki/How-To-Use

1.1.5.Hystrix官宣,停更进维

https://github.com/Netflix/Hystrix

a. 被动修复bugs
b. 不再接受合并请求
c. 不再发布新版本

1.2.Hystrix重要概念

1.2.1.服务降级

a. 服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback

b. 哪些情况会触发降级
1). 程序运行异常
2). 超时
3). 服务熔断触发服务降级
4). 线程池/信号量打满也会导致服务降级

1.2.2.服务熔断

a. 类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
b. 就是保险丝
服务的降级->进而熔断->恢复调用链路

1.2.3.服务限流

秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行

1.3.hystrix案例

1.3.1.构建

a. 新建cloud-provider-hystrix-payment8001

b. POM
增加hystrix依赖:

  <!-- hystrix -->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
  </dependency>

c. YML

server:
  port: 8001

spring:
  application:
    name: cloud-provider-hystrix-payment

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka
      #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002:7002/eureka  # 集群版

d. 主启动

@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
}

e. 业务类
1). service

@Service
public class PaymentService {

    /**
     * 正常访问,肯定OK
     *
     * @param id
     * @return
     */
    public String paymentInfo_OK(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + " paymentInfo_OK,id:" + id + "\t" + "O(∩_∩)O哈哈~";
    }

    public String paymentInfo_Timout(Integer id) {
        int timeNumber = 3;
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:" + Thread.currentThread().getName() + " paymentInfo_Timout,id:" + id + "\t" + "O(∩_∩)O哈哈~" + " 耗时(秒):" + timeNumber;
    }

}

2). 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_Timout(@PathVariable("id") Integer id) {
        String result = paymentService.paymentInfo_Timout(id);
        log.info("******result: " + result);
        return result;
    }
}

f. 正常测试
1). 启动eureka7001

2). 启动cloud-provider-hystrix-payment8001

3). 访问
3.1). success的方法
http://localhost:8001/payment/hystrix/ok/31

3.2). 每次调用耗费3秒钟
http://localhost:8001/payment/hystrix/timout/31

4). 上述module均OK
以上述为根基平台,从正确->错误->降级熔断->恢复

1.3.2.高并发测试

a. 上述在非高并发情形下,还能勉强满足

b. Jmeter压力测试
1). 开启Jmeter,来20000个并发压死8001,20000个请求都去访问paymentInfo_Timeout服务

2). 再来一个访问
http://localhost:8001/payment/hystrix/timeout/31

3). 看演示结果
3.1). 两个请求都在自己转圈圈
3.2). 为什么会被卡死

tomcat的默认的工作线程数都被打满了,没有多余的线程来分解压力和处理。

c. Jmeter压测结论
上面还只是服务提供者8001自己测试,假如此时外部的消费者80也来访问,那消费者只能干等,最终导致消费者80不满意,服务端8001直接被拖死

d. 看热闹不嫌弃事大,80新建加入
1). cloud-consumer-feign-hystrix-order80
1.1). 新建

1.2). POM

    <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.neo.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </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>

1.3). YML

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

1.4). 主启动

@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderHystrixMain80.class, args);
    }
}

1.5). 业务类
1.5.1). PaymentHystrixService

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {

    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    String paymentInfo_Timout(@PathVariable("id") Integer id);
}

1.5.2). OrderHystrixController

@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_Timout(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_Timout(id);
        return result;
    }
}

1.6). 正常测试
http://localhost/consumer/payment/hystrix/ok/31

1.7). 高并发测试
1.7.1). 2W个线程压8001

1.7.2). 消费端80微服务再去访问正常的ok微服务8001地址

1.7.3). 访问http://localhost/consumer/payment/hystrix/ok/32
消费者80:
要么转圈圈等待
要么消费端报超时错误

1.3.3.故障现象和导致原因

a. 8001同一层次的其它接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完毕
b. 80此时调用8001,客户端访问响应缓慢,转圈圈

1.3.4.上述结论

正因为有上述故障或不佳表现,才有我们的降级/容错/限流等技术诞生

1.3.5.如何解决?解决的要求

a. 超时导致服务器变慢(转圈)
超时不再等待

b. 出错(宕机或程序运行出错)
出错要有兜底

c. 解决
1). 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须要有服务降级
2). 对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级
3). 对方服务(8001)OK,调用者(80)自己故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级

1.3.6.服务降级

a. 降级配置
@HystrixCommand

b. 8001先从自身找问题
设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,做服务降级fallback

c. 8001fallback
1). 业务类启用

    /**
     * 超时访问,演示降级
     *
     * @param id
     * @return
     */
    @HystrixCommand(fallbackMethod = "paymentInfo_TimoutHandler", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
    })
    public String paymentInfo_Timout(Integer id) {
        int timeNumber = 5;
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:" + Thread.currentThread().getName() + " paymentInfo_Timout,id:" + id + "\t" + "O(∩_∩)O哈哈~" + " 耗时(秒):" + timeNumber;
    }

    public String paymentInfo_TimoutHandler(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + " 系统繁忙或者运行报错,请稍后再试,id:" + id + "\t" + "/(ㄒoㄒ)/~~";
    }

@HystrixCommand报异常后如何处理
1.1). 一旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法

1.2). 图示

上图故意制造两个异常:

  1. int age = 10/0;计算异常
  2. 我们能接受3秒钟,它运行5秒钟,超时异常
    当前服务不可用了,做服务降级,兜底的方案都是paymentInfo_TimoutHandler

2). 主启动类激活
添加新注解@EnableCircuitBreaker

d. 80fallback
1). 80订单微服务,也可以更好的保护自己,自己也依样葫芦进行客户端降级保护

2). 题外话,切记
我们自己配置过的热部署方法对Java代码的改动明显,但对@HystrixCommand内属性的修改建议重启微服务

3). YML

feign:
  hystrix:
    enabled: true

4). 主启动
@EnableHystrix

5). 业务类

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentInfoFallbackMethod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
    })
    public String paymentInfo_Timout(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_Timout(id);
        return result;
    }

    public String paymentInfoFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,/(ㄒoㄒ)/~~";
    }

e. 目前问题
1). 每个业务方法对应一个兜底的方法,代码膨胀
2). 统一和自定义的分开

f. 解决问题
1). 解决代码膨胀:每个方法配置一个兜底的方法
1.1). @DefaultProperties(defaultFallback="")
说明:

@DefaultProperties(defaultFallback="")
1: 1每个方法配置一个服务降级方法,技术上可以,实际上傻X
1: N除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback="")统一跳转到统一处理结果页面
通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量,O(∩_∩)O~

1.2). controller配置

1.3). 测试配置成功

2). 解决代码混乱(代码耦合度高):和业务逻辑混在一起
2.1). 服务降级,客户端去调用服务端,碰上服务端宕机或关闭
本次案例服务降级处理是在客户端80实现完成的,与服务端8001没有关系,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦

2.2). 未来我们要面对的异常
运行、超时、宕机

2.3). 修改cloud-consumer-feign-hystrix-order80
2.3.1). 根据cloud-consumer-feign-hystrix-order80已经有的PaymentHystrixService接口,重新新建一个类(PaymentFallbackService)实现该接口,统一为接口里面的方法进行异常处理

2.3.2). PaymentFallbackService类实现PaymentHystrixService接口

@Component
public class PaymentFallbackService implements PaymentHystrixService {

    @Override
    public String paymentInfo_OK(Integer id) {
        return "-------PaymentFallbackService fall back-paymentInfo_OK,/(ㄒoㄒ)/~~";
    }

    @Override
    public String paymentInfo_Timout(Integer id) {
        return "-------PaymentFallbackService fall back-paymentInfo_Timout,/(ㄒoㄒ)/~~";
    }
}

2.3.3). YML

# 用于服务降级 在注解@FeignClient中添加fallbackFactory属性
feign:
  hystrix:
    enabled: true # 在Feign中开启Hystrix

2.3.4). PaymentHystrixService接口
添加fallback属性:fallback = PaymentFallbackService.class

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {

    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    String paymentInfo_Timout(@PathVariable("id") Integer id);
}

2.3.5). 测试

  1. 单个eureka先启动7001
  2. PaymentHystrixMain8001启动
  3. 正常访问:http://localhost/consumer/payment/hystrix/ok/31
  4. 故意关闭微服务8001
  5. 客户端自己调用提示

微服务8001关闭前:

微服务8001关闭后:

1.3.7.服务熔断

a. 断路器
一句话就是家里的保险丝

b. 熔断是什么

熔断机制概述
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当监测到该节点微服务调用响应正常后,恢复调用链路。
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。

https://martinfowler.com/bliki/CircuitBreaker.html

c. 实操
修改cloud-provider-hystrix-payment8001
1). PaymentService
注解@HystrixProperty的属性,都可以在com.netflix.hystrix.HystrixCommandProperties类中找到

    // =====服务熔断
    @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;
    }

为什么配置这些参数:

The precise way that the circuit opening and closing occurs is as follows:
1.Assuming the volume across a circuit meets a certain threshold : HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
2.And assuming that the error percentage, as defined above exceeds the error percentage defined in : HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
3.Then the circuit-breaker transitions from CLOSED to OPEN.
4.While it is open, it short-circuits all requests made against that circuit-breaker.
5.After some amount of time (HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), the next request is let through (this is the HALF-OPEN state). If the request fails, the circuit-breaker returns to the OPEN state for the duration of the sleep window. If the request succeeds, the circuit-breaker transitions to CLOSED and the logic in 1. takes over again.

2). PaymentController

    // ====服务熔断
    @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/31
错误访问:http://localhost:8001/payment/circuit/-31
测试:
在10秒钟内请求10次且失败率达到60%以上,服务会降级并开启断路器,正确请求也返回fallback方法:

请求失败率阈值低于60%后,断路器开始半开,到调用链路逐渐自动恢复

d. 原理(小总结)
1). 结论

2). 熔断类型
2.1). 熔断打开
请求不再进行调用当前服务,内置设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设始终则进入半熔断状态
2.2). 熔断半开
部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
2.3). 熔断关闭
熔断关闭不会对服务进行熔断

3). 官网断路器流程图

3.1). 官网步骤

The precise way that the circuit opening and closing occurs is as follows:
1.Assuming the volume across a circuit meets a certain threshold : HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
2.And assuming that the error percentage, as defined above exceeds the error percentage defined in : HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
3.Then the circuit-breaker transitions from CLOSED to OPEN.
4.While it is open, it short-circuits all requests made against that circuit-breaker.
5.After some amount of time (HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), the next request is let through (this is the HALF-OPEN state). If the request fails, the circuit-breaker returns to the OPEN state for the duration of the sleep window. If the request succeeds, the circuit-breaker transitions to CLOSED and the logic in 1. takes over again.

3.2). 断路器在什么情况下开始起作用

涉及到断路器的三个重要参数:快照时间窗口、请求总数阈值、错误百分比阈值。

  1. 快照时间窗口:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗口,默认为最近的10秒。
  2. 请求总数阈值:在快照时间窗口内,必须满足请求总数阈值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其它原因失败,断路器都不会打开。
  3. 错误百分比阈值:当请求总数在快照时间窗口内超过了阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阈值情况下,这时候就会将断路器打开。

3.3). 断路器开启或关闭的条件
3.3.1). 当满足一定的阈值的时候(默认10秒内超过20个请求次数)
3.3.2). 当失败率达到一定的时候(默认10秒内超过50%的请求失败)
3.3.3). 达到以上阈值,断路器将会开启
3.3.4). 当开启的时候,所有请求都不会进行转发
3.3.5). 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发,如果成功,断路器会关闭,若失败,继续开启。重复3.3.4)和3.3.5)

3.4). 断路器打开之后

  1. 再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
  2. 原来的主逻辑要如何恢复?
    对于这一问题,hystrix也为我们实现了自动恢复功能。
    当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器进入打开状态,休眠时间窗重新计时。

3.5). 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 2021";
}
public String str_fallbackMethod()
{
    return "*****fall back str_fallbackMethod";
}

1.3.8.服务限流

后面高级篇讲解Alibaba的Sentinel说明
什么事都要有一个取舍和侧重,毕竟Hystrix是×,后续Alibaba限流比它更好,把时间花在更重要的知识上面。

1.4.hystrix工作流程

1.4.1.https://github.com/Netflix/Hystrix/wiki/How-it-Works

1.4.2.Hystrix工作流程

1.官网图例

2.步骤说明

1.4.服务监控hystrixDashboard

1.4.1.概述

除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起地请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。

1.4.2.仪表盘8001

a. 新建cloud-consumer-hystrix-dashboard9001

b. POM

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>

c. YML

server:
  port: 9001

d. HystrixDashboardMain9001 + 新注解@EnableHystrixDashboard

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashboardMain9001.class, args);
    }
}

e. 所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置

        <!-- actuator监控信息完善 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

f. 启动cloud-consumer-hystrix-dashboard9001该微服务后续将监控微服务8001
访问http://localhost:9001/hystrix,能看到如下图标说明微服务监控程序搭建成功。

1.4.3.断路器演示(服务监控HystrixDashboard)

a. 修改cloud-provider-hystrix-payment8001
注意:新版本Hystrix需要在主启动类PaymentHystrixMain8001中指定监控路径,否则报错:Unable to connect to Command Metric Stream.

    /**
     * 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
     * ServletRegistrationBean因为spring boot的默认路径不是"/hystrix.stream",
     * 只要在自己的项目里配置上下面的servlet就可以了
     * @return
     */
    @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;
    }

b. 监控测试
1.启动1个eureka或3个eureka集群均可

2.观察监控窗口
1). 9001监控8001

启动8001服务

2). 测试地址
访问http://localhost:8001/payment/circuit/31
访问http://localhost:8001/payment/circuit/-31
上述测试通过
先访问正确地址,再访问错误地址,再正确地址,会发现图示断路器都是慢慢放开的

a. 监控结果,成功

b. 监控结果,失败

3). 如何看?
a. 7色

b. 1圈

实心圆:共两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色<黄色<橙色<红色递减。
该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例

c. 1线

曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。

d. 整图说明

4). 搞懂一个才能看懂复杂的

2.Gateway新一代网关

2.1.概述简介

2.1.1.官网

1.上一代zuul 1.X
https://github.com/Netflix/zuul/wiki

2.当前gateway
https://docs.spring.io/spring-cloud-gateway/docs/3.0.4-SNAPSHOT/reference/html/

2.1.2.是什么

Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关;但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul,那就是SpringCloud Gateway一句话:gateway是原zuul1.x版的替代

1.概述
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链的方式提供了网关基本的功能,例如:安全、监控/指标,和限流。

2.一句话
1). SpringCloud Gateway使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。
2). 源码架构

2.1.3.能干嘛

1.反向代理
2.鉴权
3.流量控制
4.熔断
5.日志监控

2.1.4.微服务架构中网关在哪里

2.1.5.有Zuul了怎么又出来了gateway

1.我们为什么选择Gateway?
1.1.Netflix不太靠谱,Zuul2.0一直跳票,迟迟不发布

一方面因为Zuul1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来也非常的简单便捷。
Gateway是基于异步非阻塞模型上进行开发的, 性能方面不需要担心。虽然Netflix早就发布了最新的Zuul 2.x,但Spring Cloud貌似没有整合计划。而且Netflix相关组件都宣布进入维护器;不知前景如何?
多方面综合考虑Gateway是很理想的网关选择。

1.2.SpringCloud Gateway具有如下特性

基于SpringFramework 5,Project Reactor和Spring Boot 2.0进行构建
动态路由:能够匹配任何请求属性;
可以对路由指定Predicate(断言)和Filter(过滤器);
集成Hystrix的断路器功能;
集成Spring Cloud服务发现功能;
易于编写的Predicate(断言)和Filter(过滤器);
请求限流功能;
支持路径重写。

1.3.SpringCloud Gateway与Zuul的区别

在SpringCloud Finchley正式版之前,Spring Cloud推荐的网关是Netflix提供的Zuul:
1、Zuul 1.x,是一个基于阻塞I/O的API Gateway
2、Zuul 1.x基于Servlet 2.5使用阻塞架构,它不支持任何长连接(如 WebSocket)Zuul的设计模式和Nginx较像,每次I/O操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul的性能相对较差。
3、Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul 2.x的性能较Zuul 1.x有较大提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway的RPS(每秒请求数)是Zuul的1.6倍。
4、Spring Cloud Gateway建立在Spring Framework 5、Project Reactor和Spring Boot 2之上,使用非阻塞API。
5、Spring Cloud Gateway还支持WebSocket,并且与Spring紧密集成拥有更好的开发体验

2.Zuul1.x模型

SpringCloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。
Servlet的生命周期?servlet由servlet container进行生命周期管理。
container启动时构造servlet对象并调用servlet init()进行初始化;
container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service();
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模型的弊端。

3.Gateway模型
3.1.WebFlux是什么

https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html
说明:

传统的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来实现响应式流规范。

2.2.三大核心概念

2.2.1.Route(路由)

路由是构建网关的基本模块,它由ID,目标ID,一系列的断言和过滤器组成,如果断言为true则匹配该路由

2.2.2.Predicate(断言)

参考的是Java8的java.util.function.Predicate
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由

2.2.3.Filter(过滤)

指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或之后对请求进行修改。

2.2.4.总结

web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件;而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。

2.3.Gateway工作流程

2.3.1.官网总结

客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。
Handler再通过指定的过滤器来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前("pre")或之后("post")执行业务逻辑。
Filter在"pre"类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在"post"类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

2.3.2.核心逻辑

路由转发+执行过滤器链

2.4.入门配置

2.4.1.新建Module

cloud-gateway-gateway9527

2.4.2.POM

引入gateway依赖:

        <!-- gateway -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

完整依赖:

    <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.neo.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>

2.4.3.YML

新建application.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

2.4.4.业务类

2.4.5.主启动类

@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GateWayMain9527.class, args);
    }
}

2.4.6.9527网关如何做路由映射?

1.cloud-provider-payment8001看看controller的访问地址
1.1.get:@GetMapping(value = "/payment/get/{id}")

1.2.lb:@GetMapping(value = "/payment/lb")

2.我们目前不想暴露8001端口,希望在8001外面套一层9527

2.4.7.YML新增网关配置

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
        - id: payment_route   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001    # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**    # 断言,路径相匹配的进行路由
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001    # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**    # 断言,路径相匹配的进行路由

2.4.8.测试

1.启动7001

2.启动8001
cloud-provider-payment8001

3.启动9527网关

4.访问说明

4.1.添加网关前
http://localhost:8001/payment/get/1

4.2.添加网关后
http://localhost:9527/payment/get/1

2.4.9.YML配置说明

1.Gateway网关路由有两种配置方式:
1.1.在配置文件yml中配置
见前面的步骤

1.2.代码中注入RouteLocator的Bean
a. 官网案例
Using Java config:

RemoteAddressResolver resolver = XForwardedRemoteAddressResolver
    .maxTrustedIndex(1);

...

.route("direct-route",
    r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24")
        .uri("https://downstream1")
.route("proxied-route",
    r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24")
        .uri("https://downstream2")
)

b. 百度国内新闻网址
http://news.baidu.com/guonei

c. 自己写一个
1). 业务需求
通过9527网关访问到外网的百度新闻网址

2). 编码
cloud-gateway-gateway9527业务实现
新建GateWayConfig配置类:

@Configuration
public class GateWayConfig {

    /**
     * 配置一个id为route_name的路由规则,
     * 当访问http://localhost:9527/guonei时会自动转发到地址:http:/news.baidu.com/guonei
     * @param routeLocatorBuilder
     * @return
     */
    @Bean
    public RouteLocator customRouteLocater(RouteLocatorBuilder routeLocatorBuilder) {
        return routeLocatorBuilder.routes()
                .route("path_route_neo", r -> r.path("/guonei")
                        .uri("http://news.baidu.com/guonei"))
                .build();
    }

    @Bean
    public RouteLocator customRouteLocater2(RouteLocatorBuilder routeLocatorBuilder) {
        return routeLocatorBuilder.routes()
                .route("path_route_neo2", r -> r.path("/guoji")
                        .uri("http://news.baidu.com/guoji"))
                .build();
    }
}

2.5.通过微服务名实现动态路由

默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能

1.启动
一个eureka7001 + 两个服务提供者8001/8002

2.POM

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

3.YML

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_route   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001    # 匹配后提供服务的路由地址
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**    # 断言,路径相匹配的进行路由
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001    # 匹配后提供服务的路由地址
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**    # 断言,路径相匹配的进行路由

a. 需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能。
b. lb://serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri

4.测试
访问http://localhost:9527/payment/lb,效果:8001/8002两个端口切换

2.6.Predicate的使用

2.6.1.是什么

启动我们的gateway9527

2.6.2.Route Predicate 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。

2.6.3.常用的Route Predicate

1.After Route Predicate

我们的问题是:上述这个After好懂,这个时间串怎么获得?

public class ZonedDateTimeDemo {
    public static void main(String[] args) { 
        // ZonedDateTime zbj = ZonedDateTime.now(ZoneId.from("America/New_York")); // 用指定时区获取当前时间
        ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
        System.out.println(zbj);
    }
}

YML:

- After=2021-08-27T23:39:59.929+08:00[Asia/Shanghai]

2.Before Route Predicate
YML:

spring:
  cloud:
    gateway:
      routes:
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Before=2021-08-27T23:39:59.929+08:00[Asia/Shanghai]  # 断言,路径相匹配的进行路由

3.Between Route Predicate
YML:

spring:
  cloud:
    gateway:
      routes:
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Between=2021-08-27T23:39:59.929+08:00[Asia/Shanghai], 2021-08-27T23:49:59.929+08:00[Asia/Shanghai]  # 断言,路径相匹配的进行路由

4.Cookie Route Predicate

Cookie Route Predicate需要两个参数,一个是Cookie name,一个是正则表达式。
路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行

a. YML

spring:
  cloud:
    gateway:
      routes:
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Cookie=username, zzyy  # 匹配后提供服务的路由地址

b. 不带上cookie访问

curl http://localhost:9527/payment/lb

c. 带上cookies访问

curl http://localhost:9527/payment/lb --cookie "username=zzyy"

5.Header Route Predicate

两个参数:一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。

YML:

spring:
  cloud:
    gateway:
      routes:
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Header=X-Request-Id, \d+  # 请求头要有X-Request-Id属性并且值为整数的正则表达式

带上header访问:

curl http://localhost:9527/payment/lb -H "X-Request-Id:123"

6.Host Route Predicate

Host Route Predicate接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔得模板,用.号作为分隔符。
它通过参数中得主机地址作为匹配规则。

YML:

spring:
  cloud:
    gateway:
      routes:
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Host=**.neo.com

带上host访问:

curl http://localhost:9527/payment/lb -H "Host: www.neo.com"

7.Method Route Predicate

YML:

spring:
  cloud:
    gateway:
      routes:
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Method=GET

带上method访问:

curl http://localhost:9527/payment/lb -X POST

8.Path Route Predicate
YML:

spring:
  cloud:
    gateway:
      routes:
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**  # 断言,路径相匹配的进行路由

9.Query Route Predicate
YML:

spring:
  cloud:
    gateway:
      routes:
        - id: payment_route2   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Query=username, \d+  # 要有参数为username并且值还要是整数才能路由

10.小总结
说白了,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。

2.7.Filter的使用

2.7.1.是什么

路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。
Spring Cloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生

2.7.2.Spring Cloud Gateway的Filter

1.生命周期
1.1.pre
1.2.post

2.种类
2.1.GatewayFilter
单一的有31种:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories

2.2.GlobalFilter
全局的有10种:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#global-filters

2.7.3.常用的GatewayFilter

1.AddRequestParameter
YML:

spring:
  cloud:
    gateway:
      routes:
        - id: payment_route   # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          filters:
            - AddRequestParameter=X-Request-Id, 1024  # 过滤器工厂会在匹配的请求头加上一对请求头,名称为X-Request-Id值为1024

2.7.4.自定义过滤器

1.自定义全局GlobalFilter
1.1.两个主要接口介绍
implements GlobalFilter, Ordered

1.2.能干嘛
全局日志记录、统一网关鉴权

1.3.案例代码
新建MyLogGateWayFilter全局过滤器:

@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ㄒ)/~~");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

1.4.测试
1.4.1.启动

1.4.2.正确
http://localhost:9527/payment/lb?uname=z3

1.4.3.错误
http://localhost:9527/payment/lb

3.SpringCloud Config分布式配置中心

服务配置Config、服务总线Bus并没有说停止更新进入到维护,这两个也还在用,但是,慢慢地被后起之秀Alibaba Nacos所替代。

3.1.概述

3.1.1.分布式系统面临地配置问题

现在父工程下的子工程越来越多了,一、每新建一个工程就会有一个application.yml,配置文件越来越膨胀,二、东西多了就需要有统一的管理,假设现在有A、B、C、D工程都连接同一个数据库,如果没有一个统一的分布式配置中心的话,修改数据库四个微服务需要修改四次,而每个工程的application.yml都是同一份,那么,假设有40个微服务呢?需要修改40次,随着微服务数量的增大,开发工程师就哭了——每天改配置就行了;因此,我们就像网关一样需要有一个同一个的配置:能够一处修改,处处生效!这样,就减轻了我们的配置压力,增强了管理配置方面的功能。
三、我们经常碰到这样的情况:上线以后发版本有开发环境、测试环境、生产环境、灰度发布的预发布环境等,例如:有dev、test、prod三个环境,就有对应的3套配置的管理系统和业务要求,那么一个配置文件不能满足,另外,这些配置文件当中任何一个配错了都给自己找麻烦,东西越来越多,配置的路径越来越多,所以说面临严重的配置问题。

微服务意味着要将单体应用中的业务拆分成一个个自服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个applicaion.yml,上百个配置文件的管理...

3.1.2.是什么

是什么
Spring Cloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置
怎么玩
SpringCloud Config分为服务端和客户端两部分
服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务并为客户端提供获取配置信息,加密/解密信息等访问接口
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器,默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

3.1.3.能干嘛

1.集中管理配置文件
2.不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
3.运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
4.当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
5.将配置信息以REST接口的形式暴露(post、curl访问刷新均可)

3.1.4.与GitHub整合配置

由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式

3.1.5.官网

https://docs.spring.io/spring-cloud-config/docs/current/reference/html/

3.2.Config服务端配置与测试

3.2.1.初始化git项目

用你自己的账户在GitHub上新建一个名为springcloud-config的新Repository
由上一步获得刚新建的git地址:git@github.com:yydsgit/springcloud-config.git
本地硬盘目录上新建git仓库并clone
本地地址:E:\IntelliJ IDEA\Project\

pwd
git clone git@github.com:yydsgit/springcloud-config.git

此时,在本地E盘符下E:\IntelliJ IDEA\Project\springcloud-config

表示多个环境的配置文件
保存格式必须为UTF-8
如果需要需改,此处模拟运维人员操作git和github:
git add .
git commit -m "init yml"
git push orgin master

3.2.2.新建Module模块cloud-config-center-3344

它即为Cloud的配置中心模块CloudConfig Center

3.2.3.POM

        <!-- config-server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>

3.2.4.YML

server:
  port: 3344

spring:
  application:
    name: cloud-config-center # 注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          uri: https://github.com/yydsgit/springcloud-config.git  # GitHub上面的git仓库名字
          # 搜索目录
          search-paths:
            - springcloud-config
      # 读取分支
      label: master

# 服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

3.2.5.主启动类

新建主启动类ConfigCenterMain3344,启用配置中心@EnableConfigServer

@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigCenterMain3344.class, args);
    }
}

3.2.6.windows下修改hosts文件,增加映射

3.2.7.测试通过Config微服务是否可以从GitHub上获取配置内容

1.启动微服务3344

2.http://config-3344.com:3344/master/config-dev.yml

3.2.8.配置读取规则

1.官网

2./{label}/{application}-{profile}.yml
2.1.master分支
http://config-3344.com:3344/master/config-dev.yml
http://config-3344.com:3344/master/config-test.yml
http://config-3344.com:3344/master/config-prod.yml

2.2.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-prod.yml

3./{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-prod.yml
http://config-3344.com:3344/config-xxxx.yml(不存在的配置)

4./{application}/{profile}[/{label}]
http://config-3344.com:3344/config/dev/master
http://config-3344.com:3344/config/test/master
http://config-3344.com:3344/config/prod/master

5.重要配置细节总结
label:分支(branch)
name:服务名
profiles:环境(dev/test/prod)

成功实现了用SpringCloud Config通过GitHub获取配置信息

3.3.Config客户端配置与测试

3.3.1.新建cloud-config-client-3355

3.3.2.POM

        <!-- config-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

3.3.3.bootstrap.yml

3.3.3.1.是什么

application.yml是用户级的资源配置项
bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个“Boostrap Context”,作为Spring应用的Application Context父上下文。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment
Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。Bootstrap ContextApplication Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap ContextApplication Context配置的分离。
要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的,因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml。

3.3.3.2.内容
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  # 配置中心地址

# 服务注册到eureka地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

说明:

3.3.4.主启动

新建主启动类ConfigClientMain3355:

@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3355 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3355.class, args);
    }
}

3.3.5.业务类

将配置信息以REST接口的形式暴露:

@RestController
public class ConfigClientController {
    
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    public String getConfigInfo() {
        return configInfo;
    }
}

3.3.6.测试

1.启动Config配置中心3344微服务并自测
a.http://config-3344.com:3344/master/config-dev.yml
b.http://config-3344.com:3344/master/config-test.yml

2.启动3355作为Client准备访问
http://localhost:3355/configInfo

修改客户端3355配置文件label为dev,profile为test,热部署后也成功访问dev分支上config-test.yml的配置文件:

成功实现了客户端3355访问SpringCloud Config3344通过GitHub获取配置信息

3.3.7.问题随时而来,分布式配置的动态刷新问题

修改config-dev.yml配置并提交到GitHub中,比如加个变量age或者版本号version

1.Linux运维修改GitHub上的配置文件

2.刷新3344,发现ConfigServer配置中心立刻响应

3.刷新3355,发现ConfigClient客户端没有任何响应

4.3355没有变化除非自己重新或者重新加载
5.难道每次运维修改配置文件,客户端都需要重启?

3.4.Config客户端之动态刷新

避免每次更新配置都要重启客户端微服务3355

3.4.1.动态刷新

修改3355模块

3.4.1.1POM引入actuator模块
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
3.4.1.2.修改YML,暴露监控端口
# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"
3.4.1.3.@RefreshScope业务类Controller修改

3.4.1.4.修改GitHub

修改config-dev.yml配置version由2改为3,并提交到GitHub

1.访问服务端3344
访问http://config-3344.com:3344/master/config-dev.yml,服务端立刻生效:

2.访问客户端3355
访问http://localhost:3355/configInfo,客户端配置还是没有立即生效:

3.4.1.5.需要运维人员发送Post请求刷新3355

1.必须是POST请求
2.curl -X POST "http://localhost:3355/actuator/refresh"

3.测试
访问http://localhost:3355/configInfo,成功实现了客户端3355刷新到最新配置内容,避免了服务重启:

3.4.2.想想还有什么问题

假设有多个微服务客户端3355/3366/3377...,每个微服务都要执行一次post请求,手动刷新?
可否广播,一次通知,处处生效?大范围的自动刷新?

假如有100台机器,不见得这100台机器的配置都这么统一和顺利,服务端配置改变了,100台机器都要改?有没有可能需要精确打击:100台机器只需通知98台刷新配置,另外2台因为版本发包的问题不需要换成最新的配置,我们要求该通知的通知,不该通知的不被通知,进行定点通知、定点清除
所以说遇到这种功能,现有的发POST请求actuator/refresh手动刷新可能就有点力不从心,想:①广播通知,②避免每次要执行一次POST,③与广播通知分开进行精确打击、精准通知等一系列的功能,目前我们做不到,所以引入下一章:消息总线。

4.SpringCloud Bus消息总线

4.1.概述

4.1.1.上一讲的加深和扩充,一言以蔽之

1.分布式自动刷新配置功能
2.Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态刷新。

4.1.2.是什么

Spring Cloud Bus配合Spring Cloud Config 使用可以是实现配置的动态刷新。

1.Bus支持两种消息代理:RabbitMQ和Kafka

4.1.3.能干嘛

Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。

4.1.4.为何被称为总线

什么是总线
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其它连接在该主题上的实例都知道的消息。

基本原理
ConfigClient实例能监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。

4.2.RabbitMQ环境配置

  • 安装 Erlang
  • 安装 RabbitMQ:
    以 windowns 版本为例,下载安装后,进入sbin目录输入以下命令启动管理功能

浏览器输入http://localhost:15672/ 进入管理页面(用户名密码默认都是 guest)

4.3.SpringCloud Bus动态刷新全局广播

1.必须先具备良好的RabbitMQ环境
2.演示广播效果,增加复杂度,再以3355为模板再制作一个3366
2.1). 新建cloud-config-client-3366

2.2). POM

    <dependencies>
        <!-- config-client-->
        <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>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.neo.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </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>

2.3). YML
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: "*"

2.4). 主启动

@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3366 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3366.class, args);
    }
}

2.5). controller

@RestController
@RefreshScope
public class ConfigClientController {

    @Value("${server.port}")
    private String serverPort;

    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    public String getConfigInfo() {
        return "serverPort: " + serverPort + "\t\n\n configInfo: " + configInfo;
    }
}

3.设计思路
3.1). 利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置
3.2). 利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置

我们采用第二种,第一种方式不适合的原因有三:
a. 打破了微服务的职责单一性。负责业务模块的微服务不应该承担配置刷新的职责
b. 破坏了微服务各节点的对等性
c. 有一定的局限性。例如微服务迁移时,它的网络地址常常发生变化,如果想要做到自动刷新,还需要增加更多的配置

4.动态刷新全局广播配置
a. 在配置中心微服务3344以及所有需要接收消息的客户端3355\3366中导入 Maven 依赖

<!-- 添加消息总线RabbitMQ支持 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

b. 添加对应配置,使其支持消息总线

########添加在配置中心3344、所有需要接收消息的客户端3355、3366中#########

spring:
  # rabbitmq相关配置,15672是web管理界面的端口,5672是mq访问的接口
  rabbitmq:
    host: 192.168.200.128
    port: 5672
    username: admin
    password: 123

#############添加在配置中心微服务3344中################

# rabbitmq相关配置,暴露bus刷新配置的端点
management:
  endpoints:
    web:
      exposure:
        include: 'bus-refresh'

c. 测试,修改 github 中的配置,使用 curl 发送请求curl -X POST http://localhost:3344/actuator/bus-refresh后,刷新每个微服务的页面发现都已被修改。实现了一次修改,广播通知,处处生效!

4.4.SpringCloud Bus动态刷新定点通知

如果不想全部通知,只想定点通知,上述的配置都不用变,只需要发送请求的时候在后面指定「微服名:端口」
curl -X POST http://localhost:3344/actuator/bus-refresh/{destination}

以我的为例就是:
curl -X POST http://localhost:3344/actuator/bus-refresh/config-client:3355

流程图:

1、配置中心微服务通过远程库获取配置信息,同时订阅 RabbitMQ 主题
2、客户端通过配置中心获取配置信息,同时订阅 RabbitMQ 主题
3、当我们修改远程库的配置后
4、发送 POST 请求
5、配置中心向 RabbitMQ 发送刷新事件
6、客户端监听到刷新事件
7、从配置中心拉取新的配置

5.SpringCloud Stream消息驱动

5.1.消息驱动概述

5.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。

屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型

官网
1.https://spring.io/projects/spring-cloud-stream#overview

Spring Cloud Stream是用于构建与共享消息传递系统连接的高度可伸缩的事件驱动微服务框架,该框架提供了一个灵活的编程模型,它建立在已经建立和熟悉的Spring熟语和最佳实践上,包括支持持久化的发布/订阅、消费组以及消息分区三个核心概念

2.https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/
3.Spring Cloud Stream中文指导手册:https://m.wang1314.com/doc/webapp/topic/20971999.html

5.1.2.设计思想

1.标准MQ

1.1.生产者/消费者之间靠消息(Message)媒介传递信息内容
1.2.消息必须走特定的通道(MessageChannel)
1.3.消息通道里的消息如何被消费呢,谁负责收发处理
消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅

2.为什么用Cloud Stream

比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同。
像RabbitMQ有exchange,kafka有Topic和Partitions分区
这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候SpringCloud Stream给我们提供了一种解耦合的方式。

2.1.Stream为什么可以统一底层差异?

在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性;通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。

2.2.Binder

Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度耦合,服务可以关注更多自己的业务流程。

INPUT对应于消费者,OUTPUT对应于生产者

3.Stream中的消息通信方式遵循了发布-订阅模式
Topic主题进行广播
a.在RabbitMQ中就是Exchange
b.在Kafka中就是Topic

5.1.3.Spring Cloud Stream标准流程套路

  • Binder:很方便的连接中间件,屏蔽差异。
  • Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过对Channel实现对队列进行配置。
  • Source和Sink:简单可以理解为参照对象就是Spring Cloud Stream本身,从Spring Cloud Stream发布消息就是输出,从Spring Cloud Stream接受消息就是输入。

5.1.4.编码API和常用注解

5.2.案例说明

1.RabbitMQ环境已经准备

2.工程中新建三个子模块

  • cloud-stream-rabbitmq-provider8801,作为生产者进行发消息模块
  • cloud-stream-rabbitmq-provider8802,作为消息接收模块
  • cloud-stream-rabbitmq-provider8803,作为消息接收模块

5.3.消息驱动之生产者

5.3.1.新建Module

5.3.2.POM

增加部分:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>

5.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.200.128
                port: 5672
                username: admin
                password: 123
      bindings: # 服务的整合处理
        output: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次是json,本文则设置“text/plain”
          binder: defaultRabbit # 设置要绑定的具体设置

# 服务注册到eureka地址
eureka:
  client:
    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地址

5.3.4.主启动类StreamMQMain8801

@SpringBootApplication
public class StreamMQMain8801 {

    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8801.class, args);
    }
}

5.3.5.业务类

a. 发送消息接口

public interface IMessageProvider {
    public String send();
}

b. 发送消息接口实现类

@EnableBinding(Source.class) // 定义消息的推送管道
public class MessageProviderImpl implements IMessageProvider {

    @Resource
    private MessageChannel output; // 消息发送管道

    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();
        System.out.println(serial + "======");
        output.send(MessageBuilder.withPayload(serial).build());
        return serial;
    }
}

c. Controller

@RestController
public class SendMessageController {

    @Resource
    private IMessageProvider messageProvider;

    @GetMapping(value = "/sendMessage")
    public String sendMessage() {
        return messageProvider.send();
    }
}

5.3.5.测试

a. 启动7001Eureka
b. 启动RabbitMQ
c. 启动8801

d. 访问
http://localhost:8801/sendMessage

5.4.消息驱动之消费者

5.4.1.新建Module

5.4.2.POM

增加部分:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>

5.4.3.YML

server:
  port: 8802

spring:
  application:
    name: cloud-stream-consumer
  cloud:
    stream:
      binders: # 在此处配置要绑定的rabbitmq的服务信息
        defaultRabbit: # 表示定义的名称,用于binding整合
          type: rabbit # 消息组件类型
          environment: # 设置rabbitmq的相关的环境配置
            spring:
              rabbitmq:
                host: 192.168.200.128
                port: 5672
                username: admin
                password: 123
      bindings: # 服务的整合处理
        input: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次是json,本文则设置“text/plain”
          binder: defaultRabbit # 设置要绑定的具体设置

# 服务注册到eureka地址
eureka:
  client:
    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地址

5.4.4.主启动类StreamMQMain8802

@SpringBootApplication
public class StreamMQMain8802 {

    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8802.class, args);
    }
}

5.4.5.业务类

@RestController
@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 serverPort:" + serverPort);
    }
}

5.4.6.测试8801发送8802接收消息

http://localhost:8801/sendMessage

5.5.分组消费与持久化

5.5.1.依照8802,clone出来一份运行8803

5.5.2.启动

a. RabbitMQ
b. 7001服务注册
c. 8801消息生产
d. 8802消息消费
f. 8803消息消费

5.5.3.运行后有两个问题

a. 有重复消费问题
b. 消息持久化问题

5.5.4.消费

目前是8802、8803同时都收到了消息,存在重复消费问题
a. http://localhost:8801/sendMessage

b. 如何解决

分组和持久化属性group

c. 生产实际案例

比如在如下场景中,订单系统我们做继承部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中得消息分组来解决

注意在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次,不同组是可以全面消费的(重复消费)。


故障现象:重复消费
导致原因:默认分组group是不同的,组流水号不一样,被认为不同组,可以消费

5.5.5.分组

解决方案:自定义配置分组(自定义配置分为同一个组,解决重复消费问题)

a. 原理

微服务应用放置于同一个group中,就能保证消息只会被其中一个应用消费一次。
不同的组是可以消费的,同一个组内会发送竞争关系,只有其中一个可以消费。

b. 8802/8803都变成不同组,group两个不同
1). group: bingfengA、bingfengB
2). 8802修改YML

3). 8803修改YML

4). 配置


分布式微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例,本例启动了两个消费微服务(8802、8803)
多数情况,生产者发送消息给某个具体微服务时只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在Spring Cloud Stream中提供了消费组的概念。

5). 结论
存在重复消费问题

c. 8802/8803实现了轮询分组,每次只有一个消费者(8801模块发的消息只能被8802或8803其中一个接收到,这样避免了重复消费)
d. 8802/8803都变成相同组,group两个相同
1). group: bingfengA
2). 8802修改YML

3). 8803修改YML

4). 结论
同一个组的多个微服务实例,每次只会有一个拿到消息

5.5.6.持久化

通过上述,解决了重复消费问题,再看看持久化,停止8802、8803并去掉8802的分组group:bingfengA(8803的分组group:bingfengA没有去掉),8801先发送4条消息到RabbitMQ

先启动8802(无分组属性配置),后台没有打出来消息
再启动8803(有分组属性配置),后台打出来了MQ上的消息

6.SpringCloud Sleuth分布式请求链路跟踪

6.1.概述

6.1.1.为什么会出现这个技术?需要解决哪些问题?

在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个前端请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。

6.1.2.是什么

https://github.com/spring-cloud/spring-cloud-sleuth
Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案
在分布式系统中提供追踪解决方案并且兼容支持了zipkin

6.1.3.解决

6.2.搭建链路监控步骤

6.2.1.zipkin

a. 下载

SpringCloud从F版已不需要自己构建Zipkin Server了,只需调用jar包即可
https://repo1.maven.org/maven2/io/zipkin/zipkin-server/2.23.4/zipkin-server-2.23.4-exec.jar

b. 运行jar

java -jar zipkin-server-2.23.4-exec.jar

c. 运行控制台

http://localhost:9411/zipkin/
术语
1.完整的调用链路
表示一请求链路,一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各Span通过parent id关联起来

一条链路通过Trace Id唯一标识,Span表示发起的请求信息,各span通过parent id关联起来

整个链路的依赖关系如下:

2.名词解释
Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识
Span:表示调用链路来源,通俗的理解Span就是一次请求信息

6.2.2.服务提供者

修改cloud-provider-payment8001
a. POM
增加的部分:

        <!-- 包含了sleuth+zipkin -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

b. YML
增加的部分:

spring:
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      # 采样率值介于0到1之间,1则表示全部采集
      probability: 1

c. 业务类PaymentController
添加:

    @GetMapping("/payment/zipkin")
    public String paymentZipkin() {
        return "hi, i'am paymentzipkin server fall back, welcome to bingfeng, O(∩_∩)O哈哈~";
    }

6.2.3.服务消费者(调用方)

修改cloud-consumer-order80
a. POM
增加的部分:

        <!-- 包含了sleuth+zipkin -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

b. YML
增加的部分:

spring:
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      # 采样率值介于0到1之间,1则表示全部采集
      probability: 1

c. 业务类OrderController
添加:

    // zipkin+sleuth
    @GetMapping("/consumer/payment/zipkin")
    public String paymentZipkin() {
        String result = restTemplate.getForObject("http://localhost:8001" + "/payment/zipkin/", String.class);
        return result;
    }

6.2.4.依次运行eureka7001、8001、80

80调用8001几次测试下

6.2.5.打开浏览器访问:http://locahost:9411

a. 会出现以下界面

查看

b. 查看依赖关系

c. 原理

posted @ 2021-08-14 12:08  冰枫丶  阅读(115)  评论(0编辑  收藏  举报