版本选择:

  • SpringBoot:2.6.11
  • SpringCloud:2021.0.4(由SpringCloud决定SpringBoot的版本)
  • SpringCloudAlibaba:2021.0.4.0
  • Java:Java8
  • Maven:3.5及以上
  • MySQL:5.7及以上

可以通过https://start.spring.io/actuator/info确定具体的版本对应关系:

"spring-cloud": {
    "Hoxton.SR12": "Spring Boot >=2.2.0.RELEASE and <2.4.0.M1",
    "2020.0.6": "Spring Boot >=2.4.0.M1 and <2.6.0-M1",
    "2021.0.0-M1": "Spring Boot >=2.6.0-M1 and <2.6.0-M3",
    "2021.0.0-M3": "Spring Boot >=2.6.0-M3 and <2.6.0-RC1",
    "2021.0.0-RC1": "Spring Boot >=2.6.0-RC1 and <2.6.1",
    "2021.0.4": "Spring Boot >=2.6.1 and <3.0.0-M1",
    "2022.0.0-M1": "Spring Boot >=3.0.0-M1 and <3.0.0-M2",
    "2022.0.0-M2": "Spring Boot >=3.0.0-M2 and <3.0.0-M3",
    "2022.0.0-M3": "Spring Boot >=3.0.0-M3 and <3.0.0-M4",
    "2022.0.0-M4": "Spring Boot >=3.0.0-M4 and <3.0.0-M5",
    "2022.0.0-M5": "Spring Boot >=3.0.0-M5 and <3.1.0-M1"
}

初级部分

1. 关于Cloud组件以及停更说明

SpringCloud是微服务一站式服务解决方案,其主流核心技术以及所作更换如下图:

  • Eureka停更不停用,可以使用Zookeeper、Nacos等替换,推荐Nacos,几乎可以完美的替换Eureka
  • 服务调用,准备推出LoadBalancer代替Ribbon,目前Ribbon仍然可以正常使用
  • Feign改为OpenFeign
  • 服务降级,Hystrix停更,改为resilience4j或者阿里巴巴的sentinel
  • 服务网关,Zuul改为gateway
  • 服务配置,Config改为Nacos
  • 服务总线,Bus改为Nacos

538fe0b92dca440cb803eb6d8dbb6300.png

c16d4b03abe548c38e95bb79d3dcfe99.png

2. 微服务架构编码构建

2.1 创建父工程之后做的准备

①.设置字符编码为UTF-8

880dfce300f34006895c2836e09d5f7d.png

②.注解生效激活

7568cf85c6724602a20da50344b7b6c4.png

2.2 父工程的POM文件

<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud2022</artifactId>
<version>1.0-SNAPSHOT</version>
<!--父工程的打包方式为 pom-->
<packaging>pom</packaging>

<!--统一管理 jar 包版本-->
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <junit.version>4.12</junit.version>
    <log4j.version>1.2.17</log4j.version>
    <lombok.version>1.16.18</lombok.version>
    <druid.version>1.2.8</druid.version>
    <mybatis.spring.boot.version>2.2.0</mybatis.spring.boot.version>
</properties>

<!-- 只声明依赖,并不实现引入,所以子项目还需要写要引入的依赖 -->
<dependencyManagement>
    <dependencies>
        <!-- SpringBoot、SpringCloud、SpringCloudAlibaba是搭建分布式微服务架构的标配 -->
        <!-- 导入 SpringCloud 需要使用的依赖信息 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.4</version>
            <type>pom</type>
            <!-- import依赖范围表示将spring-cloud-dependencies包中的依赖信息导入 -->
            <scope>import</scope>
        </dependency>
        <!-- 导入 SpringBoot 需要使用的依赖信息 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.6.11</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- 导入 SpringCloudAlibaba 需要使用的依赖信息 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.0.4.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- druid 连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!-- mybatis 依赖信息 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.spring.boot.version}</version>
        </dependency>
        <!-- junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>
        <!-- log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>


<!--maven构建过程相关配置-->
<build>
    <!--构建过程中所需要用到的插件-->
    <plugins>
        <!--这个插件将springboot应用打包成一个可执行的jar包-->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

3. 支付模块构建

3.1 cloud-provider-payment8001工程的pom文件

<!-- 引入web的starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <!--这个是监控系统健康情况的工具和 web 要写到一块-->
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- mybatis的整合 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- druid连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</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>

3.2 application.yaml配置文件

server:
  port: 8001

spring:
  application:
    name: cloud-provider-payment8001
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud2022?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    username: root
    password: abc123
mybatis:
  # 用于指定 XxxMapper.xml 配置文件的位置
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.springcloud.entities  # 所有 Entity 别名类所在包

# 针对具体的某个包,设置日志级别,以便打印日志,就可以看到Mybatis打印的 SQL 语句了
logging:
  level:
    com.atguigu.springcloud.mapper: debug

3.3 主启动类

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

3.4 业务类

3.4.1 建库建表

CREATE DATABASE cloud2022;
CREATE TABLE `payment`(
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `serial` VARCHAR(200) DEFAULT '',
    PRIMARY KEY (`id`)
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

3.4.2 建实体类Payment

package com.atguigu.springcloud.entities;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
    private Long id;
    private String serial;
}

3.4.3 PaymentMapper接口和PaymentMapper.xml文件

