尚硅谷 Spring Cloud学习笔记(一)

微服务架构概述

Spring Cloud是分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶。
服务注册与发现:Eureka
服务负载与调用:Netflix Oss Ribbbon
服务熔断降级:Hystrix
服务网关:Zuul
服务分布式配置:Spring Cloud Config
服务开发:Spring Boot

Boot和Cloud课程使用版本选择

上篇:SpringBoot 2.x 版和Spring Cloud H版
下篇:SpringCloud Alibaba
SpringBoot: 2.2.2.release
SpringCloud: Hoxton.SR1
Cloud Alibaba: 2.1.0.release
Java: Java8
Maven: 3.5及以上
Mysql: 5.7及以上

一、创建父工程

1.字符编码
在这里插入图片描述
2.注解生效激活
在这里插入图片描述
3.Java编译版本选择Java8
在这里插入图片描述
4.父pom文件

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>cloud2020</artifactId>
  <version>1.0-SNAPSHOT</version>
  <modules>
    <module>cloud-provider-payment8001</module>
  </modules>
  <packaging>pom</packaging><!--表示是总的父工程-->


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

  <!--子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version-->
  <!--dependencyManagement 里只是声明依赖,并不实现导入,因此子项目需要显示的声明需要用的依赖-->
  <!--如果不在子项目中声明依赖,是不会从父项目继承下来的。只有在子项目写了该依赖,并且没有指定具体版本,才会从父项目中继承,并且version和scope都读取自父pom,
  如果子项目指定了版本号,那么会使用子项目指定的jar版本-->
  <dependencyManagement>
    <dependencies>
      <!--spring boot 2.2.2-->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.2.2.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--spring cloud Hoxton.SR1-->
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Hoxton.SR1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!--spring cloud alibaba 2.1.0.RELEASE-->
      <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>2.2.0.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!-- MySql -->
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
      </dependency>
      <!-- Druid -->
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>${druid.verison}</version>
      </dependency>
      <!-- mybatis-springboot整合 -->
      <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>${mybatis.spring.boot.verison}</version>
      </dependency>
      <!--lombok-->
      <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.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>


  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>2.3.0.RELEASE</version>
        <configuration>
          <fork>true</fork>
          <addResources>true</addResources>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>


二、支付模块构建

微服务模块构建步骤:
1.建module
2.改pom
3.写yml
4.主启动
5.业务类
1.支付模块pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-payment8001</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</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>
        </dependency>
    </dependencies>

</project>
2.application.yml
server:
  port: 8001

spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource  #当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动包
    url: jdbc:mysql://localhost:3306/springcloud?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
    username: root
    password: 123456
    ​devtools:
     restart:
     enabled: true
     
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.springcloud.entities  #所有entity别名所在包,mybatis的xml文件中可以使用别名


注意ymal中的格式,否则可能因为空格等问题导致无法启动。

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);
    }
}

4.业务逻辑及测试
4.1 建库建表
CREATE DATABASE `springcloud`;
USE `springcloud`;
CREATE TABLE `payment` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `serial` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
4.2 dao、service、controller实现

PaymentMapper.xml

<?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.dao.PaymentDao">
    <resultMap id="BaseResultMap" type="com.atguigu.springcloud.entities.Payment">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <id column="serial" property="serial" jdbcType="VARCHAR"/>
    </resultMap>

    <insert id="create" parameterType="payment" useGeneratedKeys="true" keyProperty="id">
        insert into payment(serial) values (#{serial})
    </insert>
    <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
        select * from payment where id = #{id}
    </select>
</mapper>

PaymentController.java

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment) {
        log.info("前端传递数据:"+payment);
        int result = paymentService.create(payment);
        log.info("************************插入结果:" +result);
        if (result > 0) {
            return new CommonResult(200,"插入数据库成功",result);
        } else {
            return new CommonResult(444,"插入数据库失败", null);
        }
    }

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult getPaymentById(@PathVariable("id") Long id) {
        Payment payment = paymentService.getPaymentById(id);
        log.info("*************************查询结果:" +payment);
        if (payment != null) {
            return new CommonResult(200, "查询成功", payment);
        } else {
            return new CommonResult(445,"没有对应id: "+id+" 的记录", null);
        }
    }
}

4.3 postman测试

插入数据测试:
在这里插入图片描述
查询数据测试:
在这里插入图片描述

4.4 注意点 DashBoard

若没有Run DashBoard窗口,打开Run DashBoard。IDEA 2020叫Services,在 view -> tool windows -> service 打开dashboard
在这里插入图片描述

5. 热部署

1.Adding devtools to project
添加到子模块pom.xml

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
</dependency>

2.Adding plugin to pom.xml
添加到父类总工程

<build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>2.3.0.RELEASE</version>
        <configuration>
          <fork>true</fork>
          <addResources>true</addResources>
        </configuration>
      </plugin>
    </plugins>
  </build>

3.Enabling automatic build(开启自动编译)
在这里插入图片描述

4.Update value of
快捷键: ctrl+shift+Alt+/
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.重启IDEA

三、消费者订单模块构建

1.消费者模块pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-order80</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.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>
        </dependency>
    </dependencies>

</project>
2.application.yml
server:
  port: 80
3.主启动类
package com.atguigu.springcloud;

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

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

4.业务逻辑
4.1 消费者模块对支付服务的横向调用

RestTemplate: 提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集。完成80到8001的远程调用。

4.2 RestTemplate的使用

使用RestTemplate访问restful接口非常简单。(url, requestMap, ResponseBean.class) 这三个参数分别代表REST请求地址,请求参数,HTTP响应转换成的对象类型。

4.3 目录结构

在这里插入图片描述

4.4 controller
package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderController {

    public static final String PAYMENT_URL = "http://localhost:8001";
    //实现两个服务之间的调用 restTemplate
    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment) {
        return restTemplate.postForObject(PAYMENT_URL+ "/payment/create", payment ,CommonResult.class);
    }

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
        return restTemplate.getForObject(PAYMENT_URL+ "/payment/get/" +id, CommonResult.class);
    }
}

4.5 配置类
package com.atguigu.springcloud.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {
    //引入RestTemplate

    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
//applicationContext.xml  <bean id="" class="">
4.6 测试订单模块(80)调用支付模块(8001)

在这里插入图片描述
在这里插入图片描述

4.7 工程重构的提出

订单模块结构:
在这里插入图片描述
支付模块结构:
在这里插入图片描述
此时,两个模块中有重复的代码,所以将重复的代码提取出来,专门形成一个jar包,一处部署,处处通用。
进行工程重构。

四、工程重构 添加公共模块

新建cloud-api-commons
1.建module
2.改pom
3.写yml
1.公共模块pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-api-commons</artifactId>
    <dependencies>
        <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>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.1.0</version>
        </dependency>
    </dependencies>

</project>
2. 将entities拷贝到commons模块

在这里插入图片描述

3.maven打包到公用本地库

用maven打包,发布上传到公用本地库,供其他工程调用。
maven -> clean -> install
在这里插入图片描述

4. 对订单模块和支付模块分别改造

1.删除各自原先有的entities文件夹
2.各自引入自定义的commons包

<dependency>
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
</dependency>

3.测试运行

5. 目前工程样图

在这里插入图片描述

五、Eureka服务注册与发现

1. Eureka基础知识
1.1什么是服务治理

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

1.2什么是服务注册与发现

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

1.3 Eureka 的两个组件

Eureka 包含两个组件: Eureka Server和Eureka Client

1.3.1 Eureka Server

提供服务注册服务,各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直接看到。

1.3.2 Eureka Client

是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30s)。如果EurekaServer在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90s)。

2. 单机Eureka构建步骤
2.1 IDEA生成EurekaServer端服务注册中心
默认端口号:7001
1.建module
2.改pom
3.写yml
4.主启动
5.业务类
2.1.1 EurekaServer 模块 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-euraka-server7001</artifactId>

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

        <dependency><!--引入自定义的api通用包-->
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
    </dependencies>

</project>
2.1.2 application.yml配置
server:
  port: 7001
eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false #false表示不向注册中心注册自己
    fetch-registry: false #表示自己就是注册中心,职责就是维护服务实例,并不需要去检索服务
    #设置Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址 
    service-url: 
     defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/      

2.1.3 主启动类
package com.atguigu.springcloud;

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

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

注意记得使用注解 @EnableEurekaServer 代表本服务为服务注册中心

2.1.4 测试

在这里插入图片描述
测试成功,目前未有服务注册。

2.2 cloud-provider-payment8001注册到Eureka 成为服务提供者
2.2.1 改pom.xml

添加Eureka Client 依赖

<!--eureka-client-->
<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.2.2 主启动类修改

添加 @EnableEurekaClient 注解

2.2.3 修改yml

添加Eureka有关的配置

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url: 
      defaultZone: http://localhost:7001/eureka
2.2.4 测试

1.启动EurakaServer
2.启动EurekaClient
在这里插入图片描述
服务注册成功

2.2.5 自我保护机制

在这里插入图片描述

2.3 cloud-consumer-order80注册到Eureka
2.3.1 修改pom.xml

添加Eureka Client 依赖

<!--eureka-client-->
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.3.2 修改application.yml
server:
  port: 80
spring:
  application:
    name: cloud-order-service

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      defaultZone: http://localhost:7001/eureka
2.3.3 修改主启动类

添加 @EnableEurekaClient 注解

2.3.4 测试

在这里插入图片描述
cloud-consumer-order80注册到Eureka成功

3. 集群Eureka构建步骤
3.1 Eureka集群原理说明

服务注册: 将服务信息注册进注册中心
服务发现: 从注册中心上获取服务信息
实质: 存key-服务名 取value-调用地址
在这里插入图片描述
1.先启动Eureka注册中心
2.启动服务提供者payment支付服务
3.支付服务启动后会把自身信息(比如服务地址以别名方式)注册进Eureka
4.消费者order服务在需要调用接口时,使用服务别名去注册中心获取实际的RPC远程调用地址
5.消费者获得调用地址后,底层实际采用HttpClient技术实现远程调用
6.消费者获得服务地址后会缓存到本地jvm内存中,默认每间隔30s更新一次服务调用地址。

