spring cloud下

六、服务网关

概述

  1. 服务网关进行日志、限流、权限、安全等等操作。

zuul

  1. springcloud中集成的zuul版本,采用的是tomcat容器,使用的是传统的servlet io处理模型。

  2. servlet是tomcat的核心组件之一,另外两个组件是监听器和过滤器。

  3. servlet是由servlet container进行生命周期管理

  4. servlet的生命周期:

    • container启动时构造servlet对象并调用**servlet init()**进行初始化
    • container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程),然后调用service()
    • container关闭时调用**servlet destroy()**消费servlet。
  5. servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下,这种模型是适用的,但是一旦高并发,线程数量就会上涨,而线程资源代价是昂贵的(上下文切换造成内存消耗大),严重影响请求的处理时间。在一些简单业务场景下,不希望为每个请求分配一个线程,只需要1个或几个线程就能应对极大并发请求,这种业务场景下servlet模型没有优势。

    所以Zuul 1.X是基于servlet的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet),是阻塞式处理模型。

  6. 传统的Web框架,比如说strust2,springmvc等都是基于Servlet API与servlet容器基础之上运行的。

    在servlet3.1之后有了异步非阻塞的支持,而webflux是一个典型异步非阻塞的框架,他的核心是基于Reactor的相关API实现的。

    Spring WebFlux是spring5.0引入的新的响应式框架,区别于Spring MVC,它不需要依赖于Servlet API,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。

gateway

概述

  1. springcloud gateway是springcloud生态系统中的网关,目标是替代zuul。

  2. springcloud gateway是基于WebFlux框架实现的,WebFlux框架是非阻塞式的web框架,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

  3. Springcloud gateway的目标是提供统一的路由方式,且基于Filter链的方式提供了网关基本的功能,如安全、监控和限流。

    image-20220412183425220

  4. 可以理解为网关是所有微服务的入口。

  5. gateway是基于异步非阻塞模型开发的。

特性

  1. 动态路由:能够匹配任何请求属性
  2. 可以对路由指定Predicate(断言)和Filter(过滤器)
  3. 集成Hystrix的断路器功能
  4. 集成Springcloud服务发现功能
  5. 请求限流功能
  6. 支持路径重写

三大核心概念

  1. 路由(Route)

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

  2. 断言(Predicate)

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

    predicate就可以理解为匹配条件,定位到真正的服务节点。

    Springcloud gateway包括许多内置的RoutePredicateFactories,所有这些Predicate都与Http请求的不同属性匹配,多个Predicate工厂可以进行组合。

  3. 过滤器(Filter)

    使用过滤器,可以在请求被路由前或者后对请求进行修改。

gateway工作流程

  1. 客户端向spring cloud gateway发出请求,然后在gateway handler mapping中找到与请求相匹配的路由,将其发送到gateway web handler

    handler再通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑。

    过滤器可能会在发送代理请求之前或之后执行过滤器逻辑。

    “pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。

    “post”类型的过滤器中可以做响应内容、响应头的修改,日志输出,流量监控等。

网关配置

在application.yml中配置
  1. 建module

  2. 修改pom

    要注意网关作为一种微服务,也要注册进注册中心

    <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <!--引入公共实体类项目的jar包坐标-->
            <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>
    

    注意,gateway网关微服务,不要引入以下jar包,不需要

            <!--子类没有写版本号的,都继承了父类的版本号-->
            <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>
    

    不然启动会报错

    image-20220412224900162

  3. 修改application.yml

    server:
      port: 9527
    
    spring:
      application:
        name: cloud-gateway
    
    eureka:
      instance:
        hostname: cloud-gateway-service
      client:
        ## 表示是否将自己注册进eureka server
        register-with-eureka: true
        ## 表示是否从eureka server抓取已有的注册信息,默认为true
        ## 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
        fetchRegistry: true
        service-url:
          defaultZone: http://eureka7001.com:7001/eureka
    
  4. 主启动类

    @SpringBootApplication
    @EnableEurekaClient
    public class GatewayMain9527 {
        public static void main(String[] args) {
            SpringApplication.run(GatewayMain9527.class, args);
        }
    }
    
  5. 配置好网关之后,如何做路由映射?

    修改yml配置文件如下:

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

    做了以上配置,9527就挡在了8001的前面。

  6. 添加网关前:http://localhost:8001/payment/get/31

    添加网关后:http://localhost:9527/payment/get/31

    添加网关后,就通过网关作为微服务的入口,调用微服务。

代码中注入RouteLocator的Bean
  1. 添加配置类GateWayConfig

    @Configuration
    public class GateWayConfig {
        @Bean
        public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
            RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
            // 访问9527服务的path为guonei,则会转发到"http://news.baidu.com/guonei"这个url
            routes.route("path_route_atguigu1",
                    r -> r.path("/guonei")
                            .uri("http://news.baidu.com/guonei")).build();
            return routes.build();
        }
    }
    