@Mapper
public interface PaymentMapper {
    public int create(Payment payment);
    public Payment getPaymentById(@Param("id") Long id);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.springcloud.mapper.PaymentMapper">
    <!-- 添加,并返回添加成功的主键的值 -->
    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
        insert into payment(serial) values (#{serial});
    </insert>
    <resultMap id="BaseResultMap" type="com.atguigu.springcloud.entities.Payment">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <id column="serial" property="serial" jdbcType="VARCHAR"/>
    </resultMap>
    <select id="getPaymentById" parameterType="long" resultMap="BaseResultMap">
        select * from payment where id = #{id};
    </select>
</mapper>

3.4.4 service和impl实现类

public interface PaymentService {
    public int create(Payment payment);
    public Payment getPaymentById(Long id);
}

@Service
public class PaymentServiceImpl implements PaymentService {

    @Autowired
    private PaymentMapper paymentMapper;

    @Override
    public int create(Payment payment) {
        return paymentMapper.create(payment);
    }

    @Override
    public Payment getPaymentById(Long id) {
        return paymentMapper.getPaymentById(id);
    }
}

3.4.5 Controller业务类

@RestController
@Slf4j
public class PaymentController {
    @Autowired
    private PaymentService paymentService;
    @PostMapping("/payment/create")
    public ResultEntity create(@RequestBody Payment payment){
        int result = paymentService.create(payment);
        log.info("******插入结果:"+result);
        if(result > 0){
            return ResultEntity.successWithData(result);
        }else{
            return ResultEntity.failed("插入数据库失败!");
        }
    }
    @GetMapping("/payment/get/{id}")
    public ResultEntity<Payment> getPaymentById(@PathVariable("id") Long id){
        Payment payment = paymentService.getPaymentById(id);
        log.info("******插入结果:"+payment);
        if(payment != null){
            return ResultEntity.successWithData(payment);
        }else{
            return ResultEntity.failed("没有对应记录,查询失败!");
        }
    }
}

4. 消费者订单模块构建

4.1 cloud-consumer-order80工程的pom文件

<!-- 引入web的starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <!--这个是监控系统健康情况的工具和 web 要写到一块-->
    <artifactId>spring-boot-starter-actuator</artifactId>
</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>

4.2 application.yaml配置文件

server:
  port: 80

spring:
  application:
    name: cloud-consumer-order80

4.3 主启动类

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

4.4 业务类

4.4.1 配置类,往容器中注入RestTemplate组件

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

4.4.2 OrderController类

@RestController
@Slf4j
public class OrderController {
    @Autowired
    private RestTemplate restTemplate;
    // 注意:get请求不能使用@requestbody接收参数
    @GetMapping("/consumer/payment/create")
    public ResultEntity<Payment> create(Payment payment){
        // 1.声明远程微服务的主机地址加端口号
        String host = "http://localhost:8001";
        // 2.声明具体要调用的功能的URL地址
        String url = "/payment/create";
        // 3.通过RestTemplate调用远程微服务
        return restTemplate.postForObject(host+url,payment,ResultEntity.class);
    }
    @GetMapping("/consumer/get/payment/{id}")
    public ResultEntity<Payment> getPaymentById(@PathVariable("id") Long id){
        // 1.声明远程微服务的主机地址加端口号
        String host = "http://localhost:8001";
        // 2.声明具体要调用的功能的URL地址
        String url = "/payment/get/"+id;
        // 3.通过RestTemplate调用远程微服务
        return restTemplate.getForObject(host+url,ResultEntity.class);
    }
}

5. Eureka服务注册与发现

5.1 Eureka基础知识

5.1.1 服务治理

  • SpringCloud封装了Netflix公司开发的Eureka模块来实现服务治理。在传统的RPC远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。

5.1.2 服务注册与发现

  • Eureka采用了CS的设计结构,Eureka Server作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行(这点和zookeeper很相似)。在服务注册与发现中,有一个注册中心。当服务器启动时候,会把当前自己服务器的信息比如服务地址、通讯地址等以别名方式注册到注册中心上。另一方(消费者或服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用。RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何rpc远程框架中,都会有一个注册中心(存放服务地址相关信息(接口址))

ae97b2bdc3fb47c3b6e3ca9a17c47646.png

5.1.3 Eureka两组件:Eureka Server 和 Eureka Client

  • Eureka Server 提供服务注册服务,各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
  • Eureka Client 通过注册中心进行访问,是一个java客户端,用于简化EurekaServer的交互,客户端同时也具备一个内置的、使用轮询(roundrobin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)

5.2 单机Eureka构建步骤

5.2.1 EurekaServer端服务注册中心的构建

①.cloud-eureka-server7001工程的pom文件

<dependencies>
    <!--旧版本2018
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    -->
    <!--新版本2021-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

②.application.yaml配置文件

server:
  port: 7001

spring:
  application:
    name: cloud-eureka-server7001
eureka:
  instance:
    hostname: localhost #eureka服务端的主机地址
  client:
    registerWithEureka: false # 注册:自己就是注册中心,所以自己不注册自己
    fetchRegistry: false      # 订阅:自己就是注册中心,所以不需要 "从注册中心取回信息"
    service-url:              # 客户端(指的是Consumer、Provider)访问 当前 Eureka 时使用的地址
      defaultZone: http://localhost:${server.port}/eureka/

③.主启动类

//启用 Eureka 服务器功能
@EnableEurekaServer
@SpringBootApplication
public class EurekaMain7001 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain7001.class,args);
    }
}

④.通过localhost:7001访问注册中心

84ec9cf1b23b4d3aafaec7c37ba92c55.png

5.2.2 将EurekaClient端cloud-provider-payment8001注册进EurekaServer成为服务提供者provider

①.改pom文件

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

②.改application.yaml配置文件

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/

③.改主启动类

// 下面两个注解功能大致相同
// @EnableDiscoveryClient    启用发现服务功能,不局限于Eureka注册中心
// @EnableEurekaClient       启用Eureka客户端功能,必须是Eureka注册中心
@EnableEurekaClient
@SpringBootApplication
public class PaymentMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8001.class,args);
    }
}

5.2.3 将EurekaClient端cloud-consumer-order80注册进EurekaServer成为服务消费者consumer

①.改pom文件

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

②.改application.yaml配置文件

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/

③.改主启动类

// 下面两个注解功能大致相同
// @EnableDiscoveryClient    启用发现服务功能,不局限于Eureka注册中心
// @EnableEurekaClient       启用Eureka客户端功能,必须是Eureka注册中心
@EnableEurekaClient
@SpringBootApplication
public class OrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain80.class,args);
    }
}

5.2.4 测试,通过localhost:7001访问注册中心

db0a0a0631ce44f2a601b080199b5a0d.png

5.3 集群Eureka构建步骤

5.3.1 Eureka集群原理:互相注册,相互守望,对外暴露出一个整体,实现负载均衡和故障容错

17d75962830e4348a74f633fcbbf7ab6.png

5.3.2 Eureka集群环境构建

①.参考cloud-eureka-server7001新建cloud-eureka-server7002工程的pom文件

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

②.修改映射配置,模拟两台机器,但本质是同一个(域名映射)

397184280dc24609b819b510c2b95a42.png

③.修改application.yaml配置文件

server:
  port: 7001

spring:
  application:
    name: cloud-eureka-server7001
eureka:
  instance:
    #hostname: localhost #单机
    hostname: eureka7001.com  #eureka服务端的实例名称,集群
  client:
    registerWithEureka: false # 注册:自己就是注册中心,所以自己不注册自己
    fetchRegistry: false      # 订阅:自己就是注册中心,所以不需要 "从注册中心取回信息"
    service-url:              # 客户端(指的是Consumer、Provider)访问 当前 Eureka 时使用的地址
      # defaultZone: http://localhost:${server.port}/eureka/ 单机
      defaultZone: http://eureka7002.com:7002/eureka/ # 集群:相互注册,相互守望

=========================================================================================
server:
  port: 7002

spring:
  application:
    name: cloud-eureka-server7002
eureka:
  instance:
    #hostname: localhost #单机
    hostname: eureka7002.com  #eureka服务端的实例名称,集群
  client:
    registerWithEureka: false # 注册:自己就是注册中心,所以自己不注册自己
    fetchRegistry: false      # 订阅:自己就是注册中心,所以不需要 "从注册中心取回信息"
    service-url:              # 客户端(指的是Consumer、Provider)访问 当前 Eureka 时使用的地址
      # defaultZone: http://localhost:${server.port}/eureka/ 单机
      defaultZone: http://eureka7001.com:7001/eureka/ # 集群:相互注册,相互守望

④.主启动类

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

⑤.启动集群测试

2a2646e0fe954e118fdfa05c3b910c49.png

5.3.3 将订单支付两微服务注册进eureka集群

①.修改两者的application.yaml配置文件

eureka:
  client:
    service-url:
      # defaultZone: http://localhost:7001/eureka/  单机
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/  # 集群版

②.测试,集群搭建成功且不影响订单支付两微服务工程的使用

37c0809e1575469e936647d9b9f62b7e.png

74179fe54cd44a85bc27eaf17b42ee54.png

5.3.4 支付服务提供者8001集群环境构建,略(多次复制该工程,然后修改端口号即可,另外需要在配置类中的RestTemplate上面使用负载均衡的注解@LoadBalanced)

5.4 actuator微服务信息完善

# 客户端和服务端都需要引入这两个依赖才能完成下面的两个操作
<!-- 引入web的starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <!--这个是监控系统健康情况的工具和 web 要写到一块-->
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

5.4.1 主机名称:服务名称的规范和修改(在cloud-provider-payment8001中修改)

eureka:
  client:
    service-url:
      # defaultZone: http://localhost:7001/eureka/  单机
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/  # 集群版
  instance:
    # 用于修改主机名称:服务名称
    instance-id: payment8001

facb4cf515c5447ebb539f70e597fa06.png

5.4.2 希望访问信息有ip地址