问题:微服务RPC远程服务调用最核心的是什么?
答:高可用!若注册中心只有一个,它出故障了就导致整个服务环境不可用,所以需要搭建Eureka注册中心集群,实现负载均衡+故障容错

集群原理:互相注册,相互守望
多个EurekaServer 互相注册

3.2 EurekaServer集群环境搭建
3.2.1 搭建新的EurekaServer

参考cloud-eureka-server7001搭建cloud-eureka-server7002

3.2.2 修改映射配置

1.找到 C:\Windows\System32\drivers\etc 路径下的hosts文件
在这里插入图片描述
2.修改映射配置添加进hosts
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
在这里插入图片描述

3.2.3 修改两个EurekaServer的yml(以前是单机配置)

单机配置:

server:
  port: 7001
eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false #false表示不向注册中心注册自己
    fetch-registry: false #表示自己就是注册中心,职责就是维护服务实例,并不需要去检索服务
    #设置Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
    service-url:
     defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

集群配置
cloud-eureka-server7001 yml配置

server:
  port: 7001
eureka:
  instance:
    hostname: eureka7001.com   #eureka服务端实例名称
  client:
    register-with-eureka: false #false表示不向注册中心注册自己
    fetch-registry: false #表示自己就是注册中心,职责就是维护服务实例,并不需要去检索服务
    #设置Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
    service-url:
     defaultZone: http://eureka7002.com:7002/eureka/    #相互注册

cloud-eureka-server7002 yml配置

server:
  port: 7002
eureka:
  instance:
    hostname: eureka7002.com
  client:
    register-with-eureka: false #false表示不向注册中心注册自己
    fetch-registry: false #表示自己就是注册中心,职责就是维护服务实例,并不需要去检索服务
    #设置Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
    service-url:
     defaultZone: http://eureka7001.com:7001/eureka/    #相互注册

3.2.4 启动测试

在这里插入图片描述
在这里插入图片描述

测试成功!

4.将支付服务8001发布到Eureka集群配置中
4.1修改cloud-provider-payment8001 yml 配置

之前单机Eureka配置

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service  #微服务名称,一般不要轻易改动,因为入驻Eureka会使用它作为应用名称
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource  #当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动包
    url: jdbc:mysql://localhost:3306/springcloud?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
    username: root
    password: 123456

  devtools:
    restart:
      enabled: true
mybatis:
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: com.atguigu.springcloud.entities  #所有entity别名所在包,mybatis的xml文件中可以使用别名

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      defaultZone: http://localhost:7001/eureka

修改后集群配置:

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service  #微服务名称,一般不要轻易改动,因为入驻Eureka会使用它作为应用名称
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource  #当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动包
    url: jdbc:mysql://localhost:3306/springcloud?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
    username: root
    password: 123456

  devtools:
    restart:
      enabled: true
mybatis:
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: com.atguigu.springcloud.entities  #所有entity别名所在包,mybatis的xml文件中可以使用别名

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版

5.将订单服务80发布到Eureka集群配置中
5.1 修改cloud-consumer-order80 yml配置

之前的单机配置:

server:
  port: 80
spring:
  application:
    name: cloud-order-service

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      defaultZone: http://localhost:7001/eureka   #单机版

集群配置:

server:
  port: 80
spring:
  application:
    name: cloud-order-service

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版
6. 测试集群搭建

1.先启动EurekaServer,7001、7002服务
2.启动服务提供者8001
3.启动消费者80

7.支付服务提供者集群环境构建
7.1 搭建支付服务集群

参考cloud-provider-payment8001,新建cloud-provider-payment8002。
因为之前的配置已经将cloud-provider-payment8001注册到了EurekaServer,所以参考cloud-provider-payment8001的cloud-provider-payment8002应该也是可以注册到Eurake集群中的。这两个服务只是端口上的不同。

7.2修改8001、8002Controller

在8001、8002的PaymentController中,输出目前提供服务的端口号。

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

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

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment) {//此处一定要加@RequestBody,否则80调用8001时,无法传递数据
        log.info("前端传递数据:"+payment);
        int result = paymentService.create(payment);
        log.info("************************插入结果:" +result);
        if (result > 0) {
            return new CommonResult(200,"插入数据库成功,serverPort为: "+serverPort,result);
        } else {
            return new CommonResult(444,"插入数据库失败,serverPort为: "+serverPort, null);
        }
    }

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        Payment payment = paymentService.getPaymentById(id);
        log.info("*************************查询结果:" +payment);
        if (payment != null) {
            return new CommonResult(200, "查询成功,serverPort为: "+serverPort, payment);
        } else {
            return new CommonResult(445,"没有对应id: "+id+" 的记录,serverPort为: "+serverPort, null);
        }
    }
}

7.3测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由于服务cloud-consumer-order80的OrderController中,RestTemplate使用的端口号是固定的8001,所以使用该OrderController提供的路径进行操作,是一直使用8001这个服务。
在这里插入图片描述

而现在有8001、8002两个服务,所以cloud-consumer-order80不再关注具体的ip和端口,只认服务名称。所以现在要将之前在OrderController中写死的地址修改为服务名称——CLOUD-PAYMENT-SERVICE

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderController {

    //public static final String PAYMENT_URL = "http://localhost:8001";
    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
    //实现两个服务之间的调用 restTemplate
    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment) {
        return restTemplate.postForObject(PAYMENT_URL+ "/payment/create", payment ,CommonResult.class);
    }

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
        return restTemplate.getForObject(PAYMENT_URL+ "/payment/get/" +id, CommonResult.class);
    }
}

重新使用cloud-consumer-order80提供的get方式测试:
在这里插入图片描述
现在配置的路径是微服务名称,但是RestTemplate并没有通过服务名称调用服务的能力,不知道这个微服务名称对应的是哪个主机,哪个端口,因为没有开启RestTemplate的利用服务名进行调用和负载均衡功能。

7.4 负载均衡

使用 @LoadBalanced 赋予RestTemplate利用服务名进行调用和客户端负载均衡的能力,否则没有该注解只能通过ip+端口访问。就算注册中心该服务名只对应了一个服务,也需要该注解才可以通过服务名进行调用。
所以需要在cloud-consumer-order80服务的ApplicationContextConfig配置类中为RestTemplate打上@LoadBalanced注解。完成了默认的轮询负载机制。

package com.atguigu.springcloud.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {
    //引入RestTemplate

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
//applicationContext.xml  <bean id="" class="">

再进行测试:
在这里插入图片描述
在这里插入图片描述
8001、8002端口交替服务,负载均衡效果达到。
Ribbon和Eureka整合后,Consumer可以直接调用服务而不用再关心地址和端口号,且服务还有了负载均衡的功能。
消费者只需要关心微服务名称
可能会遇到以下异常,如果一步一步跟着老师配,是没问题的,多等待一会,就正常了。
在这里插入图片描述

8. actuator微服务信息完善
8.1 主机名称:服务名称的修改

当前问题:
在这里插入图片描述
在这里插入图片描述

含有主机名称

对服务的application.yml配置进行配置,设置服务实例的instance-id

server:
  port: 8002

spring:
  application:
    name: cloud-payment-service  #微服务名称,一般不要轻易改动,因为入驻Eureka会使用它作为应用名称
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource  #当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动包
    url: jdbc:mysql://localhost:3306/springcloud?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
    username: root
    password: 123456

  devtools:
    restart:
      enabled: true
mybatis:
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: com.atguigu.springcloud.entities  #所有entity别名所在包,mybatis的xml文件中可以使用别名

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版
  instance:
    instance-id: payment8002    #设置服务实例id

设置前后对比:
在这里插入图片描述

在这里插入图片描述

8.2 设置访问信息有IP信息提示

在这里插入图片描述
未进行设置前,鼠标放在对应的服务实例instance-id上没有ip信息的显示
想要显示ip地址信息,只需要在对应服务的application.yml中配置prefer-ip-address为true即可。
对cloud-provider-payment8002服务的application.yml进行配置:

server:
  port: 8002

spring:
  application:
    name: cloud-payment-service  #微服务名称,一般不要轻易改动,因为入驻Eureka会使用它作为应用名称
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource  #当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动包
    url: jdbc:mysql://localhost:3306/springcloud?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
    username: root
    password: 123456

  devtools:
    restart:
      enabled: true
mybatis:
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: com.atguigu.springcloud.entities  #所有entity别名所在包,mybatis的xml文件中可以使用别名

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版
  instance:
    instance-id: payment8002     #设置服务实例id
    prefer-ip-address: true      #访问路径显示ip地址

测试:
在这里插入图片描述

9. 服务发现Discovery

功能: 对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息。

9.1 实现

1.修改cloud-provider-payment8001的Controller

注入DiscoveryClient

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentService;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private DiscoveryClient discoveryClient;

    @Resource
    private PaymentService paymentService;//服务发现 client端

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

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment) {//此处一定要加@RequestBody,否则80调用8001时,无法传递数据
        log.info("前端传递数据:"+payment);
        int result = paymentService.create(payment);
        log.info("************************插入结果:" +result);
        if (result > 0) {
            return new CommonResult(200,"插入数据库成功,serverPort为: "+serverPort,result);
        } else {
            return new CommonResult(444,"插入数据库失败,serverPort为: "+serverPort, null);
        }
    }

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        Payment payment = paymentService.getPaymentById(id);
        log.info("*************************查询结果:" +payment);
        if (payment != null) {
            return new CommonResult(200, "查询成功,serverPort为: "+serverPort, payment);
        } else {
            return new CommonResult(445,"没有对应id: "+id+" 的记录,serverPort为: "+serverPort, null);
        }
    }

    @GetMapping(value = "/payment/discovery")
    public Object discovery() {
        List<String> services = discoveryClient.getServices();//获得服务列表的信息
        for (String service : services) {
            log.info("*********************service: "+service);
        }
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        for (ServiceInstance instance : instances) {
            log.info(instance.getInstanceId()+ "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" +instance.getUri());
        }

        return this.discoveryClient;
    }

}