动态路由

  1. 以前服务消费者直接调用服务提供者,我们通过Ribbon + RestTemplate来进行服务调用并实现负载均衡。或者通过OpenFeign来进行服务调用,Feign内置了Ribbon。

    现在在服务提供者前面,有一个网关,我们通过网关,来进行服务调用,通过网关来作为微服务调用的入口,而网关的端口一般是9527,所以服务消费者访问的是9527端口,那么需要通过网关来做负载均衡。

    服务的提供者,也就是微服务可以部署在多台服务器上,避免单点故障,都是分布式的部署方式。我们只需要找网关,由网关来做负载均衡,消费者只认挡在微服务前面的9527。

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

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

    修改gateway9527微服务的application.yml如下

    server:
      port: 9527
    
    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true #开启从注册中心获取注册服务列表,根据服务名创建动态路由的功能,利用微服务名进行路由
          routes:
            - id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
              uri: lb://cloud-payment-service #匹配后提供服务的路由地址
    ##          uri: http://localhost:8001 #匹配后提供服务的路由地址
              predicates:
                - Path=/payment/get/** #断言,路径相匹配的进行路由
            - id: payment_routh2
              uri: lb://cloud-payment-service #匹配后提供服务的路由地址
    ##          uri: http://localhost:8001
              predicates:
                - Path=/payment/lb/**
    
    eureka:
      instance:
        hostname: cloud-gateway-service
      client:
        ## 表示是否将自己注册进eureka server
        register-with-eureka: true
        ## 表示是否从eureka server抓取已有的注册信息,默认为true
        ## 单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
        fetchRegistry: true
        service-url:
          defaultZone: http://eureka7001.com:7001/eureka
    

    把以前写死的uri地址,换成lb+“微服务名称”

    uri: lb://cloud-payment-service

  3. 测试

    image-20220413155206740

Predicate

  1. Springcloud gateway包括许多内置的RoutePredicateFactories,所有这些Predicate都与Http请求的不同属性匹配,多个Predicate工厂可以进行组合。

    可以把Route Predicate理解为路由匹配。多个路由匹配相组合。不同的路由匹配对应于Http请求的不同属性。

    Springcloud gateway创建动态路由时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route。

Filter

概述
  1. 过滤器可用于修改http请求和返回的http响应。

  2. Springcloud gateway内置了多种路由过滤器对象,他们都由GatewayFilter工厂来生产。

  3. 使用方式:

    和predicate路由匹配一样,在yml配置文件中进行修改

    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true #开启从注册中心获取注册服务列表,根据服务名动态创建路由的功能,利用微服务名进行路由
          routes:
            - id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
              uri: lb://cloud-payment-service #匹配后提供服务的路由地址
    ##          uri: http://localhost:8001 #匹配后提供服务的路由地址
              filter:
                - AddRequestParameter=X-Request-Id,1024 #过滤器会在匹配的请求头加上一个键值对参数,名称为X-Request-Id,值为1024
              predicates:
                - Path=/payment/get/** #断言,路径相匹配的进行路由
    
自定义过滤器
  1. 实际工作中,用自定义过滤器多一些。

  2. 自定义全局GlobalFilter,实现两个接口--GlobalFilter,Ordered。

    image-20220413172539581

    @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,非法用户");
                exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
                return exchange.getResponse().setComplete();
            }
            //通过之后,去下一个过滤器
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            return 0;
        }
    }
    

    exchange当作是request、response。

    chain是过滤器链

    image-20220413173122373

    如果request不带uname参数,那么就是非法用户。

    image-20220413173159980

    2022-04-13 17:31:52.504  INFO 2180 --- [ctor-http-nio-2] c.a.s.filter.MyLogGatewayFilter          : ****用户名为null,非法用户
    

    注意:因为这是get请求,所以在浏览器的地址栏中能直接这样写,能直接通过浏览器地址栏去发送请求,那么是get请求,post请求不能通过这种方式去发送。

七、服务配置

概述

  1. alibaba的nacos能做服务注册中心、服务配置、服务总线,所以到后面统一整理。

  2. 在实际生产中有很多微服务,我们要将他们的配置进行统一管理,因为可能他们用到的数据库、注册中心等配置是一样的,所以要统一管理,一处修改,处处生效。

  3. 实际开发中,有生产环境、测试环境、开发环境。

  4. 微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息,所以一套集中式的、动态的配置管理设施是必不可少的。

    问题:每一个微服务自己带着一个application.yml,实际开发中存在上百个文件的配置管理。

    Springcloud提供了Config Server来解决这个问题。

Config Server

概述

  1. springcloud config为微服务架构中的微服务提供集中化的外部配置支持,config server为各个不同微服务应用的所有环境提供了一个中心化的外部配置。

    公有的配置放在config server里,私有的配置,各微服务自己配置。

  2. springcloud config分为服务端和客户端两部分。

    服务端也就是分布式配置中心,它是一个独立的微服务应用,为客户端提供配置信息。

    客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。

    配置中心默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便地管理和访问配置内容。

作用

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

使用springcloud config

配置中心(服务端)

  1. 新建一个本地仓库项目,取名为springcloud-config,和git远程仓库相连,用来存储配置。

    在此项目中,修改配置,并推送至远程仓库,实现配置的版本控制。

  2. 新建module:cloud-config-center-3344

  3. 修改pom文件

        <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>
    
  4. 修改application.yml

    server:
      port: 3344
    
    spring:
      application:
        name: cloud-config-center #注册进eureka注册中心的服务名
      cloud:
        config:
          server:
            git:
              uri: https://github.com/shaileneF/springcloud-config.git
              ##搜索目录
              search-paths:
                - springcloud-config
              username: xxx
              password: xxx
          #### 读取分支
          label: main
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:7001/eureka
    
  5. 主启动类

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

客户端

  1. 新建module

  2. 修改pom

        <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>
    
  3. 新建bootstrap.yml

    application.yml是用户级的资源配置项

    bootstrap.yml是系统级别的,优先级更高

    boostrap属于高优先级,相当于客户端微服务通过bootstrap.xml读取共同的外部配置文件,application.yml则是自己私有的配置文件。

    server:
      port: 3355
    
    spring:
      application:
        name: config-client #注册进eureka注册中心的服务名
      cloud:
        config:
          #### 读取分支
          label: main #分支名称
          name: config #配置文件名称
          profile: dev #读取后缀名称
          uri: http://localhost:3344 #配置中心地址
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:7001/eureka
    
  4. 主启动类

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

    @RestController
    public class ConfigClientController {
        @Value("${config.info}")
        private String configInfo;
    
        @GetMapping("/configInfo")
        public String getConfigInfo() {
            return configInfo;
        }
    
    }
    
  6. 测试

    image-20220413223635965

问题

  1. 修改github上的配置文件内容,config server配置中心立刻响应,config client客户端没有任何响应,难道每次修改github上的配置文件,客户端都要重启?

解决

  1. 修改config client

  2. 修改pom文件

    添加以下两个依赖

            <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>
    
  3. 修改bootstrap.yml文件,暴露监控端点

    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
  4. Controller类添加此注解

    @RefreshScope

  5. 运维发送post请求刷新config client

    curl -X POST "http://localhost:3355/actuator/refresh"
    

八、服务总线

概述

  1. 什么是总线

    在微服务架构的系统中,通常会使用轻量级的消息代理,来构建一个共用的消息主题,并让系统中所有微服务实例都**连接(订阅)**上来。由于该主题中产生的消息会被所有订阅的微服务实例监听和消费,所以称它为消息总线。

  2. ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus),当一个服务刷新数据的时候,它会把这个消息生产到topic中,这样其他订阅此topic的微服务就能得到通知,然后去更新自己的配置。

Spring cloud Bus

概述

  1. bus其实就是消息中间件,目前支持RabbitMQ、kafka

    是用来将分布式系统的节点(各个微服务)与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。

  2. springcloud config配合springcloud bus可以实现配置的动态刷新。

  3. springcloud bus能管理和传播分布式系统间的消息,可以当作微服务间的通信通道。

通过bus实现全局广播

  1. 再新建一个config客户端

    用一个config center和两个config 客户端做测试,通过bus实现全局广播通知

  2. pom文件、yml文件、主启动类和之前一样写就可以了,没有什么变化。

  3. controller层的写法发生一点改变,添加了端口,因为此时要启动多个config client。

    public class ConfigClientController {
        @Value("${config.info}")
        private String configInfo;
    
        @Value("${server.port}")
        private String serverPort;
    
        @GetMapping("/configInfo")
        public String getConfigInfo() {
            return "serverport:" + serverPort + "\t" + "configInfo:" + configInfo;
        }
    }
    

设计思想

  1. 利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置
  2. 利用消息总线触发一个服务端ConfigServer的/bus/refresh,而刷新所有客户端的配置。

第二点的架构显然更加合理。

第一点不合适的原因如下:

  1. 打破了微服务的职责单一性,客户端本来就是干活的,是业务模块,它本不应该承担配置刷新的职责。
  2. 破坏了微服务各节点的对等性
  3. 有一定的局限性, 例如微服务在迁移时,它的网络地址通常会发生变化,如果想通过客户端来做到自动刷新,那就会增加更多的修改,这样不合理。

我们用的是第二种设计思想,由总控,由configServer来通知、来广播其他config client。

设计实现

cloud-config-center-3344配置中心服务端
  1. 给cloud-config-center-3344配置中心服务端添加消息总线支持

    pom文件引入新的依赖

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

    ## rabbitmq相关配置
    rabbitmq:
      host: 阿里云
      port: 5672
      username: admin
      password: 123
    #暴露bus刷新配置的端点
    management:
      endpoints:
        web:
          exposure:
            include: 'bus-refresh'
    
config client
  1. pom文件

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

    ## rabbitmq相关配置
    rabbitmq:
      host: 42.192.182.4
      port: 5672
      username: admin
      password: 123
    
测试
  1. 修改配置

  2. 执行

    curl -X POST "http://localhost:3344/actuator/bus-refresh"
    

    实现一次修改,一次发送,处处生效。

    注意发送这个post请求,是对config server,对配置中心发送,不是对客户端微服务。

    所以只需要发送一次,只刷一个config server,所有config client都被广播到,所有config client的配置都更新了。

  3. 效果

    执行了上面那条指令之后,也就是刷新了3344config server的配置之后,不用重启项目,不用重启config server,也不用重启两个config client,分别刷新以下三个浏览器界面,可以看到配置更新

    image-20220422151353040

    image-20220422151406381

    image-20220422151416281

  4. 我们是通过rabbitmq来实现广播通知的,原理就是客户端监听mq中的这个队列

    image-20220422151957201

动态刷新定点通知

  1. 比如只通知3355,不通知3366

  2. 指令

    curl -X POST "http://localhost:配置中心的端口号/actuator/bus-refresh/{destination}"
    

    /bus/refresh请求不再发送到具体的服务实例上,而是发送给config server,并通过destination参数类指定需要更新配置的服务

    destination:

    spring.application.name:端口号
    

九、消息驱动

spring cloud stream

  1. 为什么引入stream?

    在一个完整的项目中,Java后端使用的mq和大数据部分使用的mq可能不是同一个mq,可能存在两种mq,那么就涉及到mq的切换、维护等。

    引入stream,让我们不再关注具体MQ的细节,我们只需要用一种适配绑定的方式,自动在各种MQ内切换。

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

    应用程序通过inputs或者outputs来与spring cloud stream中的binder对象交互

    通过我们配置来binding,而spring cloud stream的binder对象负责与消息中间件交互

设计思想

  1. 比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同

    像rabbitmq有exchange,kafka有topic和partitions分区的概念

    现在用stream,来实现沟通,屏蔽差异,通过destination binder。

  2. 如果我们先用了一种消息队列,后面的业务需求想往另一种消息队列进行迁移,那么这是灾难性的,因为原先的消息队列和我们的系统是耦合的,这时候springcloud stream给我们提供了一种解耦合的方式。

  3. 在没有绑定器这个概念的情况下,我们的springboot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,他们的实现细节会有较大的差异,通过定义绑定器binder作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。

    通过向应用程序暴露统一的Channel通道,使得应用程序不再需要考虑各种不同的消息中间件实现

  4. 通过定义绑定器binder作为中间层,实现了应用程序与消息中间件细节的隔离。

    image-20220422164048905

    相当于消息队列中间件被绑定器挡在后面。

    应用程序只需要和绑定器做交互。

  5. stream中的消息通信方式遵循了发布-订阅模式

标准流程套路及注解

  1. image-20220422173458808

    binding很方便的连接消息中间件,屏蔽消息中间件的差异

    channel是信道,在消息通讯系统中就是实现存储和转发的媒介,通过channel对队列进行配置。

    source和sink,简单地可以理解为输入输出,从stream发布消息就是输出,接收消息就是输入。

  2. 常用API和注解

    image-20220422174318322

案例说明

新建三个子module

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

生产者

  • pom

    <dependencies>
        <!--stream-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</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>
    
  • application.yml

    server:
      port: 8801
    
    spring:
      application:
        name: cloud-stream-provider
      rabbitmq:
        host: 阿里云
        port: 5672
        username: admin
        password: 123
        virtual-host: /
      cloud:
        stream:
          binders: #在此处配置要绑定的rabbitmq的服务信息
            defaultRabbit: #表示定义的名称,用于binding整合
              type: rabbit ## 消息组件类型
          bindings: ## 服务的绑定,整合
            output: #表示这是服务的生产者
              destination: studyExchange #表示要使用的Exchange名称
              content-type: application/json #设置消息类型,本次为json,文本则设置“text/plain”
              binder: defaultRabbit
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:7001/eureka
      instance:
        lease-renewal-interval-in-seconds: 2 #设置心跳的时间间隔(默认是30秒)
        lease-expiration-duration-in-seconds: 5
        instance-id: send-8801.com #在信息列表显示主机名称
        prefer-ip-address: true #访问的路径变为ip地址
    
  • 主启动类

    @SpringBootApplication
    public class StreamMQMain8801 {
        public static void main(String[] args) {
            SpringApplication.run(StreamMQMain8801.class, args);
        }
    }
    
  • 业务类

    发送消息接口

    public interface IMessageProvider {
        String send();
    }
    
  • 发送消息接口实现类

    @EnableBinding(Source.class) //通过此注解定义消息生产者的发送管道(是源,source),channel和exchange绑定
    public class IMessageProviderImpl 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;
        }
    }
    
  • Controller

    @RestController
    public class SendMessageController {
        @Resource
        private IMessageProvider iMessageProvider;
    
        @GetMapping(value = "/sendMessage")
        public String sendMessage() {
            return iMessageProvider.send();
        }
    }
    

消费者

  • pom文件和生产者一样

  • application.yml

    server:
      port: 8802
    
    spring:
      application:
        name: cloud-stream-consumer
      rabbitmq:
        host: 阿里云
        port: 5672
        username: admin
        password: 123
        virtual-host: /
      cloud:
        stream:
          binders: #在此处配置要绑定的rabbitmq的服务信息
            defaultRabbit: #表示定义的名称,用于binding整合
              type: rabbit ## 消息组件类型
          bindings: ## 服务的绑定,整合
            input: #表示这是服务的消费者
              destination: studyExchange #表示要使用的Exchange名称
              content-type: application/json #设置消息类型,本次为json,文本则设置“text/plain”
              binder: defaultRabbit
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:7001/eureka
      instance:
        lease-renewal-interval-in-seconds: 2 #设置心跳的时间间隔(默认是30秒)
        lease-expiration-duration-in-seconds: 5
        instance-id: receive-8801.com #在信息列表显示主机名称
        prefer-ip-address: true #访问的路径变为ip地址
    
  • 主启动类

    @SpringBootApplication
    public class StreamMQMain8802 {
        public static void main(String[] args) {
            SpringApplication.run(StreamMQMain8802.class, args);
        }
    }
    
  • 消费者业务类

    由于这是微服务,这是消费者,是消费消息的,就不写什么service业务层了,只写controller层来测试一下(但是要注意在实际开发中,一个微服务可能既是服务提供者,也是服务消费者,同时针对于消息队列,一个微服务可能既是消息生产者,也是消息的消费者。)

    @RestController
    @Slf4j
    @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);
        }
    }
    

    要注意仍然用到@EnableBinding这个注解,只不过括号里的是消费端的Sink.class

  • 总结:

    消费者端用以下两个注解:

    • @EnableBinding(Sink.class)
    • @StreamListener(Sink.INPUT)

    生产者用以下注解:

    • @EnableBinding(Source.class)

分组消费

  1. 再写一个消费者module,端口号8803

  2. 运行后有两个问题:

    • 重复消费

      生产者发送消息,目前是8802和8803都收到了,存在重复消费问题。

      如果一个订单同时被两个消费者获取到,那么会造成数据错误,因为消费者消费到消息,那么是要对消息做处理的,就会有两个消费者对同一条消息进行处理,所以要避免这个问题

      image-20220422205146287

      导致原因:不同消费者微服务(应用程序)默认分组是不同的。

    • 消息持久化问题

  3. 解决重复消费问题:

    用stream中的消息分组来解决。

    注意Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。

    不同组是全面消费的(重复消费)

    如果没有手动做分组的话,默认每一个消费者微服务(应用程序)的组都不一样,不同的组都要把消息消费一次,导致了重复消费。

    现在自定义配置分组,自定义配置分为同一个组,解决重复消费问题

  4. 自定义配置分组

    原理:微服务应用放置于同一个group中,就能够保证消息只被组中其中一个消费者消费一次。

    不同的组是可以重复消费的,但是同一个组内会发生竞争关系,只有其中一个可以消费。

    自定义配置分组就是在yml文件中配置,如下:

    bindings: ## 服务的绑定,整合
      input: #表示这是服务的消费者
        destination: studyExchange #表示要使用的Exchange名称
        content-type: application/json #设置消息类型,本次为json,文本则设置“text/plain”
        binder: defaultRabbit
        group: atguiguB
    

    现在要实现每次只有一个消费者消费消息,8801模块发送的消息只能被8802或者8803其中一个接收到,这样就避免了重复消费。

    那么把两个消费者配置成相同组。

    把两个消费者都配置成atguiguB

    效果如下:

    8801发送6条消息

    ***serial: c5f4370d-a3f0-4303-93fd-6c3dee0e4df1
    ***serial: a2d90834-4b5a-4219-8588-4e64dabee641
    ***serial: afe5fb00-c5a7-4ee7-9e7f-d02fa801886f
    ***serial: b4450607-12ff-4323-a516-4f70b6749ea7
    ***serial: 6a3d5224-6871-4ca9-ba3e-4c2daddd0583
    ***serial: 3e891ece-bcc2-4931-a4b9-31f8fb59b6b8
    

    8802和8803两个消费者微服务轮询接收消息

    8802

    消费者1,----->接收到的消息 c5f4370d-a3f0-4303-93fd-6c3dee0e4df1	serverPort: 8802
    消费者1,----->接收到的消息 afe5fb00-c5a7-4ee7-9e7f-d02fa801886f	serverPort: 8802
    消费者1,----->接收到的消息 6a3d5224-6871-4ca9-ba3e-4c2daddd0583	serverPort: 8802
    

    8803

    消费者2,----->接收到的消息 a2d90834-4b5a-4219-8588-4e64dabee641	serverPort: 8803
    消费者2,----->接收到的消息 b4450607-12ff-4323-a516-4f70b6749ea7	serverPort: 8803
    消费者2,----->接收到的消息 3e891ece-bcc2-4931-a4b9-31f8fb59b6b8	serverPort: 8803
    

持久化

  1. 消息持久化问题:

    • 停止8802消费者和8803消费者

    • 8802消费者的分组被去掉,8803的分组不去掉,那么现在8802消费者是没有持久化的,但是8803消费者是持久化。

    • 此时8801发送4条消息到rabbitmq

    • 先启动8802,无分组属性配置,后台没有打出来消息

    • 再启动8803,有分组属性配置,后台打出来了MQ上的消息。

  2. group属性在避免消费者重复消费,和消息持久化,避免消息丢失上有着重要的作用。

Spring Cloud Alibaba

spring-cloud-alibaba/README-zh.md at 2.2.x · alibaba/spring-cloud-alibaba (github.com)open in new window

https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html

  1. 大致作用:
    • 服务限流降级
    • 服务注册与发现---eureka、zookeeper、consul等的作用
    • 分布式配置管理
    • 消息驱动能力
    • 阿里云对象存储
    • 分布式任务调度

十、Nacos

概述

  1. nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台

    nacos就是注册中心 + 配置中心的组合。

    替代eureka做注册中心,替代spring cloud config做配置中心

nacos做注册中心

服务提供者

  1. 建module

  2. 改pom

        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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>
    
  3. application.yml

    server:
      port: 9001
    
    spring:
      application:
        name: nacos-payment-provider
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
    
    
    management:
      endpoints:
        web:
          exposure:
            include: '*'
    
  4. 主启动类

    @SpringBootApplication
    @EnableDiscoveryClient
    public class PaymentMain9001 {
        public static void main(String[] args) {
            SpringApplication.run(PaymentMain9001.class, args);
        }
    }
    
  5. 业务类

    @RestController
    public class PaymentController {
        @Value("${server.port}")
        private String port;
    
        @GetMapping(value = "/payment/nacos/{id}")
        public String getPayment(@PathVariable Integer id) {
            return "nacos registry, serverPort: " + port + "\t" + "id: " + id;
        }
    }
    

服务消费者

  1. 同样的方式建module、改pom,写application.yml,主启动类,以及业务类,进行服务的远程调用。

    服务的远程调用可以通过openFeign,知道服务名,从注册中心进行服务发现,进而进行服务调用。

    可以通过Ribbon+RestTemplate

  2. 为什么nacos自带负载均衡?

    nacos集成了netflix的ribbon,ribbon是支持负载均衡的。

    image-20220427192401949

  3. pom文件和服务提供者一样

  4. application.yml

    server:
      port: 83
    
    spring:
      application:
        name: nacos-order-consumer
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
    
    #消费者将要去访问的微服务提供者的服务名称(注册进nacos的服务提供者)
    service-url:
      nacos-user-service: http://nacos-payment-provider
    
  5. 主启动类

    @SpringBootApplication
    @EnableDiscoveryClient
    public class OrderNacosMain83 {
        public static void main(String[] args) {
            SpringApplication.run(OrderNacosMain83.class, args);
        }
    }
    
  6. 因为nacos自带ribbon,所以我们要写RestTemplate的配置类

    @Configuration
    public class ApplicationContextConfig {
        @Bean
        @LoadBalanced
        public RestTemplate getRestTemplate() {
            return new RestTemplate();
        }
    }
    

nacos做配置中心

概述

  1. 以前是通过springcloud config作为配置中心。

    结合springcloud bus实现动态刷新配置,不用重启服务。

    配置通过github来存储,并且实现配置的动态刷新。

    现在是通过nacos来完成这一功能

基础配置

  1. 新建module

  2. pom

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
  3. 配置文件要写bootstrap.yml和application.yml两个

    nacos和springcloud-config一样,在项目初始化的时候,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。

    springboot中的配置文件加载是存在优先级的,bootstrap要高于application

    相当于先有共性,后有个性

    bootstrap.yml:

    server:
      port: 3377
    
    spring:
      application:
        name: nacos-config-client
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
          config:
            server-addr: localhost:8848
            file-extension: yaml
    

    application.yml:

    spring:
      profiles:
        active: dev #表示开发环境
    
  4. Controller

    @RestController
    @RefreshScope // 支持Nacos的动态刷新功能
    public class ConfigClientController {
    
        @Value("${config.info}")
        private String configInfo;
    
        @GetMapping("/config/info")
        public String getConfigInfo() {
            return configInfo;
        }
    }
    
  5. image-20220427221615036

分类配置

  1. nacos的namespace是可以用于区分部署环境的,group和dataID逻辑上区分两个目标对象。

    image-20220427223205012

    比如说我们现在有三个环境:开发、测试和生产,我们就可以创建三个namespace,不同的namespace之间是隔离的。

    一个namespace下面可以进行多个group分组。

    group可以把不同的微服务划分到同一个分组里。

  2. 命名空间namespace:配置隔离

    • 默认:public(保留空间),默认新增的所有配置都在public空间

    • 开发、测试、生产:利用命名空间做环境隔离

      要同时在bootstrap.yml中对namespace进行配置

    • 每一个微服务之间互相隔离配置,每一个微服务都创建自己的命名空间,只加载自己命名空间下的所有配置。

  3. 配置分组:

    默认所有的配置集都属于:DEFAULT_GROUP

    一个namespace下面可以进行多个group分组。

    比如说某一个微服务,针对618、双11可以设置不同的分组。618、1111是同一个namespace下的多个group

  4. 分组配置如下:

    image-20220427224110964

    image-20220427224143741

  5. 通过namespace来区分配置环境

    • 修改bootstrap.yml

      spring:
        application:
          name: nacos-config-client
        cloud:
          nacos:
            discovery:
              server-addr: localhost:8848
            config:
              server-addr: localhost:8848
              file-extension: yaml
              group: DEV_GROUP
              namespace: f0d42ed7-6cba-4fe0-be60-5e802b16886d
      

nacos集群和持久化配置

概述

  1. nacos上一些重要的信息需要持久化,只是把信息配置进nacos里面,可能还不够,需要配置到数据库里,推荐的数据库是mysql,所以要进行linux的mysql安装配置

  2. image-20220427232304944

  3. nacos在没有配置mysql数据之前,自带了一个内嵌式数据库。

    内存中的东西,一断电就没有了,存入数据库,存入磁盘,才是持久化。

  4. 如果启动多个默认配置下的nacos节点,每个nacos都自带一份自己的小的嵌入式数据库derby,数据存储是存在一致性问题的,为了解决这个问题,nacos采用了集中式存储的方式来支持集群化部署,目前只支持mysql,保证了数据一致性。

    我们需要使nacos不再使用它自带的嵌入式数据库,而应配置MySQL,使用MySQL。

配置

  1. 需要1个nginx,3个nacos,1个mysql

  2. 在linux服务器上安装mysql和nginx,这不是本章的重点,重点在于三者之间的配置,建立起他们之间的联系。

  3. 安装nacos linux版本。

  4. mysql配置步骤

    • 找到nacos的安装路径下的conf目录的nacos-mysql.sql文件

      这是sql语句源文件,是在nacos中进行mysql配置的。

    • 在linux服务器上的mysql,新建数据库nacos_config

    • 执行命令use nacos_config;

    • 复制nacos-mysql.sql文件中的全部sql语句执行。

  5. conf目录下的application.properties配置

    spring.datasource.platform=mysql
    
    db.num=1
    db.url.0=jdbc:mysql://aliyun:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
    db.user=root
    db.password=mysql
    
  6. linux服务器上,nacos集群配置cluster.conf(conf目录下)

    • 先用hostname -I查询服务器ip地址

      [root@VM-0-2-centos conf]## hostname -I
      172.17.0.2 
      
    • 配置

      172.17.0.2:3333
      172.17.0.2:4444
      172.17.0.2:5555
      
    • 经过以上两个步骤,配置好了nacos的集群,通过不同端口启动。

    • 编辑nacos的启动脚本startup.sh,使它能够接受不同的启动端口

      bin目录下有startup.sh

      第一步

      image-20220428184814381

      第二步

      image-20220428184927825

  7. nginx配置

    • 修改nginx配置文件

      #gzip  on;
      upstream cluster{
          server 127.0.0.1:3333;
          server 127.0.0.1:4444;
          server 127.0.0.1:5555;
      }
      
      server {
          listen       1111;
          server_name  localhost;
      
          #charset koi8-r;
      
          #access_log  logs/host.access.log  main;
      
          location / {
             ## root   html;
             ## index  index.html index.htm;
             proxy_pass http://cluster;
          }
      
  8. 测试通过nginx访问linux上的nacos集群

    nacos的集群,三个nacos服务的端口分别是3333、4444、5555

    但是现在不直接访问,通过nginx进行访问,nginx配置好的端口号是1111

    分别启动三个nacos服务

     ./startup.sh -p 3333
     ./startup.sh -p 4444
     ./startup.sh -p 5555
    

    启动nginx

    ./nginx -c /usr/local/nginx/conf/nginx.conf
    

    访问http://42.192.182.4:1111/nacos/#/login

    image-20220428191323625

    发布配置

    image-20220428191620215

    检查mysql

    image-20220428191752846

    微服务进行注册

    spring:
      application:
        name: nacos-payment-provider
      cloud:
        nacos:
          discovery:
            #server-addr: localhost:8848
            #改成linux服务器上nginx的地址,通过nginx进行转发,服务器上有3个nacos服务,通过nginx做负载均衡
            server-addr: 42.192.182.4:1111
    

Sentinel实现熔断与限流

概述

  1. 相当于hystrix的阿里版,做微服务,分布式系统的流量控制、熔断降级。

  2. 介绍 · alibaba/Sentinel Wiki (github.com)open in new window

  3. 作用:

    防止服务雪崩

    做服务降级

    服务熔断

    服务限流

  4. sentinel分为两个部分

    • 核心库(Java客户端):不依赖任何框架、库,能够运行于所有的Java运行时环境,同时对Dubbo、springcloud等框架也有较好的支持。
    • 控制台(Dashboard):基于springboot开发,打包后可以直接运行,不需要额外的tomcat等容器。

微服务集成sentinel

  1. 新建module

  2. 改pom

    <dependencies>
        <!--nacos config 和 discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--后续做持久化用到-->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</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>
    
  3. application.yml

    server:
      port: 8401
    
    spring:
      application:
        name: cloudalibaba-sentinel-service
      cloud:
        nacos:
          discovery:
            ## nacos服务注册中心地址
            server-addr: localhost:8848
        sentinel:
          transport:
            #配置sentinel dashboard地址
            dashboard: localhost:8080
            ## 默认8719端口,假如被占用会自动从8719开始+1扫描,直至找到未被占用的端口
            port: 8719
    
    management:
      endpoints:
        web:
          exposure:
            include: '*'
    

流控规则

  1. 介绍

    • 资源名:唯一名称,默认请求路径

    • 针对来源:sentinel可以针对调用者进行限流,填写微服务名称,默认default(不区分来源)

    • 阈值类型/单机阈值:

      • QPS(每秒钟的请求数量):当调用该api的QPS达到阈值的时候,进行限流
      • 线程数:当调用该API的线程数达到阈值的时候,进行限流。
    • 是否集群:不需要集群

    • 流控模式:

      • 直接:api达到限流条件时,直接限流

      • 关联:当关联的资源达到阈值时,限流自己

        当与A关联的资源B达到阈值后,就限流A自己

        image-20220428235324056

      • 链路:只记录链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)

    • 流控效果:

      • 快速失败:直接抛异常

      • warm up:

        即预热/冷启动方式,当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮,通过冷启动,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

        默认冷加载因子是3,即请求QPS从阈值/3开始,经预热时长逐渐升至设定的QPS阈值,相当于给系统一个预热缓冲的时间,请求限制阈值刚开始很低,经预热时长,才逐渐达到设定阈值。

        image-20220429000459359

      • 排队等待:匀速排队,让请求匀速通过,阈值类型必须设置为QPS,否则无效。

        image-20220429000950900

        这种方式主要用于处理间隔性突发的流量,例如消息队列。例如这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

熔断降级

  1. image-20220429005351780

  2. RT:平均响应时间

    时间窗口内通过的请求>=5,且平均响应时间均超过阈值,触发降级,接下来的时间窗口,对这个方法的调用,都自动地进行熔断。

    窗口期过后,关闭断路器。

    image-20220429010249375

  3. 异常比例:

    QPS >= 5,且异常比例(秒级统计),超过阈值时,触发降级(接下来的时间窗口);时间窗口结束后,关闭降级。

  4. 异常数

    分钟统计,超过阈值,触发降级(接下来的时间窗口),时间窗口结束后,关闭降级。

  5. sentinel的断路器是没有半开状态的。

  6. sentinel熔断降级会在调用链路中某个资源出现不稳定状态(以上三种情况)时,对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误。

    当资源被降级后,在接下来的窗口时间内,对该资源的调用都自动熔断。

热点规则

  1. 何为热点?

    热点就是经常访问的数据

    image-20220429012930432

  2. 之前的case,服务熔断降级后,或者服务限流了,都是用sentinel系统默认的提示,这个提示是可以自定义的,类似于hystrix兜底的方法,即fallback method

    通过注解

    从@HystrixCommand到@SentinelResource

    image-20220429013822810

    image-20220429014054421

    注意:资源名,不带斜线,说明是配置的@SentinelResource注解的value值,限流信息是根据我们自定义的方法,即@SentinelResource注解的blockHandler的值,若带斜线,说明是根据url地址进行限流,限流信息是sentinel的默认信息

    blockHandler相当于是@HystrixCommand注解的fallbackmethod

    以上配置的意思是:1秒钟之内,请求次数超过1了,即QPS超过1,那么会执行blockHandler的自定义方法,相当于fallbackmethod,实现服务降级。

  3. 参数例外项

    普通的配置就是,当带有某个参数的请求的QPS超过阈值,被限流,执行blockHandler的自定义方法

    但是我们期望设定的参数是某个特殊值时,它的限流值和平时不一样。假如当p1的值是5时,阈值可以是200.

    image-20220429014913106

  4. 注意:

    image-20220429015602259

@SentinelResource

  1. 这个注解的blockHandler属性,请求会根据sentinel配置的流控规则,跳转到blockHandler的方法进行执行,相当于Hystrix的fallback method,但是当调用的方法内部出现了异常,并不会跳转到blockHandler的方法执行,而是会报异常,只会根据sentinel的配置进行跳转

    如果是调用的方法内部出现了异常,又不想页面抛出异常,而是通过对应的方法进行处理,那么需要另外配置fallback属性。

  2. 兜底方案面临的问题

    • 我们自定义的处理方法和业务代码耦合在一起
    • 每个业务方法都添加一个兜底的,代码膨胀
    • 全局统一的处理方法没有体现
  3. 解决上述问题

    • 创建CustomBlockHandler类用于自定义限流处理逻辑,在这个类里统一处理跳转页面、服务降级的说明等等,来统一写限流的相关信息,将限流、降级与业务代码解耦

      public class CustomBlockHandler {
          public static CommonResult handlerException(BlockException blockException) {
              return new CommonResult(4444, "按客户自定义,global handlerException---1");
          }
      
          public static CommonResult handlerException2(BlockException blockException) {
              return new CommonResult(4444, "按客户自定义,global handlerException---2");
          }
      }
      
      @RestController
      public class RateLimitController {
      
          @GetMapping("/rateLimit/customBlockHandler")
          @SentinelResource(value = "customBlockHandler", blockHandlerClass = CustomBlockHandler.class, blockHandler = "handlerException2")
          public CommonResult customBlockHandler() {
              return new CommonResult(200, "按客户自定义", new Payment(2020L, "serial003"));
          }
      
      }
      

      image-20220429154831580

  4. 重要属性:

    • value:资源名称:必需项

    • blockHandler/blockHandlerClass:

      blockHandler对应处理BlockException的函数名称,可选项。

      blockHandler函数访问范围必须是public,返回类型需要与原业务方法相匹配,参数类型需要和原业务方法相匹配并且最后加一个额外的参数,类型为BlockException。blockHandler函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定blockHandlerClass为对应的类class对象,注意对应的函数必须为static函数,否则无法解析。

    • fallback:可选项,用于在抛出异常的时候提供fallback处理逻辑。fallback函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。

      fallback函数签名和位置要求:

      • 返回值类型必须和原业务方法返回值类型一致。
      • 方法参数列表需要和原函数一致,或者可以额外多一个Throwable类型的参数用于接收对应的异常。
      • fallback函数默认需要和原方法在同一个类中,若希望使用其他类的函数(使处理异常的代码和业务逻辑代码解耦合),则可以指定fallbackClass为对应的类的class对象,注意到对应的函数必须为staic函数,否则无法解析。

服务熔断功能

  1. @SentinelResource的属性

    • fallback管运行异常
    • blockHandler管配置违规

    若blockHandler和fallback都进行了配置,则被限流降级而抛出BlockException时只会进入blockHandler处理逻辑。

  2. 异常忽略

    image-20220429164904026

Last Updated:
Contributors: 陈杨