eureka:
  client:
    service-url:
      # defaultZone: http://localhost:7001/eureka/  单机
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/  # 集群版
  instance:
    # 用于修改主机名称:服务名称
    instance-id: payment8001
    prefer-ip-address: true  # 访问路径可以显示ip地址

df78127a609d4bd9a09b25635854bae0.png

5.5 服务发现Discovery

对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息。(即我们前面可视化页面的信息)

①.在主启动类上添加注解:@EnableDiscoveryClient
②.在Controller 里面打印信息:
import org.springframework.cloud.client.discovery.DiscoveryClient;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/payment/discovery")
public Object discovery(){
    List<String> services = discoveryClient.getServices();
    for (String element : services) {
        log.info("*******element:" + element);
    }
    List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PROVIDER-PAYMENT8001");
    for (ServiceInstance instance : instances) {
        log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
    }
    return this.discoveryClient;
}

14a94d25177443dd8c24a6b28f705cfe.png

5.6 Eureka自我保护

5.6.1 概述

保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式:

a560dbaa1fa649d5af616aecd13ac7eb.png

一句话总结:某时刻某一个微服务不可用了,Eureka不会立即清理,依旧会对该微服务的信息进行保存。属于CAP里的AP(高可用)分支 

5.6.2 为什么会产生Eureka自我保护机制

  • 为了防止EurekaClient可以正常运行,但是与Eureka Server网络不通情况下,Eureka Server不会立刻将EurekaClient服务剔除

5.6.3 什么是自我保护模式

默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销该微服务。Eureka通过"自我保护模式"来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。

3c83b847879c49c58fadf74311e9ad5a.png

在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着。综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。

5.6.4 禁止自我保护(如果想的话)

在 Eureka Server 的模块中的 yaml 文件进行配置:
eureka:
  instance:
    #hostname: localhost #单机
    hostname: eureka7001.com  #eureka服务端的实例名称,集群
  client:
    registerWithEureka: false # 注册:自己就是注册中心,所以自己不注册自己
    fetchRegistry: false      # 订阅:自己就是注册中心,所以不需要 "从注册中心取回信息"
    service-url:              # 客户端(指的是Consumer、Provider)访问 当前 Eureka 时使用的地址
      # defaultZone: http://localhost:${server.port}/eureka/ 单机
      defaultZone: http://eureka7002.com:7002/eureka/ # 集群:相互注册,相互守望
  server:
    # 关闭自我保护机制,保证不可用服务被及时删除
    enable-self-preservation: false
    # 发送心跳时间,间隔时间
    eviction-interval-timer-in-ms: 2000
在 Eureka Client 的模块中的 yaml 文件进行配置:
eureka:
  client:
    service-url:
      # defaultZone: http://localhost:7001/eureka/  单机
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/  # 集群版
  instance:
    # 用于修改主机名称:服务名称
    instance-id: payment8001
    prefer-ip-address: true  # 访问路径可以显示ip地址
    # Eureka客户端像服务端发送心跳的时间间隔,单位s,默认30s
    lease-renewal-interval-in-seconds: 1
    # Eureka服务端在收到最后一次心跳后等待时间上线,单位为s,默认90s,超时将剔除服务
    lease-expiration-duration-in-seconds: 2

6. Zookeeper服务注册与发现

6.1 SpringCloud整合Zookeeper代替Eureka

  • zookeeper是一个分布式协调工具,可以实现注册中心功能
  • zookeeper可以安装在Linux系统上
  • 关闭Linux服务器防火墙后动zookeeper服务器
  • zookeeper服务器取代Eureka服务器,zk作为服务注册中心

6.1.1 服务提供者注册进Zookeeper服务器

①.创建cloud-provider-payment8002工程,修改pom文件

<!--SpringBoot整合Zookeeper客户端-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
<!--引入自己定义的api通用包-->
<dependency>
    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<!-- 引入web的starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</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>

②.application.yaml文件

server:
  port: 8002

spring:
  application:
    name: cloud-provider-payment8002
  cloud:
    zookeeper:
      connect-string: 192.168.56.100:2181 # 安装Zookeeper的linux系统的IP地址和端口号

③.主启动类

// 启用发现服务功能,不局限于Eureka注册中心,比如Zookeeper和Consul注册中心
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain8002 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8002.class,args);
    }
}

④.业务类Controller

@RestController
@Slf4j
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;
    
    @RequestMapping("/payment/zk")
    public String paymentZk(){
        return "springcloud with zookeeper:" + serverPort + "\t" + UUID.randomUUID().toString();
    }
}

⑤.启动8002注册进Zookeeper,面临的问题

pom文件引进的Zookeeper版本和Linux虚拟机里安装的Zookeeper版本存在版本冲突的问题

d3ce20e801374276a9ada1c09b7d3957.png

⑥.Zookeeper中的服务节点是临时节点,当我们的服务一定时间内没有发送心跳,那么zk就会将这个服务节点删除了。没有自我保护机制。重新建立连接后流水号也会变 

6.1.2 服务消费者注册进Zookeeper服务器

①.创建cloud-consumerzk-order80工程,修改pom文件

<!--SpringBoot整合Zookeeper客户端-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
<!--引入自己定义的api通用包-->
<dependency>
    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<!-- 引入web的starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</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>

②.application.yaml配置文件

server:
  port: 80

spring:
  application:
    name: cloud-consumerzk-order80
  cloud:
    zookeeper:
      connect-string: 192.168.56.100:2181 # 安装Zookeeper的linux系统的IP地址和端口号

③.主启动类

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

④.业务类

config:
@Configuration
public class ApplicationContextConfig {
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}
controller:
@RestController
@Slf4j
public class OrderZKController {
    public static final String INVOKE_URL = "http://cloud-provider-payment8002";
    @Autowired
    private RestTemplate restTemplate;
    
    @RequestMapping("/consumer/payment/zk")
    public String paymentInfo(){
        String result = restTemplate.getForObject(INVOKE_URL + "/payment/zk", String.class);
        return result;
    }
    
}

7. Consul服务注册与发现

7.1 Consul概述

Consul是一套开源的分布式服务发现和配置管理系统,用Go语言开发,提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。它具有很多优点,包括:基于raft协议,比较简洁;支持健康检查,同时支持HTTP和DNS协议;支持跨数据中心的WAN集群,提供图形界面;跨平台,支持Linux、Mac、Windows。它可以做:服务发现,提供HTTP和DNS两种发现方式;健康监测,支持多种方式,HTTP、TCP、Docker、Shell脚本定制化;KV存储,Key、Value的存储方式;多数据中心可视化Web界面

7.2 安装和运行

de4e811ba549493d9427d7b4f00d4686.png

7.3 服务提供者注册进Consul

①.创建cloud-provider-payment8003工程,修改pom文件

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-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>
<!--引入自己定义的api通用包-->
<dependency>
    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</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>

②.application.yaml配置文件

server:
  port: 8003

spring:
  application:
    name: cloud-provider-payment8003
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}

③.主启动类

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

④.业务类

@RestController
@Slf4j
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

    @RequestMapping("/payment/consul")
    public String paymentConsul(){
        return "springcloud with consul:" + serverPort + "\t" + UUID.randomUUID().toString();
    }
}

7.4 服务消费者注册进Consul

①.创建cloud-consumerconsul-order80工程,修改pom文件

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-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>
<!--引入自己定义的api通用包-->
<dependency>
    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</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>

②.application.yaml配置文件

server:
  port: 80

spring:
  application:
    name: cloud-consumerconsul-order80
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}

③.主启动类

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

④.业务类

config:
@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}
controller:
@RestController
@Slf4j
public class OrderConsulController {
    public static final String INVOKE_URL = "http://cloud-provider-payment8003";
    @Autowired
    private RestTemplate restTemplate;
    