2.主启动类上添加@EnableDiscoveryClient注解

package com.atguigu.springcloud;

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

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

测试:先启动EurekaServer,再启动8001主启动类,需要稍等一会儿,访问http://localhost:8001/payment/discovery
结果:
在这里插入图片描述
就通过/payment/discovery 暴露给对方自身的服务信息。
在这里插入图片描述
在这里插入图片描述

10. Eureka的自我保护
10.1 自我保护概述

保护模式主要用于一组客户端和EurekaServer之间存在网络分区场景下的保护。一旦进入保护模式,EurekaServer将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
为了防止EurekaClient可以正常运行,但与EurekaServer网络不同的情况下,EurekaServer不会立即将EurekaClient服务剔除。默认情况下,当EurekaServer在一定时间内没有收到服务实例的心跳,便会将该服务实例从注册表中删除,但要是在短时间内丢失大量的服务实例心跳,便会触发EurekaServer的自我保护机制。可以在Eureka管理界面看到Renews threshold和Renews(last min),当后者(最后一分钟收到的心跳数)小于前者(心跳阈值)的时候,触发保护机制,会出现红色的警告。
在这里插入图片描述

如果在EurekaServer的首页看到以下这段提示,则说明Eureka进入了保护模式:

在这里插入图片描述
突发事件EUREKA可能错误地声称实例在未启动时已启动。续订小于阈值,因此实例不会为了安全而过期。

从警告可以看出,Eureka认为虽然收不到实例心跳,但它还是认为服务实例是健康的,eureka会保护这些实例,不会把它们从注册表中删除。在自我保护模式中,EurekaServer会保护服务注册表中的信息,不会注销任何服务实例。
综上,Eureka的自我保护模式是一种应对网络异常的安全保护措施。它的设计哲学就是宁可保留错误的服务注册信息,也不盲目的注销任何可能健康的服务实例。使用自我保护模式,可以使Eureka集群更加健壮、稳定。

10.2 如何禁止自我保护机制

Eureka的自我保护机制默认是开启的。
EurekaServer端:
使用eureka.server.enable-self-preservation = false 关闭自我保护机制
EurekaServer 7001yml配置:

server:
  port: 7001
eureka:
  instance:
    hostname: eureka7001.com   #eureka服务端实例名称
  client:
    register-with-eureka: false #false表示不向注册中心注册自己
    fetch-registry: false #表示自己就是注册中心,职责就是维护服务实例,并不需要去检索服务
    #设置Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
    service-url:
     defaultZone: http://eureka7002.com:7002/eureka/     #相互注册

  server:
    enable-self-preservation: false    #关闭自我保护机制
    eviction-interval-timer-in-ms: 2000       #心跳时间间隔 2s

在这里插入图片描述
上图红字表示自我保护机制已经被关闭

Eureka Client端:
euraka.instance.lease-renewal-interval-in-seconds: 1 #Eureka客户端向服务端发送心跳的时间间隔,单位为s (默认30s)
euraka.instance.lease-expiration-duration-in-seconds: 2 #Eureka服务端在收到最后一次心跳后等待时间上限,单位为s (默认90s),超时将服务剔除
服务8001 yml配置:

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service  #微服务名称,一般不要轻易改动,因为入驻Eureka会使用它作为应用名称
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource  #当前数据源操作类型
    driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动包
    url: jdbc:mysql://localhost:3306/springcloud?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
    username: root
    password: 123456

  devtools:
    restart:
      enabled: true
mybatis:
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: com.atguigu.springcloud.entities  #所有entity别名所在包,mybatis的xml文件中可以使用别名

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版
  instance:
    instance-id: payment8001      #设置服务实例id
    prefer-ip-address: true       #访问路径显示ip地址
    lease-renewal-interval-in-seconds: 1   #Eureka客户端向服务端发送心跳的时间间隔,单位为s (默认30s)
    lease-expiration-duration-in-seconds: 2   #Eureka服务端在收到最后一次心跳后等待时间上限,单位为s (默认90s),超时将服务剔除

以上配置后,若服务被手动停掉,过了心跳等待时间后,EurekaServer就会将服务立即剔除。

六、 Zookeeper服务注册与发现

1 搭建zookeeper环境

zookeeper是分布式协调工具,可以实现注册中心功能。关闭Linux服务器防火墙后启动zookeeper服务器,zookeeper服务器取代Eureka服务器,zk作为服务注册中心。

在这里插入图片描述
关闭防火墙:

systemctl stop firewalld

查看防火墙状态:

systemctl status firewalld

在这里插入图片描述
查看ip:
在这里插入图片描述
保证虚拟机与windows宿主机能互相ping通
自己在设置上用到的一些文章:
虚拟机ping不通windows主机:https://blog.csdn.net/hskw444273663/article/details/81301470

2 创建服务提供者
2.1.新建module cloud-provider-payment8004
2.2.pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-payment8004</artifactId>
    <dependencies>
        <!--SpringBoot整合web组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入自己定义的api通用包,可以使用Payment支付Entity-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--springBoot整合zookeeper客户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zookeeper-discovery</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>
        </dependency>
    </dependencies>

</project>
2.3 创建yml文件
# 8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
  port: 8004
# 服务名--注册到zookeeper的服务名称
spring:
  application:
    name: cloud-provider-payment
  cloud:
    zookeeper:
      connect-string: 192.168.234.135:2181  #2181是zookeeper配置文件中配置的端口号
      

zookeeper配置文件信息:
在这里插入图片描述

2.4 主启动类

使用@EnableDiscoveryClient注解,用于向使用zookeeper作为注册中心时注册服务

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用zookeeper或者consul作为注册中心时注册服务
public class PaymentMain8004 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8004.class, args);
    }
}

2.5 编写controller
package com.atguigu.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@Slf4j
public class PaymentController {

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

    @RequestMapping(value = "/payment/zk")
    public String paymentzk() {
        return "springcloud with zookeeper: " + serverPort + "\t" + UUID.randomUUID().toString();
    }
}

2.6 启动8004注册进zookeeper

1.启动zookeeper
2.启动主启动类
有可能会因为jar包与zookeeper版本问题导致启动报错。
在这里插入图片描述
要是存在该问题,则需要将依赖自带的zookeeper jar包排除掉,重新引入与安装的zookeeper版本一致的zookeeper jar包


    <!--SpringBoot整合Zookeeper客户端-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
        <exclusions>
            <!--先排除自带的zookeeper3.5.3-->
            <exclusion>
                <groupId>org.apache.zookeeper</groupId>
                <artifactId>zookeeper</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--添加zookeeper3.4.6版本 -->
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.7</version>
    </dependency>

在这里插入图片描述
在这里插入图片描述
说明微服务的提供者 支付模块8004成功注册到了zookeeper

3 cloud-provide-payment模块注册zookeeper验证

在这里插入图片描述
在这里插入图片描述
用json工具转换后:

{
  "name": "cloud-provider-payment",
  "id": "9f6de68e-7135-4607-bdc8-c028efb92674",
  "address": "LAPTOP-F75EJ3M7",
  "port": 8004,
  "sslPort": null,
  "payload": {
    "@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
    "id": "application-1",
    "name": "cloud-provider-payment",
    "metadata": {}
  },
  "registrationTimeUTC": 1637394403732,
  "serviceType": "DYNAMIC",
  "uriSpec": {
    "parts": [
      {
        "value": "scheme",
        "variable": true
      },
      {
        "value": "://",
        "variable": false
      },
      {
        "value": "address",
        "variable": true
      },
      {
        "value": ":",
        "variable": false
      },
      {
        "value": "port",
        "variable": true
      }
    ]
  }
}

每个微服务作为zNode节点放到了zookeeper中
思考:服务节点是临时节点还是持久节点?
结论:节点是临时性的。服务注册到zookeeper中,当服务离线又重新注册到zookeeper中,服务的id是不一样的。

4 服务消费者创建与注册到zookeeper
4.1 新建cloud-consumerzk-order80

1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumerzk-order80</artifactId>
    <dependencies>
        <dependency><!--引入自定义的api通用包-->
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--springBoot整合zookeeper客户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zookeeper-discovery</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>
        </dependency>

    </dependencies>


</project>

2.application.yml

# 80表示注册到zookeeper服务器的支付服务提供者端口号
server:
  port: 80
# 服务名--注册到zookeeper的服务名称
spring:
  application:
    name: cloud-consumer-order
  cloud:
    zookeeper:
      connect-string: 192.168.234.135:2181  #2181是zookeeper配置文件中配置的端口号

3.主启动类

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

4.业务类(配置Bean、Controller)

package com.atguigu.springcloud.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
package com.atguigu.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderZKController {
    public static final String INVOKE_URL = "http://cloud-provider-payment";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping(value = "/consumer/payment/zk")
    public String paymentInfo() {
        String result = restTemplate.getForObject(INVOKE_URL+"/payment/zk", String.class);
        return result;
    }
}

4.2 结果

在这里插入图片描述
说明orderZK80和provider8004都注册到了zookeeper上
在这里插入图片描述

七、Consul服务注册与发现

1.Consul简介

是什么:Consul是一套开源的分布式服务发现配置管理系统,由HashCorp公司用go语言开发。它提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。

能干嘛:
1.服务发现:提供HTTP和DNS两种发现方式
2.健康检测:支持多种方式,HTTP、TCP、Docker、Shell脚本定制化
3.KV存储:Key、Value的存储方式
4.多数据中心:Consul支持多数据中心
5.可视化Web界面

去哪下: https://www.consul.io/downloads.html
怎么玩: https://www.springcloud.cc/spring-cloud-consul.html

2.安装并运行Consul

下载地址:https://www.consul.io/downloads
官网安装说明:https://learn.hashicorp.com/tutorials/consul/get-started-install
下载完成后,只有一个consul.exe的文件,在目录下使用cmd,查看版本号信息。
在这里插入图片描述
输入以下命令,使用开发模式启动:

consul agent -dev

在这里插入图片描述

通过地址访问Consul的首页 http://localhost:8500
结果页面
在这里插入图片描述

3. 服务提供者注册进consul
3.1新建Module支付服务provider8006

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-consul-payment8006</artifactId>
    <dependencies>
        <!--springcloud consul server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>

        <!--SpringBoot整合web组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!--引入自己定义的api通用包,可以使用Payment支付Entity-->
        <dependency>
            <groupId>org.example</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>
        </dependency>
    </dependencies>

</project>

application.yml

#服务端口号
server:
  port: 8006
spring:
  application:
    name: consul-provider-payment
  #consul注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}   #hostname: 127.0.0.1

主启动类:

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

controller:

package com.atguigu.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@Slf4j
public class PaymentController {

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

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

访问: http://localhost:8006/payment/consul
在这里插入图片描述

4. 服务消费者注册进consul
4.1 新建module 消费服务cloud-consumerconsul-order80

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumerconsul-order80</artifactId>
    <dependencies>
        <dependency><!--引入自定义的api通用包-->
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!--springcloud consul server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</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>
        </dependency>

    </dependencies>

</project>

application.yml

#服务端口号
server:
  port: 80
spring:
  application:
    name: cloud-consumer-order
  #consul注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}   #hostname: 127.0.0.1


主启动类:
代码省略,主要就是两个注解

@SpringBootApplication
@EnableDiscoveryClient

配置config bean

package com.atguigu.springcloud.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {

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

controller:

package com.atguigu.springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderConsulController {
    public static final String INVOKE_URL = "http://consul-provider-payment";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping(value = "/consumer/payment/consul")
    public String paymentInfo() {
        String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul", String.class);
        return result;
    }
}

4.2 测试

在这里插入图片描述
在这里插入图片描述

5. 三个注册中心的异同点
组件名语言CAP服务健康检查对外暴露接口Spring Cloud集成
EurekaJavaAP可配支持HTTP已集成
ConsulGoCP支持HTTP/DNS已集成
ZookeeperJavaCP支持客户端已集成

CAP:
C: Consistency(强一致性)
A: Availability (可用性)
P: Partition tolerance (分区容错性)
CAP理论关注粒度是数据,而不是整体系统设计的策略

CAP的核心理论是:一个分布式系统不可能同时很好的满足一致性、可用性和分区容错性这三个要求,因此,根据CAP原理将NoSQL数据库分成了满足CA原则、满足CP原则、满足AP原则三大类。
CA:单点集群,满足一致性、可用性的系统,通常在扩展性上不太强大
CP:满足一致性、分区容错性的系统,通常性能不是特别高
AP:满足可用性、分区容错性的系统,通常可能对一致性要求低一些

八、Ribbon负载均衡服务调用

1.概述

Ribbon是什么:spring cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡工具。简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时、重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询、随机连接等)去连接这些服务器。我们很容易使用Ribbon实现自定义的负载均衡算法。
能干嘛:
LB负载均衡是什么:简单的说就是将用户的请求平均的分配到多个服务上,从而达到HA(高可用)。一句话就是负载均衡+RestTemplate调用。

常见的负载均衡有软件Nginx、LVS,硬件F5等。

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

集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件如F5,也可以是软件如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方。
进程内LB:将LB的逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可以用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内LB,它只是一个类库,集成与消费方进程,消费方通过它来获取到服务提供方的地址。
前面有提到80通过轮询负载访问8001/8002。

2.Ribbon负载均衡演示
2.1架构说明

总结:Ribbon其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和Eureka结合只是其中一个实例。

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

2.2 Ribbon依赖说明

之前实的8001、8002、7001、7002、80样例没有引入spring-cloud-starter-ribbon也可以使用轮询的负载均衡,猜测spring-cloud-starter-netflix-euraka-client自带了spring-cloud-starter-ribbon引用,证明如下:
在这里插入图片描述

2.3 RestTemplate的使用
2.3.1 getForObject方法/getForEntity方法

getForObject方法: 返回对象为响应体中数据转换成的对象,基本上可以理解为json
getForEntity: 返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderController {

    //public static final String PAYMENT_URL = "http://localhost:8001";
    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
    //实现两个服务之间的调用 restTemplate
    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment) {
        return restTemplate.postForObject(PAYMENT_URL+ "/payment/create", payment ,CommonResult.class);
    }

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
        return restTemplate.getForObject(PAYMENT_URL+ "/payment/get/" +id, CommonResult.class);
    }

    @GetMapping("/consumer/payment/getForEntity/{id}")
    public CommonResult<Payment> getPayment2(@PathVariable("id") Long id) {
        ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+ "/payment/get/" +id, CommonResult.class);
        log.info(entity.getStatusCode().toString()+ "\t" + entity.getHeaders().toString());//getStatusCode()是服务提供者设置的状态码
        if (entity.getStatusCode().is2xxSuccessful()) {
            return entity.getBody();
        } else {
            return new CommonResult<>(444, "操作失败");
        }

    }
    
}
3. Ribbon核心组件IRule

IRule接口: 根据特定的算法从服务列表中选取一个要访问的服务

/*
*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.loadbalancer;

/**
 * Interface that defines a "Rule" for a LoadBalancer. A Rule can be thought of
 * as a Strategy for loadbalacing. Well known loadbalancing strategies include
 * Round Robin, Response Time based etc.
 * 
 * @author stonse
 * 
 */
public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

在这里插入图片描述

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

修改cloud-consumer-order80

注意配置细节
官方文档明确给出了警告:自定义的配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的。

新建package com.atguigu.myrule
上面的包中新建MySelfRule规则类
在这里插入图片描述

package com.atguigu.myrule;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MySelfRule {
    @Bean
    public IRule myRule() {
        return new RandomRule();//定义为随机
    }
}

cloud-consumer-order80主启动类添加@RibbonClient注解

package com.atguigu.springcloud;

import com.atguigu.myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain80.class, args);
    }
}

测试
在这里插入图片描述
测试结果为调用服务的端口为8002与8001随机,不再是之前的轮询方式。

4.Ribbon负载均衡算法

首先恢复为原来的轮询的负载均衡算法,将主启动类上的注解@RibbonClient(name = “CLOUD-PAYMENT-SERVICE”, configuration = MySelfRule.class)注释掉即可。

4.1 原理

负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,要是服务重启,rest接口计数重新从1开始。

List <ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
如  List [0] instances = 127.0.0.1:8002
    List [1] instances = 127.0.0.1:8001

8001和8002组合成为集群,他们共计两台机器,集群总数为2,按照轮询算法原理:
当请求总数为1时:1 % 2 = 1,则获得服务地址为 127.0.0.1:8001
当请求总数为2时:2 % 2 = 0,则获得服务地址为 127.0.0.1:8002
当请求总数为3时:3 % 2 = 1,则获得服务地址为 127.0.0.1:8001
以此类推…

4.2 手写负载均衡轮询算法

原理+JUC(CAS+自旋锁的复习)

4.2.1 7001/7002集群启动(已有)
4.2.2 8001/8002微服务改造,使用自己写的负载均衡器。

改造8001 controller 添加请求 /payment/lb 对应的方法

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentService;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private DiscoveryClient discoveryClient;

    @Resource
    private PaymentService paymentService;//服务发现 client端

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

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment) {//此处一定要加@RequestBody,否则80调用8001时,无法传递数据
        log.info("前端传递数据:"+payment);
        int result = paymentService.create(payment);
        log.info("************************插入结果:" +result);
        if (result > 0) {
            return new CommonResult(200,"插入数据库成功,serverPort为: "+serverPort,result);
        } else {
            return new CommonResult(444,"插入数据库失败,serverPort为: "+serverPort, null);
        }
    }

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        Payment payment = paymentService.getPaymentById(id);
        log.info("*************************查询结果:" +payment);
        if (payment != null) {
            return new CommonResult(200, "查询成功,serverPort为: "+serverPort, payment);
        } else {
            return new CommonResult(445,"没有对应id: "+id+" 的记录,serverPort为: "+serverPort, null);
        }
    }

    @GetMapping(value = "/payment/discovery")
    public Object discovery() {
        List<String> services = discoveryClient.getServices();//获得服务列表的信息
        for (String service : services) {
            log.info("*********************service: "+service);
        }
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        for (ServiceInstance instance : instances) {
            log.info(instance.getInstanceId()+ "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" +instance.getUri());
        }

        return this.discoveryClient;
    }

    @GetMapping(value = "/payment/lb")
    public String getPaymentLB() {
        return serverPort;
    }

}

改造8002 controller 添加请求 /payment/lb 对应的方法

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

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

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment) {//此处一定要加@RequestBody,否则80调用8001时,无法传递数据
        log.info("前端传递数据:"+payment);
        int result = paymentService.create(payment);
        log.info("************************插入结果:" +result);
        if (result > 0) {
            return new CommonResult(200,"插入数据库成功,serverPort为: "+serverPort,result);
        } else {
            return new CommonResult(444,"插入数据库失败,serverPort为: "+serverPort, null);
        }
    }

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        Payment payment = paymentService.getPaymentById(id);
        log.info("*************************查询结果:" +payment);
        if (payment != null) {
            return new CommonResult(200, "查询成功,serverPort为: "+serverPort, payment);
        } else {
            return new CommonResult(445,"没有对应id: "+id+" 的记录,serverPort为: "+serverPort, null);
        }
    }

    @GetMapping(value = "/payment/lb")
    public String getPaymentLB() {
        return serverPort;
    }
}

4.2.3 订单微服务改造

1.ApplicationContextBean去掉注解@LoadBalanced
因为要保证使用的是自己写的负载均衡器,这个注解使用的是Ribbon的负载均衡器

2.LoadBalancer接口

package com.atguigu.springcloud.lb;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

public interface LoadBalancer {
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

3.MyLB

package com.atguigu.springcloud.lb;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class MyLB implements LoadBalancer{
    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement() {
        int current;
        int next;
        do {
            current = this.atomicInteger.get();
            next = current>= Integer.MAX_VALUE ? 0 : current+1;
        } while (!this.atomicInteger.compareAndSet(current, next));
        System.out.println("**********访问次数 next: "+next);
        return next;
    }

    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }


}