    @RequestMapping("/consumer/payment/consul")
    public String paymentInfo(){
        String result = restTemplate.getForObject(INVOKE_URL + "/payment/consul", String.class);
        return result;
    }
    
}

7.5 三个注册中心的异同点

8861fecfd4b04990be83ab1e9c6d6005.png

CAP:

  • C:Consitency 强一致性
  • A:Available 高可用性
  • P:Partition tolerance 分布式的分区容错性(分布式微服务架构永远都要保证P,所以我们的系统要么是AP,要么是CP
  • 经典的CAP图理论而言,三个里面不可能全占,最多只能同时较好的满足两个
  • CAP理论关注粒度是数据,而不是整体系统设计的
  • CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性、可用性和分区容错性这三个需求,因此,根据CAP原理将NoSQL数据库分成了满足CA原则、CP原则、AP原则三大类
    • CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大
    • CP - 满足一致性,分区容错性的系统,通常性能不是特别高
    • AP - 满足可用性,分区容错性的系统,通常可能对一致性要求低一些

ab56c25f727042eb92bd970d03dd18cb.png

8. Ribbon负载均衡服务调用

8.1 Ribbon概述

8.1.1 是什么?

SpringCloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项,如连接超时、重试等。简单地说,就是在配置文件中列出Load Balance(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询、随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。

8.1.2 能做什么?

①.LB(负载均衡):集中式LB、进程内LB

  • LB负载均衡是什么?简单地说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件F5等
  • 集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方
  • 进程内LB:将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

②.Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别:

  • Nginx是服务器负载均衡(集中式LB),客户端所有请求都会交给Nginx,然后由Nginx实现转发请求。即负载均衡是由服务端实现的。
  • Ribbon本地负载均衡(进程内LB),在调用微服务接口时候,会在注册中心上获取注册信息服务列表,之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

8.2 Ribbon负载均衡演示

Ribbon其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和Eureka结合只是其中的一个实例,Ribbon在工作时分成两步:

  • 第一步先选择Eureka Server,它优先选择在同一个区域内负载较少的server
  • 第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。其中Ribbon提供了多种策略:比如轮询、随相和根据响应时间加权。

000255f704f04e848c3b7507980e5d70.png

注意:pom文件中没有引入ribbon依赖却能使用Ribbon客户端负载均衡的原因是Eureka-Client依赖中已经包含ribbon依赖,所以能实现负载均衡,总而言之,Ribbon就是负载均衡+RestTemplate调用。实际上不止eureka的jar包有,zookeeper的jar包,还有consul的jar包都包含了它,就是上面使用的服务调用。

8.3 Ribbon核心组件IRule

8.3.1 IRule,根据特定算法从服务列表中选取一个要访问的服务。

Ribbon负载均衡规则类型:

  • com.netflix.loadbalancer.RoundRobinRule:轮询
  • com.netflix.loadbalancer.RandomRule:随机
  • com.netfIix.IoadbaIancer.RetryRuIe:重设,先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
  • WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
  • BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
  • AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
  • ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器

fd39047f63504b90a1d7f326a2889b6a.png

8.3.2 如何替换规则类型

在使用ribbon客户端负载均衡所在的工程下(cloud-consumer-order80),创建包com.atguigu.myrule,在包下创建MySelfRule规则配置类,然后在主启动类上添加@RibbonClient注解:@RibbonClient(name="CLOUD-PROVIDER-SERVICE", configuration = MySelfRule.class),指定该负载均衡规则对哪个提供者服务使用 ,加载自定义规则的配置类

11413ca65f9b4585a7376d5f8fa837ce.png

package com.atguigu.myrule;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;

@Configuration
public class MySelfRule {
    @Bean
    public IRule myrule(){
        return new RandomRule(); //负载均衡规则定义为随机
    }
}

注意:官方文档明确给出了警告,这个自定义配置类不能放在@ComponentScan 所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。而Springboot主启动类上的 @SpringBootApplication 注解,相当于加了@ComponentScan注解,会自动扫描当前包及子包,所以注意不要放在SpringBoot主启动类的包内。

8.4 Ribbon负载均衡算法

8.4.1 轮询算法原理

7036d4dabf9c4c338708c8339ff9377a.png

8.4.2 轮询算法源码

private AtomicInteger nextServerCyclicCounter;

public Server choose(ILoadBalancer lb, Object key) {

    Server server = null;
    int count = 0;
    while (server == null && count++ < 10) {
        List<Server> reachableServers = lb.getReachableServers();
        List<Server> allServers = lb.getAllServers();
        int upCount = reachableServers.size();
        int serverCount = allServers.size();

        int nextServerIndex = incrementAndGetModulo(serverCount);
        server = allServers.get(nextServerIndex);

        if (server == null) {
            /* Transient. */
            Thread.yield();
            continue;
        }

        if (server.isAlive() && (server.isReadyToServe())) {
            return (server);
        }

        // Next.
        server = null;
    }

    if (count >= 10) {
        log.warn("No available alive servers after 10 tries from load balancer: "  + lb);
    }
    return server;
}
private int incrementAndGetModulo(int modulo) {
    for (;;) {
        int current = nextServerCyclicCounter.get();//获取原子的值
        int next = (current + 1) % modulo;
        if (nextServerCyclicCounter.compareAndSet(current, next)) //CAS
            return next;
    }
}

8.4.3 手写轮询算法:原理+JUC(CAS+自旋锁),略,因为JUC还未学

9. OpenFeign服务接口调用

9.1 OpenFeign概述

9.1.1 是什么?

Feign是一个声明式WebService客户端。使用Feign能让编写WebService客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了SpringMVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。

9.1.2 能干嘛?

Feign旨在使编写Java Http客户端变得更容易。前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接囗会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Springcloud Ribbon时,自动封装服务调用客户端的开发量。feign自带负载均衡配置项。

9.1.3 Feign集成了Ribbon

利用Ribbon维护了Payment的服务列表信息,并目通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用

9.1.4 Feign和OpenFeign两者区别

623c851bff35465a841cf0a7ddae4fcf.png

9.2 OpenFeign使用步骤

①.pom文件(在这里创建新的工程:cloud-consumer-feign-order80)

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

②.yaml文件

server:
  port: 80

spring:
  application:
    name: cloud-consumer-feign-order80

eureka:
  client:
    service-url:
      # defaultZone: http://localhost:7001/eureka/  单机
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/  # 集群版

③.主启动类上添加注解@EnableFeignClients注解 

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * @author wuxy
 * @create 2022-11-04 14:42
 */
// 下面两个注解功能大致相同
// @EnableDiscoveryClient    启用发现服务功能,不局限于Eureka注册中心
// @EnableEurekaClient       启用Eureka客户端功能,必须是Eureka注册中心
@EnableFeignClients
@SpringBootApplication
public class OrderFeignMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderFeignMain80.class,args);
    }
}

④.业务类

业务逻辑接口+注解@FeignClient配置调用Provider服务
@FeignClient(value = "cloud-provider-payment8001",fallbackFactory = MyFallBackFactory.class)
// @FeignClient注解表示当前接口和一个Provider对应
//      注解中value属性指定要调用的Provider的微服务名称
//      注解中fallbackFactory属性指定Provider不可用时提供备用方案的工厂类型
public interface PaymentRemoteService {
    @GetMapping("/payment/get/{id}")
    public ResultEntity<Payment> getPaymentById(@PathVariable("id") Long id);
}
对应的是Payment8001工程的controller类中的getPaymentById方法
@GetMapping("/payment/get/{id}")
public ResultEntity<Payment> getPaymentById(@PathVariable("id") Long id){
    Payment payment = paymentService.getPaymentById(id);
    log.info("******插入结果:"+payment);
    if(payment != null){
        return ResultEntity.successWithData(payment);
    }else{
        return ResultEntity.failed("没有对应记录,查询失败!");
    }
}
控制层Controller:
@RestController
public class OrderFeignController {
    // 装配调用远程微服务的接口,后面向调用本地方法一样直接使用
    @Autowired
    private PaymentRemoteService paymentRemoteService;