4.OrderController

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.lb.LoadBalancer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.net.URI;
import java.util.List;

@RestController
@Slf4j
public class OrderController {

    //public static final String PAYMENT_URL = "http://localhost:8001";
    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
    //实现两个服务之间的调用 restTemplate
    @Resource
    private RestTemplate restTemplate;

    @Resource
    private LoadBalancer loadBalancer;

    @Resource
    private DiscoveryClient discoveryClient;

    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment) {
        return restTemplate.postForObject(PAYMENT_URL+ "/payment/create", payment ,CommonResult.class);
    }

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
        return restTemplate.getForObject(PAYMENT_URL+ "/payment/get/" +id, CommonResult.class);
    }

    @GetMapping("/consumer/payment/getForEntity/{id}")
    public CommonResult<Payment> getPayment2(@PathVariable("id") Long id) {
        ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+ "/payment/get/" +id, CommonResult.class);
        log.info(entity.getStatusCode().toString()+ "\t" + entity.getHeaders().toString());//getStatusCode()是服务提供者设置的状态码
        if (entity.getStatusCode().is2xxSuccessful()) {
            return entity.getBody();
        } else {
            return new CommonResult<>(444, "操作失败");
        }

    }

    @GetMapping(value = "/consumer/payment/lb")
    public String getPaymentLB() {
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");//得到对应服务名称的实例

        if (instances == null || instances.size() <= 0) {
            return null;
        }
        ServiceInstance serviceInstance = loadBalancer.instances(instances);
        URI uri = serviceInstance.getUri();
        return restTemplate.getForObject(uri + "/payment/lb", String.class);
    }

}

5.测试

在这里插入图片描述
结果:8001、8002交替服务

九、OpenFeign服务接口调用

在这里插入图片描述

1. 概述

Feign是什么:
Feign 是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud 对Feign进行了封装,使其支持Spring MVC 标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
Feign能干什么:
Feign旨在使编写Java Http客户端变得更容易。前面在使用 Ribbon + RestTemplate时,利用RestTemplate对Http请求进行封装,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign 在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。
Feign集成了Ribbon。
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

Feign和OpenFeign两者的区别

FeignOpenFeign
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。OpenFeign 是Spring Cloud在Feign的基础上支持了Spring MVC的注解,如@RequestMapping等等,OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
2.OpenFeign使用步骤

接口+注解:微服务调用接口+@FeignClient

2.1 新建cloud-consumer-feign-order80, Feign在消费端使用
2.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-feign-order80</artifactId>
    <dependencies>
        <!--openFeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency><!--引入自定义的api通用包-->
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

    </dependencies>

</project>
2.3 application.yml
server:
  port: 80

eureka:
  client:
    register-with-eureka: false  #不将自己注册进eureka
    service-url: 
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka 

主启动类 OrderFeignMain80.java

package com.atguigu.springcloud;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients   //使用Feign,激活并开启
public class OrderFeignMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderFeignMain80.class, args);
    }
}

2.4 业务类
2.4.1 业务逻辑接口+@FeignClient配置调用provider服务
2.4.2 新建PaymentFeignService接口并新增注解@FeignClient

接口的方法与提供服务的controller一致

package com.atguigu.springcloud.service;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

//@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService { //接口的方法与提供服务的controller方法一致
    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}

2.4.3 控制层Controller
package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderFeignController {
    
    @Resource
    private PaymentFeignService paymentFeignService;
    
    @GetMapping(value = "/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        return paymentFeignService.getPaymentById(id);
    }
}

2.5 测试

先启动两个Eureka集群7001/7002
再启动两个微服务8001/8002
启动OpenFeign
http://localhost/consumer/payment/get/1
在这里插入图片描述
Feign自带负载均衡配置,以上测试8001、8002间隔提供服务

在这里插入图片描述

3.OpenFeign的超时控制
3.1 超时设置,故意设置超时演示出错的情况

服务提供方8001故意写暂停程序
8001 PaymentController.java paymentFeignTimeout()

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentService;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private DiscoveryClient discoveryClient;

    @Resource
    private PaymentService paymentService;//服务发现 client端

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

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment) {//此处一定要加@RequestBody,否则80调用8001时,无法传递数据
        log.info("前端传递数据:"+payment);
        int result = paymentService.create(payment);
        log.info("************************插入结果:" +result);
        if (result > 0) {
            return new CommonResult(200,"插入数据库成功,serverPort为: "+serverPort,result);
        } else {
            return new CommonResult(444,"插入数据库失败,serverPort为: "+serverPort, null);
        }
    }

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        Payment payment = paymentService.getPaymentById(id);
        log.info("*************************查询结果:" +payment);
        if (payment != null) {
            return new CommonResult(200, "查询成功,serverPort为: "+serverPort, payment);
        } else {
            return new CommonResult(445,"没有对应id: "+id+" 的记录,serverPort为: "+serverPort, null);
        }
    }

    @GetMapping(value = "/payment/discovery")
    public Object discovery() {
        List<String> services = discoveryClient.getServices();//获得服务列表的信息
        for (String service : services) {
            log.info("*********************service: "+service);
        }
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        for (ServiceInstance instance : instances) {
            log.info(instance.getInstanceId()+ "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" +instance.getUri());
        }

        return this.discoveryClient;
    }

    @GetMapping(value = "/payment/lb")
    public String getPaymentLB() {
        return serverPort;
    }
    
    @GetMapping(value = "/payment/feign/timeout")
    public String paymentFeignTimeout() {
        try {
            TimeUnit.SECONDS.sleep(3);//暂停几秒钟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        return serverPort;
    }
}

服务消费方80 PaymentFeignService添加超时方法 paymentFeignTimeout()

package com.atguigu.springcloud.service;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

//@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService { //接口的方法与提供服务的controller方法一致
    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);

    @GetMapping(value = "/payment/feign/timeout")
    public String paymentFeignTimeout() ;//在暴露的CLOUD-PAYMENT-SERVICE微服务下找请求地址为/payment/feign/timeout方法
}

服务消费方80 OrderFeignController 添加超时方法 paymentFeignTimeout()

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderFeignController {

    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping(value = "/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        return paymentFeignService.getPaymentById(id);
    }

    @GetMapping(value = "/consumer/payment/feign/timeout")
    public String paymentFeignTimeout() {
        //openFeign 底层 ribbon , 客户端一般默认等待一秒钟
        return paymentFeignService.paymentFeignTimeout();
    }
}

测试
测试服务提供者直接提供的接口:
在这里插入图片描述
8001正常访问,三秒后返回端口

访问消费者接口
http://localhost/consumer/payment/feign/timeout
在这里插入图片描述
OpenFeign默认等待一秒钟,一秒钟后得不到微服务提供者的结果,就超时后报错。为了避免服务提供方需要花费处理的时间超过Feign 客户端默认的等待时间而报错,我们就需要设置Feign客户端的超时控制。

在yml中开启配置:

server:
  port: 80

eureka:
  client:
    register-with-eureka: false  #不将自己注册进eureka
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

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

测试:
三秒钟后返回端口号,不再报错
在这里插入图片描述

4.OpenFeign日志打印功能
4.1 是什么

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

4.2 日志级别
级别描述
NONE默认的,不显示任何日志
BASIC仅记录请求方法、URL、响应状态码和执行时间
HEADERS除了BASIC中定义的信息之外,还有请求和响应的头信息
FULL除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据
4.3 配置日志bean
package com.atguigu.springcloud.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

4.4 YML文件里需要开启日志的Feign客户端
server:
  port: 80

eureka:
  client:
    register-with-eureka: false  #不将自己注册进eureka
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

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

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

重启80 OrderFeign服务 访问 http://localhost/consumer/payment/get/1
打印日志如下:
在这里插入图片描述

十、Hystrix断路器

1.概述

分布式系统面临的问题:复杂分布式体系结构的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的失败。
**服务雪崩:**多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和C又调用其他微服务,这就是所谓的 “扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的 “雪崩效应”。

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

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

“断路器” 本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

能干嘛: 服务降级、服务熔断、接近实时的监控…
官网资料:https://github.com/Netflix/Hystrix/wiki/How-To-Use
Hystrix已停更进维

2. Hystrix重要概念
2.1服务降级

服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback
哪些情况会触发降级:
1.程序运行异常
2.超时
3.服务熔断触发服务降级
4.线程池/信号量打满也会导致服务降级

2.2 服务熔断

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

2.3 服务限流

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

3. Hystrix案例
3.1 构建
3.1.1 新建cloud-provider-hystrix-payment8001
3.1.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-hystrix-payment8001</artifactId>
    <dependencies>
        <!--hystrix-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

    </dependencies>

</project>
3.1.3 application.yml
server:
  port: 8001
spring:
  application:
    name: cloud-provider-hystrix-payment

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版

3.1.4 主启动类
package com.atguigu.springcloud;

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

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

3.1.5 业务类

service

package com.atguigu.springcloud.service;

import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    /**
     * 正常访问,肯定没有问题的方法
     * @param id
     * @return
     */
    public String paymentInfo_OK(Integer id) {
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_OK,id: " +id+ "\t"+ "^_^";
    }

    public String paymentInfo_TimeOut(Integer id) {
        int timeNumber = 3;
        //暂停几秒钟
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_TimeOut,id: " +id+ "\t"+ "0_0 耗时: " +timeNumber+ "秒钟";
    }
}

controller

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

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

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

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

3.1.6 测试

启动7001/7002
启动cloud-provider-hystrix-payment8001
在这里插入图片描述
访问 http://localhost:8001/payment/hystrix/ok/666,立即返回结果
在这里插入图片描述
访问 http://localhost:8001/payment/hystrix/timeout/666 ,三秒钟后返回
在这里插入图片描述
访问上述module均ok

以上述为根基平台,从正确 -> 错误 -> 降级熔断 -> 恢复

3.2 高并发测试

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

3.2.1 Jmeter压测测试

开启Jmeter,来两万个并发压死8001,两万个请求都去访问 paymentInfo_TimeOut
在这里插入图片描述
在这里插入图片描述
然后再去访问一下 http://localhost:8001/payment/hystrix/ok/666

演示结果:
访问 http://localhost:8001/payment/hystrix/ok/666 转圈圈,不再立即返回结果
为什么会转圈圈:tomcat的默认工作线程数被打满了,没有多余的请求来分解压力和处理

3.2.2 Jmeter压测结论

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

3.3 新建80消费者加入
3.3.1 新建 cloud-consumer-feign-hystrix-order80

在这里插入图片描述

3.3.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-feign-hystrix-order80</artifactId>
    <dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

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

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

        <dependency><!--引入自定义的api通用包-->
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

</project>
3.3.3 application.yml
server:
  port: 80

eureka:
  client:
    register-with-eureka: false  #不将自己注册进eureka
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
3.3.4 主启动类
package com.atguiggu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

3.3.5 业务类

OrdertHystrixService

package com.atguiggu.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

//@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT") //接口+@FeignClient注解
public interface OrderHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfo_OK(@PathVariable("id") Integer id);

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

OrderHystrixController

package com.atguiggu.springcloud.controller;

import com.atguiggu.springcloud.service.OrderHystrixService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderHystrixController {

    @Resource
    private OrderHystrixService orderHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    String paymentInfo_OK(@PathVariable("id") Integer id) {
        String result = orderHystrixService.paymentInfo_OK(id);
        return result;
    }

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

3.3.6 正常测试

访问 http://localhost/consumer/payment/hystrix/ok/888 立即返回结果
在这里插入图片描述
访问 http://localhost/consumer/payment/hystrix/timeout/888 超时,因为Feign消费者默认1秒钟没有响应就会超时。
在这里插入图片描述
80的application.yml中添加feign的超时设置:

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

重启80Order消费者后,再次访问 http://localhost/consumer/payment/hystrix/timeout/888 结果正常
在这里插入图片描述

3.3.7 高并发测试及结论

2W个线程压8001
消费端80微服务再去访问正常的OK微服务8001
要么转圈等待,要么消费端报超时错误。

故障现象和导致原因:
8001同一层次的其他接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完毕。80此时调用8001,客户端访问响应缓慢,转圈。

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

3.3.8 如何解决?解决的要求

超时导致服务器变慢(转圈) ----> 超时不再等待
出错(宕机或者程序运行出错)----> 出错要有兜底

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

3.4 服务降级
3.4.1 降级配置

@HystrixCommand

3.4.2 8001先从自身找问题

设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作为服务降级的fallback。

3.4.3 8001 fallback

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

package com.atguigu.springcloud.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    /**
     * 正常访问,肯定没有问题的方法
     * @param id
     * @return
     */
    public String paymentInfo_OK(Integer id) {
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_OK,id: " +id+ "\t"+ "^_^";
    }

    @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = { //fallbackMethod:兜底方法
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") //线程执行超时时间为3s,3s以内走正常逻辑,超过3s,调用指定方法
    })
    public String paymentInfo_TimeOut(Integer id) {
        int timeNumber = 5;
        //暂停几秒钟
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_TimeOut,id: " +id+ "\t"+ "0_0 耗时: " +timeNumber+ "秒钟";
    }

    public String paymentInfo_TimeOutHandler(Integer id) {
        return "调用支付接口超时或者异常:\t" + "\t当前线程:"+Thread.currentThread().getName() + " paymentInfo_TimeOutHandler, id: " +id;
    }
}

主启动类激活:添加新注解@EnableCircuitBreaker 激活

package com.atguigu.springcloud;

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

@SpringBootApplication
@EnableEurekaClient //注册进euraka
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
}

启动并测试:
在这里插入图片描述

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

重新启动8001后访问 http://localhost:8001/payment/hystrix/timeout/666,立即返回结果,没有等待,同样走了兜底的方法。
在这里插入图片描述

3.4.4 80 fallback

Hystrix 可以放在消费端,也可以放在服务端,一般是放在客户端。
80微服务,也可以更好地保护自己,依样画葫芦,照8001进行客户端降级保护。
1.Order application.yml修改

server:
  port: 80

eureka:
  client:
    register-with-eureka: false  #不将自己注册进eureka
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

feign:
  hystrix:
    enabled: true  #开启支持hystrix  为什么刚才8001服务端没有开启这个属性,因为服务端没有使用feign


# 之前controller在OrderHystrixController @HystrixCommand配置的超时时间未生效
# 关键在于feign:hystrix:enabled:true  官网解释 "Feign将使用断路器包装所有方法", 也就是将@FeignClient标记的那个service接口下所有的方法都进行了
# hystrix包装, 类似于在所有的方法上面加了@HystrixCommand, 这些方法会应用一个默认的超时时间为1s, 所以service方法上相当于还有一个1s的超时时间
# 1s就会报异常, controller 立马进入备用方法, controller上其他的设置的3秒超时的方法就没有效果了
# 改变这个默认的超时时间方法 如下:
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 6000 # 将默认的1s超时修改
# 这里设置的timeoutInMilliseconds会与方法上的注解设置的超时时间比较, 取最小值, 同时也会算上设置的ribbon的时间, 也就是三者的最小值

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

# 设置feign客户端的超时时间,用了@FeignCLient注解,默认会优先触发feign的过期时间而报错,走兜底方法。但是开启了
# feign:hystrix:enabled:true,设置的ribbon的超时时间就没用了,改为了默认的1s

2.主启动加注解@EnableHystrix 该注解中包含了@EnableCircuitBreaker

package com.atguiggu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

3.业务类及测试
将服务提供者 8001的超时时间进行修改一下,设置paymentInfo_TimeOut方法需要执行5秒,超时时间为7秒,服务端8001超时便进行服务降级执行paymentInfo_TimeOutHandler方法

package com.atguigu.springcloud.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    /**
     * 正常访问,肯定没有问题的方法
     * @param id
     * @return
     */
    public String paymentInfo_OK(Integer id) {
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_OK,id: " +id+ "\t"+ "^_^";
    }

    @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = { //fallbackMethod:兜底方法
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "7000") //线程执行超时时间为7s,7s以内走正常逻辑,超过7s,调用指定方法
    })
    public String paymentInfo_TimeOut(Integer id) {
        int timeNumber = 5;
        //int age = 10/0;
        //暂停几秒钟
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_TimeOut,id: " +id+ "\t"+ "0_0 耗时: " +timeNumber+ "秒钟";
    }

    public String paymentInfo_TimeOutHandler(Integer id) {
        return "调用8001支付接口超时或者异常:\t" + "\t当前线程:"+Thread.currentThread().getName() + " paymentInfo_TimeOutHandler, id: " +id;
    }
}

直接调用服务8001的接口,因为paymentInfo_TimeOut需要执行5s,而设置的超时时间为7s,所以直接访问服务端8001不会服务降级。

下面使用order80消费者进行服务调用:
Order80 controller

package com.atguiggu.springcloud.controller;

import com.atguiggu.springcloud.service.OrderHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderHystrixController {

    @Resource
    private OrderHystrixService orderHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id) {
        String result = orderHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "OrderTimeOutFallbackMethod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") //客户端只等待服务端1.5s
    })
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
        String result = orderHystrixService.paymentInfo_TimeOut(id);
        return result;
    }

    public String OrderTimeOutFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者80, 对方支付系统繁忙请稍后再试或者自己运行出错请检查自己-.-";
    }

    @GetMapping("/consumer/payment/hystrix/timeout2/{id}")
    @HystrixCommand(fallbackMethod = "OrderTimeOutFallbackMethod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "6000")
    })
    public String paymentInfo_TimeOut2(@PathVariable("id") Integer id){
        String result = orderHystrixService.paymentInfo_TimeOut(id);
        return result;
    }
}


该controller中,有paymentInfo_TimeOut、paymentInfo_TimeOut2两个方法。
paymentInfo_TimeOut设置的超时时间为2s,且配置文件中设置的@FeignClient标注的service中所有方法超时时间为6s(默认为1s),以及ribbon超时时间为15秒,所以该方法的超时时间为2s。由于所调用服务8001的方法需要执行5s,所以当访问http://localhost:8001/payment/hystrix/timeout/8887 时会超时,进而服务降级调用OrderTimeOutFallbackMethod方法。

验证:
两秒后返回该信息
在这里插入图片描述
paymentInfo_TimeOut2方法在注解中设置的超时时间为6s,综合配置文件的设置,该方法的超时时间为6s。由于所调用服务8001的方法需要执行5s,所以当访问http://localhost:8001/payment/hystrix/timeout2/666 时不会超时,消费端和服务端都不会服务降级。

验证:
5s后返回信息:
在这里插入图片描述

3.4.5 目前问题

1.每个业务方法对应一个兜底的方法,代码膨胀
2.公共的兜底方法和自定义的兜底方法需要分开

3.4.6 解决代码膨胀问题

feign接口系列
@DefaultProperties(defaultFallback = “”)
在类上标注@DefaultProperties(defaultFallback = “”)注解设置类中所有方法的默认公共兜底方法。若单独设置了兜底方法的则调用自己设置的,否则调用该注解设置的兜底方法。

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

controller配置
设置全局服务降级方法并在controller上打上注解@DefaultProperties(defaultFallback = “payment_Global_FallbackMethod”)
服务过期时间仍写在方法上的注解中

package com.atguiggu.springcloud.controller;

import com.atguiggu.springcloud.service.OrderHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {

    @Resource
    private OrderHystrixService orderHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id) {
        String result = orderHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
//    @HystrixCommand(fallbackMethod = "OrderTimeOutFallbackMethod", commandProperties = {
//            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") //客户端只等待服务端1.5s
//    })
    @HystrixCommand(commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
    }) //类加了@DefaultProperties注解,该方法上没有写具体方法名,就会调用@DefaultProperties配置的全局服务降级方法
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
        String result = orderHystrixService.paymentInfo_TimeOut(id);
        return result;
    }

    public String OrderTimeOutFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者80, 对方支付系统繁忙请稍后再试或者自己运行出错请检查自己-.-";
    }

    @GetMapping("/consumer/payment/hystrix/timeout2/{id}")
    @HystrixCommand(fallbackMethod = "OrderTimeOutFallbackMethod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "6000")
    })
    public String paymentInfo_TimeOut2(@PathVariable("id") Integer id){
        String result = orderHystrixService.paymentInfo_TimeOut(id);
        return result;
    }

    //全局fallback 全局方法不能有参数,否则会报找不到方法的异常
    public String payment_Global_FallbackMethod() {
        return "Global异常信息处理,请稍后再试";
    }
}