    @GetMapping("/consumer/payment/get/{id}")
    public ResultEntity<Payment> getPaymentByIdRemote(){
        return paymentRemoteService.getPaymentById();
    }
}

9.3 OpenFeign超时控制

消费侧调用服务侧,这是两个不同的微服务,一定存在着一种情况:超时,默认Feign客户端只等待一秒钟,但是当服务端处理需要超过1秒钟时,导致Feign客户端不想等待了,直接返回报错,为了避免出现这种情况,可以在yaml配置文件中设置Feign客户端的超时时间:

#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ReadTimeout: 5000
  #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ConnectTimeout: 5000

9.4 OpenFeign日志打印功能

9.4.1 是什么?

Feign提供了日志打印功能,可以通过配置来调整日志级别,从而了解Feign中Http请求的细节,说白了就是对Feign接口的调用情况进行监控和输出

9.4.2 日志级别

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息
  • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据

9.4.3 如何配置?

①.首先写一个config配置类:配置日志bean

package com.atguigu.springcloud.config;

import feign.Logger;

@Configuration
public class FeignConfig{
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}

②.然后在yaml文件中开启日志打印配置

logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.atguigu.springcloud.service.PaymentRemoteService: debug

中级部分

10. Hystrix断路器

10.1 Hystrix概述

10.1.1 分布式系统面临的问题:服务雪崩

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

10.1.2 是什么

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

10.1.3 能干嘛

  • 可以进行服务降级fallback、服务熔断break、接近实时的监控以及限流flowlimit、隔离等等,具体概念见10.2。

10.2 Hystrix重要概念

  • 服务降级fallback(在客户端):当出现服务器忙碌、网络拥堵或者其他未知异常时,向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,比如说,可以返回服务器忙,请稍后再试,不让客户等待并立刻返回一个友好的提示
    • 降级发生的情况:程序运行异常、超时、服务熔断触发服务降级、线程池/信号量打满也会导致服务降级
  • 服务熔断break(在服务端):类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。服务的降级 → 进而熔断 → 恢复调用链路
  • 服务限流flowlimit:秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行

10.3 Hystrix案例

10.3.1 构建带hystrix熔断框架的8001服务工程cloud-provider-hystrix-payment8001

①.pom文件

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--引入自己定义的api通用包-->
<dependency>
    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<!-- 引入web的starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <!--这个是监控系统健康情况的工具和 web 要写到一块-->
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- mybatis的整合 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- druid连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</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>

②.application.yaml

server:
  port: 8001
spring:
  application:
    name: cloud-provider-hystrix-payment8001
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/  # 单机

③.主启动类

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

④.业务类

@Service
public class PaymentService {
    /**
     * 正常访问,ok
     */
    public String paymentInfo_OK(Integer id){
        return "线程池:  " + Thread.currentThread().getName() + "  paymentInfo_OK,id:  " + id + "\t" + "ahhhhh!!";
    }
    /**
     * 访问超时,timeout
     */
    public String paymentInfo_TimeOut(Integer id){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:  " + Thread.currentThread().getName() + "  paymentInfo_TimeOut,id:  " + id + "\t" + "ahhhhh!!" + "耗时3秒钟";
    }
}
@RestController
@Slf4j
public class PaymentController {
    @Autowired
    private PaymentService paymentService;

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

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        String result = paymentService.paymentInfo_OK(id);
        log.info("*****result: " + result);
        return result;
    }
    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
        String result = paymentService.paymentInfo_TimeOut(id);
        log.info("*****result: " + result);
        return result;
    }
}

⑤.模拟高并发(使用JMeter

22688933d7b4455c87ed7d5fb8d0489d.png

从测试可以看出,当模拟的长时间请求被高并发以后,访问普通的短请求速率也会被拉低。因为tomcat的默认工作线程数被打满了,没有多余的线程来分解压力和处理。上面还是服务8001自己测试,如果加入外部的消费者80来访问,那消费者只能干等,最终导致消费端80不满意,服务端8001直接被拖死。测试可见,当启动高并发测试时,消费者访问也会变得很慢,甚至出现超时报错。那么该怎么解决呢?解决思路:超时不再等待、出错要有兜底

  • 对方服务(8001)超时,调用者(80)不能一直卡死等待,必须有服务降级
  • 对方服务(8001)宕机,调用者(80)不能一直卡死等待,必须有服务降级
  • 对方服务(8001)健康,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级

10.3.2 构建消费者工程cloud-consumer-feign-hystrix-order80

①.pom文件

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--引入自己定义的api通用包-->
<dependency>
    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<!-- 引入web的starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <!--这个是监控系统健康情况的工具和 web 要写到一块-->
    <artifactId>spring-boot-starter-actuator</artifactId>
</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>

②.application.yaml文件

server:
  port: 80
spring:
  application:
    name: cloud-consumer-feign-hystrix-order80
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/  # 单机

③.主启动类

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

④.业务类

OpenFeign接口:
@Component
@FeignClient(value = "cloud-provider-hystrix-payment8001")
public interface PaymentHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
Controller类:
@RestController
@Slf4j
public class OrderHystrixController {
    @Autowired
    private PaymentHystrixService paymentHystrixService;
    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
        String result = paymentHystrixService.paymentInfo_TimeOut(id);
        return result;
    }
}

10.3.3 服务降级

10.3.3.1 降级配置,使用注解代替编码@HystrixCommand

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

②.8001fallback

使用@HystrixCommand注解标注在容易出错的方法上,一旦调用服务方法失败并抛出了错误信息后,会自动调用注解@HystrixCommand标注好的fallbackMethod调用类中的指定方法

业务类启用:
/**
 * 访问超时,timeout
 */
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
        // 这个线程的超时时间是3秒钟
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String paymentInfo_TimeOut(Integer id){
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "线程池:  " + Thread.currentThread().getName() + "  paymentInfo_TimeOut,id:  " + id + "\t" + "ahhhhh!!" + "耗时5秒钟";
}
// 兜底方法
public String paymentInfo_TimeOutHandler(Integer id){
    return "线程池:  " + Thread.currentThread().getName() + "  paymentInfo_TimeOutHandler,id:  " + id + "\t" + "aaaaa!!";
}
主启动类激活:
// 启用断路器功能
@EnableCircuitBreaker
@EnableEurekaClient
@SpringBootApplication
public class HystrixPaymentMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixPaymentMain8001.class,args);
    }
}

③.80fallback

①.yaml配置文件的设置:
# 表示客户端启用hystrix的降级功能
feign:
  hystrix:
    enabled: true
②.主启动类加@EnableHystrix注解
③.业务类
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
    String result = paymentHystrixService.paymentInfo_TimeOut(id);
    return result;
}
// 兜底方法
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
    return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己!!!";
}

10.3.3.2 出现的问题

  • 每个业务方法都对应一个兜底的方法,代码膨胀
  • 业务逻辑的方法和处理异常服务降级的方法揉在一块,耦合度过高
  • 统一的降级和自定义的降级分开

10.3.3.3 解决问题 

① 代码膨胀的问题,引入@DefaultProperties(defaultFallback = "")注解,表示全局的兜底方法