测试结果:
在这里插入图片描述

3.4.7 解决服务降级方法和业务逻辑混在一起的问题

服务降级,客户端去调用服务端,碰上服务端宕机或关闭
本次案例服务降级处理是在客户端80完成实现的,与8001没有关系。只需要为Feign客户端定义的接口 (打了@FeignClient注解的接口) 添加一个服务降级处理的实现类即可实现解耦。

未来需要面对的异常:
1.运行时异常
2.超时
3.宕机

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

新建OrderFallbackService实现OrderHystrixService接口:

package com.atguiggu.springcloud.service;

import org.springframework.stereotype.Component;

@Component
public class OrderFallbackService implements OrderHystrixService{
    @Override
    public String paymentInfo_OK(Integer id) {
        return "------OrderFallbackService ----paymentInfo_OK fallback";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) {
        return "------OrderFallbackService ----paymentInfo_TimeOut fallback";
    }
}

实现了@FeignClient注解的接口,在该注解中添加 fallback = OrderFallbackService.class

package com.atguiggu.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

//@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = OrderFallbackService.class) //接口+@FeignClient注解
public interface OrderHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfo_OK(@PathVariable("id") Integer id);

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

80的controller没改变:

package com.atguiggu.springcloud.controller;

import com.atguiggu.springcloud.service.OrderHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {

    @Resource
    private OrderHystrixService orderHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id) {
        String result = orderHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
//    @HystrixCommand(fallbackMethod = "OrderTimeOutFallbackMethod", commandProperties = {
//            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") //客户端只等待服务端1.5s
//    })
    @HystrixCommand(commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
    }) //类加了@DefaultProperties注解,该方法上没有写具体方法名,就会调用@DefaultProperties配置的全局服务降级方法
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
        String result = orderHystrixService.paymentInfo_TimeOut(id);
        return result;
    }

    public String OrderTimeOutFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者80, 对方支付系统繁忙请稍后再试或者自己运行出错请检查自己-.-";
    }

    @GetMapping("/consumer/payment/hystrix/timeout2/{id}")
    @HystrixCommand(fallbackMethod = "OrderTimeOutFallbackMethod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "6000")
    })
    public String paymentInfo_TimeOut2(@PathVariable("id") Integer id){
        String result = orderHystrixService.paymentInfo_TimeOut(id);
        return result;
    }

    //全局fallback 全局方法不能有参数,否则会报找不到方法的异常
    public String payment_Global_FallbackMethod() {
        return "Global异常信息处理,请稍后再试";
    }
}

测试:
把8001服务关闭,分别访问 http://localhost/consumer/payment/hystrix/ok/1 、http://localhost/consumer/payment/hystrix/timeout/666 、http://localhost/consumer/payment/hystrix/timeout2/1
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
此时服务端provider已经down,但是我们做了服务降级处理,让客户端在服务不可用时也会获得提示信息而不会挂起耗死服务器。

之前的未配置 OrderFallbackServic 实现OrderHystrixService接口的服务降级时,关闭8001服务后,访问 结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
经过自己测试,客户端设置专属服务降级方法,若不关闭服务端,能正常调用服务。在将服务端服务关闭后会使用OrderFallbackServic 实现OrderHystrixService接口的服务降级(服务层面)的降级。
如客户端设置专属降级方法,且客户端在服务端没有关闭时调用服务会超时进行的降级是调用专属(控制层面)方法。关闭服务后再调用服务也是调用专属服务降级(控制层面)方法。
通用的服务降级方法(控制层面)无论服务端是否关闭都是优先调用的。

不会超时,正常情况下客户端对服务端服务调用:
在这里插入图片描述
由于服务关闭:服务层面降级
在这里插入图片描述

未关闭务端也会超时时,调用服务,降级调用专属方法(控制层面专属服务降级)
在这里插入图片描述
关了服务端后,调用服务降级方法也为专属方法(控制层面专属服务降级):
在这里插入图片描述

3.5 服务熔断

达到最大服务访问后,直接拒绝访问,然后调用服务降级的方法返回友好提示。
断路器:一句话就是家里的保险丝

熔断
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常后,恢复调用链路。

在springcloud框架里,熔断机制通过hystrix实现。Hystrix会监控微服务间调用的情况,当失败的调用达到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。

3.5.1 实操

1.修改cloud-provider-hystrix-payment8001
PaymentService

package com.atguigu.springcloud.service;

import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;


import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {
    /**
     * 正常访问,肯定没有问题的方法
     * @param id
     * @return
     */
    public String paymentInfo_OK(Integer id) {
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_OK,id: " +id+ "\t"+ "^_^";
    }

    @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = { //fallbackMethod:兜底方法
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "7000") //线程执行超时时间为7s,7s以内走正常逻辑,超过7s,调用指定方法
    })
    public String paymentInfo_TimeOut(Integer id) {
        int timeNumber = 5;
        //int age = 10/0;
        //暂停几秒钟
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池: "+Thread.currentThread().getName() + " paymentInfo_TimeOut,id: " +id+ "\t"+ "0_0 耗时: " +timeNumber+ "秒钟";
    }

    public String paymentInfo_TimeOutHandler(Integer id) {
        return "调用8001支付接口超时或者异常:\t" + "\t当前线程:"+Thread.currentThread().getName() + " paymentInfo_TimeOutHandler, id: " +id;
    }

    //===================服务熔断============================================
    //HystrixCommandProperties
    @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {//配置兜底方法
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),//是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),//请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),//时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")//失败率达到多少后跳闸  
    })//在一个时间窗口期内,10次请求的失败率达到60%就跳闸
    public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
        if (id < 0) {
            throw new RuntimeException("***********id 不能为负数");
        }
        String serialNumber = IdUtil.simpleUUID();//等价于UUID.randomUUID().toString()    commons中引入了hutool-all hutool.cn
        return Thread.currentThread().getName()+ "\t"+ "调用成功, 流水号: "+serialNumber;
    }

    public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
        return "id 不能为负数,请稍后再试,id:"+id;
    }
}

PaymentController

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

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

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

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

测试
自测cloud-provicer-hystrix-payment8001

正确:
在这里插入图片描述

错误:
在这里插入图片描述
当快速进行多次id小于0请求,然后再进行id大于0请求,发现刚开始小于0请求不满足条件,后面传入id大于0也不能正常进行服务,过一会才恢复正常。因为进行了服务熔断,直接拒绝服务,通通服务降级,所以大于0的id也会走兜底方法。
在这里插入图片描述

3.5.2 小总结

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

断路器在什么情况下开始起作用
涉及到断路器的三个重要参数:快照时间窗、请求总数阈值、错误百分比阈值,写在@HystrixProperties注解中,也可以全局配置在yml或properties中。

快照时间窗 circuitBreaker.sleepWindowInMilliseconds:断路器确定是否打开,需要统计的一些请求和错误的数据,而统计的时间范围就是快照时间窗,默认为最近10秒。

请求总数阈值 circuitBreaker.requestVolumeThreshold:在快照时间窗内,必须满足请求总数阈值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或者其他原因失败,断路器都不会打开。

错误百分比阈值 circuitBreaker.errorThresholdPercentage:当请求总数在快照时间窗内超过了请求总数阈值,比如发生了3次调用,如果在这30次调用中发生了15此超时异常,也就是超过50%的错误百分比,在默认设定阈值为50%的情况下,这是断路器就会打开。

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

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

3.6 服务限流

后面高级篇讲解alibaba的Sentinel时说明

3.7 服务监控hystrixDashboard

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

3.7.1 新建cloud-consumer-hystrix-dashboard9001
3.7.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

    </dependencies>

</project>
3.7.3 application.yml
server:
  port: 9001
3.7.4 HystrixDashboardMain9001+新注解@EnableHystrixDashboard
package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

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

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

需要依赖

<!--actuator监控信息完善-->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
3.7.6 启动cloud-consumer-hystrix-dashboard9001

在这里插入图片描述

3.7.7 断路器演示(服务监控hystrixDashboard)

1.修改cloud-provider-hystrix-payment8001
被监控的服务依赖中一定要加:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

新版本Hystrix需要在启动类MainAppHystrix8001中指定监控路径