10fb8ee27639420ca07c907b9f5f0188.png

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {
    @Autowired
    private PaymentHystrixService paymentHystrixService;
    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
//    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
//            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
//    })
    @HystrixCommand
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
        String result = paymentHystrixService.paymentInfo_TimeOut(id);
        return result;
    }
    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
        return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己!!!";
    }
    // 下面是全局fallback方法,自己指定了兜底方法的话就走自己的兜底方法,否则走全局的兜底方法
    public String payment_Global_FallbackMethod(){
        return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
    }
}

②.代码耦合度高的问题,只需为Feign客户端定义的接口添加一个用于处理服务降级的实现类即可实现解耦,该实现类统一为接口里面的方法进行异常处理,如果方法正常运行,则进行远程调用,如果方法出错,则运行实现类里面的兜底方法。

Feign客户端接口:
@Component
@FeignClient(value = "cloud-provider-hystrix-payment8001",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
Feign客户端接口的实现类:用于兜底的方法
@Component
public class PaymentFallbackService implements PaymentHystrixService{
    @Override
    public String paymentInfo_OK(Integer id) {
        return "-----PaymentFallbackService fall back-paymentInfo_OK,┭┮﹏┭┮";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) {
        return "-----PaymentFallbackService fall back-paymentInfo_TimeOut,┭┮﹏┭┮";
    }
}

10.3.4 服务熔断

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

通俗理解就是:

调用失败会触发降级,而降级会调用fallback方法,但无论如何降级的流程一定会先调用正常方法再调用fallback方法。假如单位时间内调用失败次数过多,也就是降级次数过多,则触发熔断,熔断以后就会跳过正常方法直接调用fallback方法,所谓“熔断后服务不可用”就是因为跳过了正常方法直接执行fallback方法

熔断的状态:

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

①.实操:修改 cloud-provider-hystrix-payment8001 工程的PaymentServcie

//======服务熔断
@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"),// 失败率达到多少后跳闸
        }) // 在10s内10次请求有60%失败,则进行熔断 // 先看次数,再看百分比
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
    if(id < 0) {
        throw new RuntimeException("******id 不能负数");
    }
    // 生成唯一流水号
    String serialNumber = IdUtil.simpleUUID();// 等价于UUID.randomUUID().toString(); //pom中有hutool-all

    return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
// 兜底方法
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id){//服务降级
    return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " +id;
}

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

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

8e4544f8bd6b4db18884dd753d483e22.png

②.实操:修改 cloud-provider-hystrix-payment8001 工程的PaymentController

//====服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
    String result = paymentService.paymentCircuitBreaker(id);
    log.info("*****result: " + result);
    return result;
}

③.熔断总结

断路器开启或者关闭的条件:

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

断路器打开之后:

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

10.4 服务监控HystrixDashBoard

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

10.4.1 仪表盘9001

①.新建cloud-consumer-hystrix-dashboard9001工程,pom文件

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</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>

②.application.yaml配置文件

server:
  port: 9001
spring:
  application:
    name: cloud-consumer-hystrix-dashboard9001

③.主启动类

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

④.测试:localhost:9001/hystrix

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

①.修改cloud-provider-hystrix-payment8001的yaml文件

management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream

②.监控测试:启动Eureka7001、Hystrix8001、HystrixDashboard9001

3b00be78bb0547b5ab6b62b388b1b65d.png

11. Zuul路由网关和GateWay新一代网关

11.1 Gateway简介:SpringCloud Gateway使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架

SpringCloud Gateway是SpringCloud的一个全新项目,基于Spring5.0+Springboot 2.0和Project Reactor等技术开发的网关,同时也是基于异步非阻塞模型,它旨在为微服务架构提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。SpringCloudGateway作为SpringCloud生态系统中的网关,目标是替代Zuul,在SpringCloud2.0以上版本中,没有对新版本的zuul2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而webFlux框架底层则使用了高性能的Reactor模式通信框架Netty。springCloud Gateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

2a951fbc2201492cb260e6242e5ee07c.png

SpringCloud Gateway具有如下特性:

  • 基于Spring5.0、Springboot 2.0和Project Reactor进行构建
  • 动态路由:能够匹配任何请求属性
  • 可以对路由指定Predicate(断言)和Filter(过滤器)
  • 集成Hystrix的断路器功能
  • 集成SpringCloud服务发现功能
  • 易于编写的Predicate(断言)和Filter(过滤器)
  • 请求限流功能
  • 支持路径重写

SpringCloudGateway与Zuul的区别:在SpringCloudFinchley正式版之前,SpringCloud推荐的网关是Netflix提供的Zuul:

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

Zuul 1.x模型

  • springcloud中所集成的zuul版本,采用的是tomcat容器,使用的是传统的servlet IO处理模型。
  • 学过尚硅谷web中期课程都知道一个题目,Servlet的生命周期,servlet由servlet container进行生命周期管理
  • container启动时构造servlet对象并调用servlet init()进行初始化
  • container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()
  • container关闭时调用servlet destory()销毁servlet

上述模式的缺点:

  • servlete—个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程在并发不高的场景下这种模型是适用的。但是一旦高并发(比如抽风用jemeter压测),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势,所以Zuul 1.x是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet)并由该servlet阻塞式处理。所以springcloud zuul无法摆脱servlet模型的弊端。

传统的Web框架比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的。但是,在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使用java8)。Spring WebFlux是Spring5.0引入的新的响应式框架区别于SpringMVC,它不需要依赖ServletAPI,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。

11.2 三大核心概念

  • Route(路由):路由是构建网关的基本模块,它由ID、目标URI、一系列的断言和过滤器组成,如果断言为true则匹配该路由
  • Predicate(断言):参考的是Java8的java.util.function.predicate。开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
  • Filter(过滤):指的是spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改
  • 总体:web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。predicate就是匹配条件;而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了

11.3 Gateway工作流程

  1. 客户端向Spring Cloud Gateway发出请求
  2. 然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler
  3. Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
  4. Gateway的核心逻辑是:路由转发+执行过滤链

过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。Filter在"pre"类型的过滤器可以做参数校验,权限校验,流量监听,日志输出,协议转换等;在"post"类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

fad7639e50e941a3acd8abf1b9315db3.png

11.4 入门配置

①.新建工程cloud-gateway-gateway9527,修改pom文件

<!-- gateway和spring web+actuator不能同时存在,即web相关jar包不能导入 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--gateWay网关作为一种微服务,也要注册进服务中心。哪个注册中心都可以,如zk-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</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>

②.application.yaml配置文件

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/  # 单机
  instance:
    # 用于修改主机名称:服务名称
    instance-id: gateway
    prefer-ip-address: true  # 访问路径可以显示ip地址
    hostname: cloud-gateway-service

③.主启动类

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

④.9527网关如何做路由映射? yml新增网关配置实现效果

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

⑤.测试

  • 启动7001、8001、9527网关
  • 访问地址:
    • 添加网关前,访问地址http://localhost:8001/payment/get/5

b71dd367508245a9bc68d3152e4cd62b.png

    • 添加网关后,访问地址http://localhost:9527/payment/get/5

 101efe65e0fe46f8846f2a75effb3565.png

⑥.如果感觉yaml配置越来越庞大,也可以通过硬编码的方式进行配置:代码中注入路由定位(RouteLocator)的bean