package com.atguigu.springcloud;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@EnableEurekaClient //注册进euraka
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }

    /**
     * 此配置是为了服务监控而配置,于服务容错本身无关,springcloud升级后的坑
     * ServletRegistrationBean 因为springboot的默认路径不是"/hystrix.stream"
     * 只要在自己的项目里配置上下面的servlet就可以了
     * 
     * @return
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}

否则Unable to connect to Command Metric Stream.

测试
启动1个Eureka或者3个eureka集群均可

观察监控窗口:
1.9001监控8001
开启监控
在这里插入图片描述

2.测试地址
http://localhost:8001/payment/circuit/31
在这里插入图片描述

http://localhost:8001/payment/circuit/-31
在这里插入图片描述

上述测试均通过 ok

先访问正确地址,再访问错误地址,再正确地址,会发现图示断路器都是慢慢放开的。

断路器关闭:
在这里插入图片描述
断路器打开:
在这里插入图片描述
如何看:
1.七色
在这里插入图片描述
2.一圈
在这里插入图片描述
实心圆:共有两种含义。它通过颜色的变化代表了实例的监控程度,它的健康度从绿色<黄色<橙色<红色递减
该实心圆除了颜色变化外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例。

3.一线
在这里插入图片描述

4.整图说明
在这里插入图片描述

十一、GateWay新一代服务网关

1.概述简介

是什么:Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用Zuul网关。但在2.x版本中,zuul升级一直跳票,SpringCloud最后自己研发了一个网关替代zuul,就是Spring Cloud GateWay。

GateWay是在spring生态系统之上构建的API服务网关,基于spring5,Spring Boot2和Project Reactor技术。GateWay旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。GateWay 基于WebFlux框架实现,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

Spring Cloud GateWay的目标是提供统一的路由方式且基于Filter链的方式提供网关的基本功能,例如:安全、监控/指标、限流。
一句话:Spring Cloud GateWay使用WebFlux中的Reactor-netty响应式编程组件,底层使用了netty通讯框架。

能干嘛:反向代理、鉴权、流量控制、熔断、日志监控…

微服务架构中网关的位置:
在这里插入图片描述
Spring Cloud GateWay 具有如下特性:
基于Spring5、Project Reactor和SpringBoot2.0进行构建;
动态路由:能够匹配任何请求属性;
可以对路由指定Predicate(断言)和Filter(过滤器);
集成Hystrix的断路器功能;
集成SpringCloud服务发现功能;
易于编写Predicate(断言)和Filter(过滤器);
请求限流功能;
支持路径重写;

2.三大核心概念

Route(路由): 路由是构建网关的基本模块,它由ID,目标URI,一些列的断言和过滤器组成,如果断言为true则匹配该路由。
Predicate(断言): 参考的是java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的内容(例如请求头和请求参数),如果请求与断言相匹配则进行路由。
Filter(过滤): 指的是Spring框架中GateWayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

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

3.GateWay工作流程

在这里插入图片描述

客户端向Spring Cloud GateWay发出请求,然后在GateWay Handler Mapping中找到与请求相匹配的路由,将其发送到GateWay Web Handler。Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器使用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。

Filter在"pre"类型的过滤器可以做参数校验,权限校验,流量监控,日志输出,协议转换等。在"post"类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

4.入门配置
4.1 新建Module

新建cloud-gateway-gateway9527
网关作为服务也需要注册到注册中心

4.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-gateway-gateway9527</artifactId>

    <dependencies>

        <dependency>
            <groupId>org.example</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

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

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


        <!--<dependency> &lt;!&ndash;依赖冲突,不能要&ndash;&gt;
            <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>
        </dependency>

    </dependencies>

</project>
4.3 application.yml
server:
  port: 9527
spring:
  application:
    name: cloud-gateway
    
eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版

4.4主启动类
4.4.1 9527主启动类
package com.atguigu.springcloud;

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

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

4.4.2 cloud-provider-payment8001 主启动类修改
package com.atguigu.springcloud;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

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

    /**
     * 此配置是为了服务监控而配置,于服务容错本身无关,springcloud升级后的坑
     * ServletRegistrationBean 因为springboot的默认路径不是"/hystrix.stream"
     * 只要在自己的项目里配置上下面的servlet就可以了
     *
     * @return
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}

4.5 业务类

4.6 9527网关做路由映射

cloud-provider-payment8001看看controller的访问地址
配置:
/payment/get/{id}
/payment/lb

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

4.7 9527application.yml新增网关配置
server:
  port: 9527
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/**



eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版


4.8 测试

启动7001/7002
启动8001
启动8527网关

通过原8001端口访问:
在这里插入图片描述
在这里插入图片描述
通过9527端口也能访问:
在这里插入图片描述
在这里插入图片描述

4.9 配置说明

GateWay网关路由有两种配置方式
1.在配置文件yml中,见前面步骤

2.代码中注入RouteLocator的Bean
官网案例:

在这里插入图片描述
百度国内新闻网址(需要外网):http://news.baidu.com/guonei
在这里插入图片描述
自己写一个通过9527网关访问到外网的百度新闻地址:
cloud-gateway-gateway9527 实现:

package com.atguigu.springcloud.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GateWayConfig {
    @Bean
    public RouteLocator customRouteLocatoor(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        /**
         * 配置了一个id为 path_route_atguiggu的路由规则,
         * 当访问http://localhost:9527/guonei 将会转发到 http://news.baidu.com/guonei
         */
        routes.route("path_route_atguiggu",
                r -> r.path("/guonei")
                        .uri("http://news.baidu.com/guonei"));
        return routes.build();
    }

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

在这里插入图片描述

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

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

5.1 启动

eureka7001/7002 + 两个服务提供者8001/8002

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

        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service       #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**



eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版


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

5.3 测试

访问: http://localhost:9527/payment/lb
结果:
在这里插入图片描述
在这里插入图片描述
8001、8002交替提供服务。

6. Predicate的使用
6.1 Predicate是什么

启动我们的gateway9527
在这里插入图片描述
Route Predicate Factories是什么东东?
SpringCloud GateWay 将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。SpringCloud GateWay包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多个Route Predicate工厂可以进行组合。

Spring Cloud GateWay创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route。

6.2 常用的Route Predicate
6.2.1 After Route Predicate

在yml文件中添加:- After=2021-12-03T23:22:29.162+08:00[Asia/Shanghai],设置在这个时间之后才匹配

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


        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service       #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            - After=2021-12-03T23:22:29.162+08:00[Asia/Shanghai]


eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版

当前时间不满足条件时:
在这里插入图片描述

6.2.2 Before Route Predicate

在什么时间之前才匹配

  • Before=2021-12-03T23:22:29.162+08:00[Asia/Shanghai]
6.2.3 Between Route Predicate

在什么时间之间才匹配

  • Between=- After=2021-12-03T23:22:29.162+08:00[Asia/Shanghai],- After=2021-12-04T23:22:29.162+08:00[Asia/Shanghai]
6.2.4 Cookie Route Predicate

Cookie Route Predicate需要两个参数,一个是Cookie Name,一个是正则表达式。路由规则会通过获取对应的Cookie name和正则表达式进行匹配,如果匹配上就执行路由,如果没有匹配上则不执行。
添加:- Cookie=username,zzyy

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


        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service       #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            #- After=2021-12-03T23:22:29.162+08:00[Asia/Shanghai]
            #- Before=2021-12-04T23:22:29.162+08:00[Asia/Shanghai]
            #- Between=2021-12-03T23:22:29.162+08:00[Asia/Shanghai],2021-12-04T23:22:29.162+08:00[Asia/Shanghai]
            - Cookie=username,zzyy


eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版

1.不带cookies访问
在这里插入图片描述
2.带上cookies访问
在这里插入图片描述

6.2.5 Header Route Predicate

两个参数:一个是属性名称,一个是正则表达式,这个属性值和正则表达式匹配则执行路由
在9527 yml文件中,predicates中添加:- Header=X-Request-Id,\d+ #请求头要有X-Request-Id属性并且值为整数
测试:
在这里插入图片描述
请求头中没有该属性时:在这里插入图片描述

6.2.6 Host Route Predicate

Host Route Predicate接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔的模板,用.号作为分隔符
例:- Host=**.atguigu.com #什么样的URL路径过来
在这里插入图片描述

6.2.7 Method Route Predicate

例:- Method=GET #什么样的请求方法:

6.2.8 Path Route Predicate

请求的路径要匹配才路由,例:- Path=/payment/lb/**

6.2.9 Query Route Predicate

带有什么参数的,例:- Query=username,\d+ #要参数名username并且值为整数
在这里插入图片描述

6.2.10 小总结

9527 yml配置:

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


        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service       #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            #- After=2021-12-03T23:22:29.162+08:00[Asia/Shanghai]
            #- Before=2021-12-04T23:22:29.162+08:00[Asia/Shanghai]
            #- Between=2021-12-03T23:22:29.162+08:00[Asia/Shanghai],2021-12-04T23:22:29.162+08:00[Asia/Shanghai]
            #- Cookie=username,zzyy
            #- Header=X-Request-Id,\d+  #请求头要有X-Request-Id属性并且值为整数
            #- Host=**.atguigu.com  #什么样的URL路径过来
            #- Method=GET   #什么样的请求方法
            #- Query=username,\d+  #要参数名username并且值为整数


eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版

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

7. Filter的使用
7.1 Filter是什么

在这里插入图片描述
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud GateWay内置了多种路由过滤器,他们都由GateWayFilter的工厂类来产生。

7.2 SpringCloud GateWay的Filter

生命周期:pre(业务逻辑之前),post(业务逻辑之后)
种类:GateWayFilter(单一的),GlobalFilter(全局的)

7.2.1 常用的GateWayFilter

AddRequestParameter:
在9527的yml中添加filter

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


        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service       #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**
            #- After=2021-12-03T23:22:29.162+08:00[Asia/Shanghai]
            #- Before=2021-12-04T23:22:29.162+08:00[Asia/Shanghai]
            #- Between=2021-12-03T23:22:29.162+08:00[Asia/Shanghai],2021-12-04T23:22:29.162+08:00[Asia/Shanghai]
            #- Cookie=username,zzyy
            #- Header=X-Request-Id,\d+  #请求头要有X-Request-Id属性并且值为整数
            #- Host=**.atguigu.com  #什么样的URL路径过来
            #- Method=GET   #什么样的请求方法
            #- Query=username,\d+  #要参数名username并且值为整数
          filters:
            - AddRequestParameter=X-Request-Id,1024  #过滤器工厂会在匹配的请求头上加一对请求头,名称为X-Request-Id值为1024

eureka:
  client:
    register-with-eureka: true  #表示将自己注册进Eureka Server
    fetch-registry: true        #表示是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    service-url:
      #defaultZone: http://localhost:7001/eureka   #单机版
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka   #集群版

7.3 自定义全局过滤器(GlobalFilter)
7.3.1 俩个主要接口介绍

implements GlobalFilter,Ordered

7.3.2 能干嘛

全局日志记录、统一网关鉴权…

7.3.3 案例代码
package com.atguigu.springcloud.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Date;

@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("********************come in MyLogGateWayFilter " + new Date());
        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;//加载过滤器的顺序,数字越小,优先级越高
    }
}

7.3.4 测试

7.3.3 代码完成后,直接启动服务7001、7002、8001、8002、9527
测试正确地址:http://localhost:9527/payment/lb?uname=23
在这里插入图片描述
在这里插入图片描述
不满足过滤条件的地址:
在这里插入图片描述
在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