@Configuration
public class GatewayConfig {
    /**
     * 配置了一个id为path_route_atguigu的路由规则,当访问地址localhost:9527/guonei时,网关会进行路由转发到http://news.baidu.com/guonei
     * @param locatorBuilder 路由构建器
     * @return 自定义的路由定位
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder locatorBuilder){
        RouteLocatorBuilder.Builder routes = locatorBuilder.routes();
        // 实际效果是:访问localhost:9527/guonei地址时,网关会进行路由转发到http://news.baidu.com/guonei
        routes.route("path_route_atguigu",
                r -> r.path("/guonei").uri("http://news.baidu.com/guonei")).build();
        return routes.build();
    }
}

11.5 通过微服务名实现动态路由(集群的负载均衡)

这里所谓的动态配置就是利用服务注册中心,来实现 负载均衡 的调用多个微服务。默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能。注意,这是GateWay 的负载均衡

①.启动一个eureka7001和两个服务提供者8001/8002

②.application.yaml配置文件的修改

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true              # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_route          # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001 # 匹配后提供服务的路由地址
          # lb:是指路由的一种通信协议,它实现了负载均衡通信功能
          uri: lb://cloud-provider-payment8001 # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**   # 断言,路径相匹配的进行路由
        - id: payment_route2
          # uri: http://localhost:8001 # 匹配后提供服务的路由地址
          # lb:是指路由的一种通信协议,它实现了负载均衡通信功能
          uri: lb://cloud-provider-payment8001 # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/create/**

11.6 常用Predicate的使用

d710192e8693429485bdcd64e0db8e56.png

①.时间级别的断言:After Route Predicate、Before Route Predicate、Between Route Predicate

predicates:
  - Path=/payment/create/**
  - After=2023-01-13T18:37:23.410+08:00[Asia/Shanghai]  # 会在这个时间之后这个路由才生效
  - Before=2023-01-13T18:37:23.410+08:00[Asia/Shanghai] # 在这个时间之后这个路由会失效
  - Between=2023-01-13T18:37:23.410+08:00[Asia/Shanghai],2024-01-13T18:37:23.410+08:00[Asia/Shanghai] # 在这个时间之间这个路由才生效

"2023-01-13T18:37:23.410+08:00[Asia/Shanghai]"这个时间格式是如何得到的?

public class T2 {
    public static void main(String[] args) {
        ZonedDateTime zonedDateTime = ZonedDateTime.now();//默认时区
        System.out.println(zonedDateTime);//2023-01-13T18:37:23.410+08:00[Asia/Shanghai]
    }
}

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

predicates:
  - Cookie=username,zzyy # 类似于键值对 username=zzyy
  # 使用cmd命令行以及curl进行测试,curl只是一种测试指令:
  # 不带Cookie的访问:curl http://localhost:9527/payment/create,断言失败,无法访问
  # 带Cookie的访问:curl http://localhost:9527/payment/create --cookie "username=zzyy",断言成功,进行路由转发,顺利访问

③.请求头断言:Header Route Predicate,两个参数,一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行

predicates:
  - Header=X-Request-Id, \d+   # 请求头要有X-Request-Id属性并且值为正数的正则表达式
  # 使用cmd命令行以及curl进行测试,curl只是一种测试指令:
  # 失败的断言:curl http://localhost:9527/payment/create,断言失败,无法访问
  # 成功的断言:curl http://localhost:9527/payment/create -H "X-Request-Id:1234",断言成功,进行路由转发,顺利访问

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

predicates:
  - Host=**.atguigu.com
  # 使用cmd命令行以及curl进行测试,curl只是一种测试指令:
  # 失败的断言:curl http://localhost:9527/payment/create,断言失败,无法访问
  # 成功的断言:断言成功,进行路由转发,顺利访问
    curl http://localhost:9527/payment/create -H "Host:www.atguigu.com"
    curl http://localhost:9527/payment/create -H "Host:news.atguigu.com"

⑤.Method Route Predicate

predicates:
  - Method=Get

⑥.Path Route Predicate

predicates:
  - Path=/payment/create/**

⑦.带查询条件的断言:Query Route Predicate,支持传入两个参数,一个是属性名,一个为属性值,属性值可以是正则表达式

predicates:
  - Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
  # 使用cmd命令行以及curl进行测试,curl只是一种测试指令:
  # 失败的断言:curl http://localhost:9527/payment/create,断言失败,无法访问
  # 成功的断言:断言成功,进行路由转发,顺利访问
    curl http://localhost:9527/payment/create?username=31

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

11.7 Filter的使用

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

Spring Cloud Gateway的Filter

  • 生命周期:业务逻辑之前(pre)、业务逻辑之后(post)
  • 种类:GatewayFilter(单一的,31种之多)、GlobalFilter(全局的,10多个)

主要用的是自定义过滤器(自定义全局GlobalFilter)

①.两个主要接口:implements GlobalFilter,Ordered

②.能干嘛:权限校验、全局日志记录、统一网关鉴权等等

③.代码案例

@Component
public class MyLogGatewayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("*********come in MyLogGatewayFilter: " + new Date());
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        MultiValueMap<String, String> queryParams = serverHttpRequest.getQueryParams();
        String uname = queryParams.getFirst("uname");
        if(uname == null){
            System.out.println("******用户名为null,非法用户!");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        // 放行
        return chain.filter(exchange);
    }

    /**
     * 用于设置加载过滤器的顺序,越小表示越早加载,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

④.测试:

访问地址http://localhost:9527/payment/get5

112871fa14f34f7585e669b66b6b944f.png

访问地址http://localhost:9527/payment/get5?uname=465

93332bfdff194d1780c9cf66df6e2f33.png

12. SpringCloud Config分布式配置中心

12.1 概述

4316237c02214c74bcc8b44a1570676c.png

①.分布式系统面临的问题——配置问题

  • 微服务意味着要将单应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务,面临着严重的配置问题由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的
  • SpringCloud提供了ConfigServer来解决这个问题。否则我们每一个微服务自己带着一个application.yaml,上百个配置文件的管理......,配置问题比较严峻。比如数据库的信息,我们可以写到一个统一的地方

②.是什么?

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

③.能干嘛?

  • 集中管理配置文件(管理的是通用的)
  • 不同环境不同配置,动态化的配置更新,分环境部署,比如dev(开发环境)、test(测试环境)、prod(产品环境)、beta(预发布环境)、release(灰度发布)等等
  • 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
  • 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
  • 将配置信息以REST接囗的形式暴露

④.与GitHub整合配置

  • 由于SpringCloud Config默认使用Git来存储配置文件(也可以用其他方式,比如SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式
  • 用自己的账号在Github上新建一个名为springcloud-config的新仓库Repository
  • 由上一步获得刚新建的Git地址:https://github.com/wrj0824/springcloud-config.git
  • 本地硬盘目录上新建git仓库并clone:E:\developer\git-springcloud\springcloud-config,命令:git clone https://github.com/wrj0824/springcloud-config.git
  • 如果需要修改,此处模拟运维人员操作git和GitHub
  • git add
    git commit -m “标记”
    git push origin master

8f4de5092395493eb337743d3192dd69.png

ab11348c8a0243aebc93787674a33939.png

12.2 Config服务端配置与测试(ConfigServer配置中心)

①.新建cloud-config-center3344工程,它即为cloud的配置中心模块,pom文件

<!-- config Server -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!--eureka-client config Server也要注册进服务中心-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</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>

②.application.yaml配置文件

server:
  port: 3344
spring:
  application:
    name: cloud-config-center # 注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          uri: https://github.com/wrj0824/springcloud-config.git # Github上面的Git仓库名字
          ####去uri地址上搜索目录名为springcloud-config的仓库
          search-paths:
            - springcloud-config
      #### 读取分支
      label: master
# 服务注册到Eureka上
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/  # 单机
  instance:
    # 用于修改主机名称:服务名称
    instance-id: payment8001
    prefer-ip-address: true  # 访问路径可以显示ip地址

③.主启动类

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

④.修改windows下的hosts文件,增加映射:127.0.0.1 config3344.com

30999e00ced741db8e9f4e5ec28c20a0.png

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

  • 启动Eureka7001、Config3344
  • 访问地址:http://config3344.com:3344/master/config-dev.yaml

59a604758610491ba9e4c329c914dc43.png

⑥.配置读取规则(有很多种,掌握一种即可)

5cd655efca834743b3325289ffeb76b6.png

最后一个出的是json串:label:分支branch、name:服务名、profiles:环境dev/test/prod

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

12.3 Config客户端配置与测试

①.新建cloud-config-client3355工程,它即为cloud的配置中心模块的客户端,pom文件

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 引入web的starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <!--这个是监控系统健康情况的工具和 web 要写到一块-->
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--eureka-client config client也要注册进服务中心-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</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>

②.新的配置文件bootstrap.yaml(为了配置文件的加载顺序和分级管理)

  • application.yaml是用户级的资源配置项
  • bootstrap.yaml是系统级的,优先级更加高
  • SpringCloud会创建一个“Bootstrap Context”,作为Spring应用的“Application Context”的父上下文。初始化的时候,“Bootstrap Context”负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的环境Environment
  • “Bootstrap”属性有高优先级,默认情况下,他们不会被本地配置覆盖。
  • “Bootstrap Context”和“Application Context”有着不同的约定,所以新增了一个“bootstrap.yaml”文件,保证“Bootstrap Context”和“Application Context”配置的分离
  • 要将Client模块下的application.yaml文件改为bootstrap.yaml,这是很关键的。因为bootstrap.yaml是比application.yaml先加载的,优先级较高
server:
  port: 3355
  
spring:
  application:
    name: cloud-config-client
  cloud:
    # Config客户端配置
    config:
      label: master # 分支名称
      name: config # 配置文件名称
      profile: dev # 读取后缀名称
      ### 上述三个综合:master分支上的config-dev.yaml的配置文件被读取,http://config3344.com:3344/master/config-dev.yaml
      uri: http://localhost:3344 # 配置中心地址

# 服务注册到Eureka上
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/  # 单机
  instance:
    # 用于修改主机名称:服务名称
    instance-id: configclient
    prefer-ip-address: true  # 访问路径可以显示ip地址

③.主启动类

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

④.业务类

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

⑤.测试

  • 启动Eureka7001
  • 启动Config配置中心3344微服务并自测 http://config3344.com:3344/master/config-dev.yaml
  • 启动ConfigClient3355作为Client准备访问 http://localhost:3355/configInfo

212bfe1a001346f69cba7a5af2a9b4a9.png

原因:Spring Cloud 新版本默认将 Bootstrap 禁用,需要将 spring-cloud-starter-bootstrap 依赖引入到工程中 

e814018055fa403e9ac30e8b594f2e84.png

成功实现了客户端3355访问ConfigCenter3344通过Github获取配置信息 

⑥.问题随之而来,分布式的动态刷新问题

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

ae5b427388ba48b8b4fd484b3d361bee.png

12.4 Config客户端之动态刷新

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

12.4.1 动态刷新的手动版

①.修改3355模块,pom引入actuator监控

<dependency>
    <groupId>org.springframework.boot</groupId>
    <!--这个是监控系统健康情况的工具和 web 要写到一块,除了网关不加,其他模块几乎都要加-->
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

②.修改yaml,暴露监控端口

### 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"

③.在业务类Controller上添加刷新的注解@RefreshScope

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

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

④.修改github,访问3344/3355看效果,发现无效,此时需要运维人员发送post请求刷新3355(每次修改,都需要进行这一步操作)

## 必须是post请求
curl -X post "http://localhost:3355/actuator/refresh"

fcaa96fa2f194fff861ff245eb975fa6.png

⑤.再次访问3344/3355看效果,发现成功实现动态刷新

⑥.动态刷新的手动版还有什么问题需要解决?

  • 假如有多个微服务客户端3355/3366/3377......
  • 每个微服务都要执行一次post请求,手动刷新
  • 可否广播,一次通知,处处生效?
  • 想大范围的自动刷新,怎么实现?
  • 综上,引入了 Bus 消息总线

13. SpringCloud Bus消息总线

13.1 概述

Bus是对上一讲Config的加深和扩充,一句话概括就是:Bus配合Config使用,可以实现分布式的自动刷新配置功能(动态刷新)

13.1.1 是什么?

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

SpringCloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了java的事件处理机制和消息中间件的功能

90068284441c48a4b64006685711a356.png

13.1.2 能干嘛?

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

4a0910396ea742428b40de0e475d06ed.png

13.1.3 为何被称为总线

①.什么是总线?

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

②.基本原理

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

13.2 RabbitMQ环境配置

  • 安装RabbitMQ的依赖环境Erlang,下载地址:http://erlang.org/download/otp_win64_21.3.exe
  • 安装RabbitMQ,下载地址:http://dl.bintray.com/rabbitmq/all/rabbitmq-server/3.7.14/rabbitmq-server-3.7.14.exe
  • 进入RabbitMQ安装目录下的sbin目录,打开cmd窗口
  • 输入以下命令启动管理功能:rabbitmq-plugins enable rabbitmq_management,这样就可以添加可视化插件了
  • 只需要启动RabbitMQ,访问地址就可以查看是否安装成功:http://localhost:15672/
  • 输入账号密码并登录:默认是guest guest

13.3 SpringCloud Bus动态刷新全局广播

13.3.1 必须先具备良好的RabbitMQ环境。为了演示广播效果,需增加复杂度,所以再以3355为模板制作一个3366

13.3.2 设计思想:

第一种:利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置(图见:13.1.1)

f04d131b60c94c21ab416e9efd0d9c52.png

第二中:利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置(图见:13.1.2)

01f8560fc3fa453e9bc7e971c4334052.png

显然图二的架构更加合适,图一不合适的原因如下:

  • 打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责
  • 破坏了微服务各节点的对等性
  • 有一定的局限性。例如:微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改

13.3.3 给cloud-config-center3344配置中心服务端添加消息总线支持

①.修改pom文件,引入相关依赖

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

②.修改application.yaml文件

# rabbitmq相关配置
rabbitmq:
  host: localhost
  port: 5672
  uesrname: guest
  password: guest
## rabbitmq相关配置,暴露bus刷新配置的端点
management:
  endpoints: # 暴露bus刷新配置的端点
    web:
      exposure:
        include: 'bus-refresh'

13.3.4 给cloud-config-client3355、3366客户端添加消息总线支持

①.pom文件

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

②.application.yaml文件

# rabbitmq相关配置
rabbitmq:
  host: localhost
  port: 5672
  uesrname: guest
  password: guest

13.3.5 测试:一次修改,广播通知,处处生效

  • 运维工程师:
    •  修改Github上配置文件增加版本号
    •  发送Post请求“http://localhost:3344/actuator/bus-refresh”,一次发送,处处生效
  • 配置中心:“http://localhost:3344/config-dev.yaml”
  • 客户端:
    • “ http://localhost:3355/configInfo”
    •  “http://localhost:3366/configInfo”
    •  获取配置信息,发现都刷新了

13.4 SpringCloud Bus动态刷新定点通知

指定具体某一个实例生效而不是全部,公式:http://localhost:配置中心的端口号/actuator/bus-refresh/{destination目的地},/bus/refresh请求不再发送到具体的服务实例上,而是发给config server并通过destination参数指定需要更新配置的服务,destination就是:springname:port

# 只通知3355,不通知366
http://localhost:3344/actuator/bus-refresh/cloud-config-client:3355

通知总结:

 14d2515ffd0a402c8dfd4ee667c0b29d.png

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