Spring Cloud 尚硅谷阳哥学习笔记,每一行代码均有解释,适合快速上手,并配合尚硅谷视频食用

Spring Cloud

@ATenOne ❤️

一、正常 SpringBoot 环境的测试

1、版本的选择

  • SpringCloud Hoxton.SR1
  • SpringBoot 2.2.2.RELEASE
  • SpringCloud Alibaba 2.1.0.RELEASE
  • Java 8
  • Maven 3.5+
  • Mysql 8.0.25

父 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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ban</groupId>
    <artifactId>SpringCloud2021</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <modules>
        <module>cloud-provider-payment8001</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <lombok.version>1.18.10</lombok.version>
        <log4j.version>1.2.17</log4j.version>
        <mysql.version>8.0.25</mysql.version>
        <druid.version>1.1.16</druid.version>
    </properties>
    <!--子模块继承之后,提供作用:锁定版本+子module不用谢groupId和version-->
    <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 阿里巴巴-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--mysql-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
                <scope>runtime</scope>
            </dependency>
            <!-- druid-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.16</version>
            </dependency>

            <!--mybatis-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.1</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>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

payment8001 下的 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>SpringCloud2021</artifactId>
        <groupId>com.ban</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-payment8001</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.20</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.boot</groupId>-->
        <!--            <artifactId>spring-boot-devtools</artifactId>-->
        <!--            <scope>runtime</scope>-->
        <!--            <optional>true</optional>-->
        <!--        </dependency>-->

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.2.2.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--包含了sleuth+zipkin-->
    </dependencies>
</project>

Maven DependenceManagement 和 Dependence 的区别与好处

如果有多个子项目都引用同一样的依赖,则可以避免在每个使用的子项目里都声明一个版本号,这样想升级或切换到另一个版本时,只需在顶层父容器里更新,而不需要一个一个子项目的修改 ; 另外如果某个子项目需要另外的一个版本,只需声明version版本

Tips : 在一个依赖中无法下载的 jar 包,换另一个项目重新下载即可❤️

2、创建一个数据库表

数据库语句如下

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

3、工程的创建与配置

创建一个 Module cloud-provider-payment8001并实现 bean 层

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
    private long id;
    private String serial;
}
@AllArgsConstructor
@NoArgsConstructor
@Data
public class CommonResult<T> {
    private Integer code;
    private String message;
    //此处定义 T 代表传输 payment 返回 payment,传入 order 返回 order
    private T data;
    public CommonResult(Integer code,String message){
        this(code,message,null);
    }
}

上述 bean 层根据数据库字段进行编写,CommonResult 作为结果返回集,适用于前后端分离的情况下进行返回相应的 code 和 message

编写 Mapper(dao) 层

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

Resource 中进行 mapper.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.ban.springcloud.mapper.PaymentMapper">
<!--    public int create(Payment payment);-->
    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
        insert into payment(serial) values(#{serial});
    </insert>

    <resultMap id="BaseResultMap" type="com.ban.springcloud.bean.Payment">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <id column="serial" property="serial" jdbcType="VARCHAR"/>
    </resultMap>
    <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
        select * from payment where id = #{id};
    </select>
</mapper>

tips:如果在正常编写中,可以使用 Mybatis-Plus 进行逆向工程,并且通过继承 BaseMapper 免除 xml 文件的编写

service 层的编写

public interface PaymentService {
    public int create(Payment payment);
    public Payment getPaymentById(@Param("id") Long id);
}
@Service
public class PaymentServiceImpl implements PaymentService {
    @Autowired
    private PaymentMapper paymentMapper;
    @Override
    public int create(Payment payment) {
        return paymentMapper.create(payment);
    }
    @Override
    public Payment getPaymentById(Long id) {
        return paymentMapper.getPaymentById(id);
    }
}

tips:service 层需要编写接口以及接口的实现类 Impl ,无论是 mapper 还是 service 不要忘记 注解驱动,service 层注入的是 mapper 层

controller 层编写

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

    @PostMapping("/payment/create")
    public CommonResult create(@RequestBody Payment payment){
        int result = paymentService.create(payment);
        log.info("*******插入结果:"+result);
        if( result > 0 ){
            return new CommonResult(200,"插入数据成功:",result);
        }else{
            return new CommonResult(444,"插入数据失败:",null);
        }
    }
    @GetMapping("/payment/get/{id}")
    public CommonResult getPaymentById(@PathVariable("id") Long id){
        Payment paymentById = paymentService.getPaymentById(id);
        log.info("********插入结果:"+paymentById);
        if( paymentById != null ){
            return new CommonResult(200,"查询成功",paymentById);
        }else{
            return new CommonResult(444,"查询失败",null);
        }
    }
}

tips:controller 层自动注入的是 service 层

4、消费者订单模块的编写

此模块命名为 cloud-consumer-order80

首先,为了链接 80 和 8001 两个端口,我们需要导入 RestTemplate 模块进行两个服务之间的桥梁

RestTemplate 的配置

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

pom.xml 为

    <parent>
        <artifactId>SpringCloud2021</artifactId>
        <groupId>com.ban</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-order80</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-devtools</artifactId>
                    <scope>runtime</scope>
                    <optional>true</optional>
                </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.ban</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

接下来根据 8001 端口的 controller 层进行 80 端口的 controller 层编写

@RestController
@Slf4j
public class OrderController {
    public static final String PAYMENT_URL = "http://localhost:8001";
    @Autowired
    private RestTemplate restTemplate;

    /**
     * @author Ban
     * @date 2022/1/13 12:21
     * 利用 RestTemplate 引用 8001 端口进行业务逻辑的操作
     */
    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment){
        return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);
    }

  // PathVariable 注解,接受请求中的 id 值,赋给 Long 类型的 id
    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id){
        return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
    }
}

tips:此处使用 postForObject 方法进行不同端口的链接,此时我们可以利用消费者订单模块的链接进行数据库的读写,并且无需编写 mapper 、 service 层,因为我们使用的是 8001 端口的方法

此时,如果使用 post 是可以在浏览器端返回值的,而且也会返回成功信息,但是数据并没有插入到数据库中,所以我们需要在 create 方法的形参中加入 @RequestBody 注解(@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据))

如果参数放在请求体中,传入后台的话,那么后台要用@RequestBody才能接收到,否则就会在数据库中不能完成curd操作

5、工程的重构

将 bean 层的两个类放在单独的模块 cloud-api-commons 中,将原有的 bean 删除,并使用 maven 中的 clean 和 install 进行重构

通过在需要使用 bean 类的依赖中导入 install 后的依赖进行 bean 的引入

        <dependency>
            <groupId>com.ban</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

小技巧:可以在错误的地方利用自动排错功能进行依赖的引入

二、服务注册中心

1、Eureka(停更)

1.1、Enreka 架构(每一个端口都是一个集群)
  • Eureka Server:提供服务注册和发现,多个Eureka Server之间会同步数据,做到状态一致(最终一致性)
  • Service Provider:服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到
  • Service Consumer:服务消费方,从Eureka获取注册服务列表,从而能够消费服务

Eureka Server:注册中心服务端

注册中心服务端主要对外提供了三个功能:

  • 服务注册:服务提供者启动时,会通过 Eureka Client 向 Eureka Server 注册信息,Eureka Server 会存储该服务的信息,Eureka Server 内部有二层缓存机制来维护整个注册表

  • 提供注册表:服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表

  • 同步状态:Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态。

Eureka Client:注册中心客户端

Eureka Client:是一个 Java 客户端,用于简化与 Eureka Server 的交互。

Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。

Eureka 采用 CS(Client/Server,客户端/服务器) 架构,它包括以下两大组件:

  • Eureka Server:Eureka 服务注册中心,主要用于提供服务注册功能。当微服务启动时,会将自己的服务注册到 Eureka Server。Eureka Server 维护了一个可用服务列表,存储了所有注册到 Eureka Server 的可用服务的信息,这些可用服务可以在 Eureka Server 的管理界面中直观看到。
  • Eureka Client:Eureka 客户端,通常指的是微服务系统中各个微服务,主要用于和 Eureka Server 进行交互。在微服务应用启动后,Eureka Client 会向 Eureka Server 发送心跳(默认周期为 30 秒)。若 Eureka Server 在多个心跳周期内没有接收到某个 Eureka Client 的心跳,Eureka Server 将它从可用服务列表中移除(默认 90 秒)。
1.2、EurekaServer 服务端的安装

首先我们创建 cloud-eureka-server7001 一个模块作为服务端

application.yml 文件如下

server:
  port: 7001
eureka:
  instance:
    hostname: eureka7001.com
  client:
    # false 表示不向注册中心注册自己
    register-with-eureka: false
    # false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
    #下方直接为集群的配置
      defaultZone: http://eureka7002.com:7002/eureka/
      # 设置与 eureka server 交互的地址查询服务和注册服务都需要依赖这个地址
#      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

pom.xml 文件如下

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            <version>2.2.2.RELEASE</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>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

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

为 7001 模块写主启动类,引入新注解 @EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain7001.class,args);
    }
}
1.3、EurekaClient 客户端的安装

接下来我们为 8001支付端和 80 用户端进行 Eureka 客户端的配置,引入了 @EnableEurekaClient 注解

首先进行修改 application.yml 文件,并引入依赖

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

pom.xml 文件

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

而后在主启动类上声明注解

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

80 用户端与 8001 基本一致,不赘述了

1.4、Eureka 集群环境的搭建

首先进行修改 hosts 文件,该文件在 /etc/hosts 目录下

**创建一个新模块 7002,其中依赖文件与 7001 相同,只需要修改 application.yml 文件即可 **

server:
  port: 7002
eureka:
  instance:
    hostname: eureka7002.com
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

7001 端口的 application.yml 也需要进行修改,上面有演示,不赘述了

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

此时,我们需要修改80 和 8001 端口的 service-url 地址,即添加两个集群的地址,一方宕机可以立即启动另一个

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

互相注册,互相守望,效果如下

1.5、支付微服务集群配置 8002、8001

创建模块 8002 作为支付端的集群

server:
  port: 8002
spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/cloud2021?characterEncoding=utf8
    username: root
    password: 374761727
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.ban.springcloud.bean #所有Entity别名类所在包

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

其余逻辑与依赖复制 8001,并且更改名字与端口号为 8002

修改 80 端口的绝对 PAYMENT_URL 因为此处为绝对引用,为了确保 8001 和 8002 端口均可进行操作,我们需要将其改为 Eureka 中的 Application-name(网页中)

public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

此时如果重启,使用 getPaymentId 方法就会报 500 错误,是因为系统此时无法分辨该选择哪个服务器来启动,这时候我们需要使用 RestTemplate 负载均衡

@LoadBalance 注解(RestTemplate)的引用解决服务名无法调用,只能使用 ip + 端口访问的问题,赋予 RestTemplate 负载均衡的能力

@Configuration
public class ApplicationContextConfig {
    @Bean
    // 配置负载均衡
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

此刻重启微服务,我们会发现 80 端口的 /consumer/payment/get/1 每一次都会返回 8001 或者 8002 (通常为一次一个)

1.6、actuator 微服务信息完善
eureka:
  client:
    # 是否将自己注册进 EnurekaServer 默认为 true
    register-with-eureka: true
    # 是否从 EurekaServer 抓去已有的注册信息,默认为 true ,单节点无所谓,集群必须设置为 true 才能配合
    # ribbon 使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  instance:
    instance-id: payment8002
    prefer-ip-address: true # 访问路径可以显示 ip 地址
1.7、Discovery 服务发现

为了让服务能够更好的被其他模块所发现,我们需要引入 Discovery 模块进行服务端各种信息的获取

我们用 8002 端口的 controller 层来进行配置

import org.springframework.cloud.client.discovery.DiscoveryClient; 
//此时引用的是上方的 cloud 包下的 DiscoveryClient
		@Autowired
    private DiscoveryClient discoveryClient;
    
    @GetMapping("/payment/discovery")
    public Object discovery(){
        List<String> services = discoveryClient.getServices();
        for( String element : services ){
            log.info("*******element"+element);
        }
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        for( ServiceInstance instance : instances ){
            log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
        }
        return this.discoveryClient;
    }

然后我们需要在主启动类上加 @EnableDiscoveryClient注解,此后会经常使用此注解

运行结果如图所示

{"discoveryClients":[{"order":0,"services":["cloud-payment-service","cloud-order-service"]},{"order":0,"services":[]}],"services":["cloud-payment-service","cloud-order-service"],"order":0}
1.8、Eureka 自我保护理论知识

当我们在本地调试基于 Eureka 的程序时,Eureka 服务注册中心很有可能会出现如下图所示的红色警告

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m3JBSvaW-1642578424347)(/Users/ban/Library/Application Support/typora-user-images/image-20220114170426821.png)]

实际上,这个警告是触发了 Eureka 的自我保护机制而出现的。默认情况下,如果 Eureka Server 在一段时间内(默认为 90 秒)没有接收到某个服务提供者(Eureka Client)的心跳,就会将这个服务提供者提供的服务从服务注册表中移除。 这样服务消费者就再也无法从服务注册中心中获取到这个服务了,更无法调用该服务

所谓 “Eureka 的自我保护机制”,其中心思想就是“好死不如赖活着”。如果 Eureka Server 在一段时间内没有接收到 Eureka Client 的心跳,那么 Eureka Server 就会开启自我保护模式,将所有的 Eureka Client 的注册信息保护起来,而不是直接从服务注册表中移除

综上,Eureka 的自我保护机制是一种应对网络异常的安全保护措施。它的架构哲学是:宁可同时保留所有微服务(健康的服务和不健康的服务都会保留)也不盲目移除任何健康的服务。通过 Eureka 的自我保护机制,可以让 Eureka Server 集群更加的健壮、稳定。

弊端

Eureka 的自我保护机制也存在弊端。如果在 Eureka 自我保护机制触发期间,服务提供者提供的服务出现问题,那么服务消费者就很容易获取到已经不存在的服务进而出现调用失败的情况

2、Zookeeper(一通百通)

3、Consul(一通百通)

三、服务调用

1、Ribbon 负载均衡服务调用(维护)

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端-负载均衡工具

基于Netflix Ribbon实现

通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用

简单来说,只要在配置文件中列出 Load Balancer(LB)后面所有的机器,Ribbon 会自动的帮助你基于某种规则(简单轮询,随机连接等)去链接这些机器,我们很容易的使用 Ribbon 实现自定义的负载均衡算法

一句话:负载均衡 + RestTemplate 调用

1.1、Nginx服务端负载均衡区别 与 Ribbon 本地负载均衡客户端的区别

Nginx 是服务器负载均衡,客户端所有请求都会交给 Nginx,然后由 Nginx 实现转发请求,即负载均衡是由服务端实现的

1.2、依赖的引入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xXSWhd0H-1642578424347)(/Users/ban/Library/Application Support/typora-user-images/image-20220115092205360.png)]

由于 client 包下已经有了 ribbon 的引入,所以不需要进行单独的引入,据说 3 版本的已经废弃了 ribbon 可能需要单独引入

代码如下( 80 端口的 controller 层 )

    @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);
        if(entity.getStatusCode().is2xxSuccessful()){
            return entity.getBody();
        }else{
            return new CommonResult<>(444,"操作失败!");
        }
    }
1.3、如何从轮循换到其他算法的呢

由于 ribbon 自定义配置类不能放在 @SpringbootApplication 注解的扫描范围下(因为这个注解中有@ComponentScan 注解),否则自定义的配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的

所以我们需要在 SpringBoot 主启动扫描不到的地方重新新建包和配置

新建包 com.ban.myrule 创建方法 MySelfRule

public class MySelfRule {
		//注入到 Spring 容器中
    @Bean
    public IRule myRule(){
        // 定义为随机
        return new RandomRule();
    }
}

并且需要在主启动类上进行声明

@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class MainApplication80 {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication80.class,args);
    }
}
1.4、手写轮询算法

留待有实力在进行耕耘

2、OpenFeign 服务接口调用(正在使用)

2.1、什么是 Feign
  • Feign 是声明式 Web Service 客户端它让微服务之间的调用变得更简单,类似 controller 调用service。SpringCloud 集成了 Ribbon 和 Eureka,可以使用Feigin提供负载均衡的http客户端
  • 只需要创建一个接口,然后添加注解即可使用Feign
2.2、Feign 与 OpenFeign 的区别

Feign是Spring Cloud组件中一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:

使用Feign的注解定义接口,调用接口,就可以调用服务注册中心的服务。

OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等

OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类

2.3、使用

Ribbon + RestTemplate = OpenFeign 作用于服务消费者端(三角图)

首先创建 cloud-consumer-feign-order80 . feign(service) . PaymentService

/**
 * @author Ban
 * @version 1.0
 * Create by 2022/1/15 15:26
 * 此处的 service 层编写与 cloud-consumer-order80 的 controller 层相同
 */
@Component
@FeignClient("CLOUD-PAYMENT-SERVICE")
public interface PaymentService {
    @GetMapping("/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}

创建 controller 层

@RestController
@Slf4j
public class OrderFeignController {
    @Autowired
    PaymentService paymentService;

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

日志增强功能的实现

首先进行配置类的编写

@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}
server:
  port: 80
eureka:
  client:
    service-url:
      defaultZone: http://eureka:7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
    # 是否将自己注册进 EnurekaServer 默认为 true
    register-with-eureka: false
logging:
  level:
    # feign 日志以什么级别监控哪个窗口
    com.ban.springcloud.feign.PaymentService: debug

主启动类:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RI776RYo-1642578424348)(/Users/ban/Library/Application Support/typora-user-images/image-20220115162357565.png)]

四、服务降级

1、Hystrix 介绍

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

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

2、Hystrix 为了解决什么问题

Hystrix被设计的目标是:

  • 对通过第三方客户端库访问的依赖项(通常是通过网络)的延迟和故障进行保护和控制。
  • 在复杂的分布式系统中阻止级联故障。
  • 快速失败,快速恢复。
  • 回退,尽可能优雅地降级。
  • 启用近实时监控、警报和操作控制。

总的来说就是:降级、熔断、接近实时的监控

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

哪些情况会触发降级?

  • 程序运行异常
  • 超时
  • 服务熔断出发服务降级
  • 线程池/信号量打满也会导致服务降级

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

**服务限流:**秒杀高并发等操作,严谨一窝蜂的过来拥挤,大家排队,一秒钟 N 个,有序进行

当一切正常时,请求看起来是这样的

img

当一个系统有延迟时,他可能阻塞整个请求

img

在高流量的情况下,一个后端依赖项的延迟可能导致所有服务器上的所有资源在数秒内饱和(PS:意味着后续再有请求将无法立即提供服务)

img

3、Hystrix 是如何实现他的目标的

  • 用一个HystrixCommand 或者 HystrixObservableCommand (这是命令模式的一个例子)包装所有的对外部系统(或者依赖)的调用,典型地它们在一个单独的线程中执行
  • 调用超时时间比你自己定义的阈值要长。有一个默认值,对于大多数的依赖项你是可以自定义超时时间的。
  • 为每个依赖项维护一个小的线程池(或信号量);如果线程池满了,那么该依赖性将会立即拒绝请求,而不是排队。
  • 调用的结果有这么几种:成功、失败(客户端抛出异常)、超时、拒绝。
  • 在一段时间内,如果服务的错误百分比超过了一个阈值,就会触发一个断路器来停止对特定服务的所有请求,无论是手动的还是自动的。
  • 当请求失败、被拒绝、超时或短路时,执行回退逻辑。
  • 近实时监控指标和配置变化。

当你使用Hystrix来包装每个依赖项时,上图中所示的架构会发生变化,如下图所示:

每个依赖项相互隔离,当延迟发生时,它会被限制在资源中,并包含回退逻辑,该逻辑决定在依赖项中发生任何类型的故障时应作出何种响应:

img

4、Hystrix 使用

4.1、Hystrix 的构建

首先为避免启动过多集群,将 7001 还原回单机版

创建 8001 端口的 hystrix 模块 命名为 cloud-provider-hystrix-payment8001

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.ban</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.20</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.boot</groupId>-->
        <!--            <artifactId>spring-boot-devtools</artifactId>-->
        <!--            <scope>runtime</scope>-->
        <!--            <optional>true</optional>-->
        <!--        </dependency>-->

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.2.2.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <!--        devtools-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
server:
  port: 8001
spring:
  application:
    name: cloud-provider-hystrix-payment
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/cloud2021?characterEncoding=utf8
    username: root
    password: 374761727
    driver-class-name: com.mysql.cj.jdbc.Driver
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka

service 层的编写

@Service
public class PaymentService {

    public String paymentInfo_OK(Integer id){
        return "线程池:"+Thread.currentThread().getName()+"paymentInfo-OK,id:"+id+"哈哈哈";
    }
    public String paymentInfo_TimeOut(Integer id){
        int timeNum = 3;
        try {
            TimeUnit.SECONDS.sleep(timeNum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:"+Thread.currentThread().getName()+" paymentINfo_TimeOut,id:"+id+"哈哈哈,耗时(秒)"+timeNum;
    }
}

controller 层的编写

@RestController
@Slf4j
public class PyamentController {
    @Autowired
    private PaymentService paymentService;

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

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

    @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;
    }
}
@SpringBootApplication
public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrixMain8001.class,args);
    }
}

此时启动 8001 和服务端 7001,ok 中可以瞬时显示,timeout 中需要 3 秒显示

当使用 Jmeter 插件进行高并发测试时,ok 中也不能瞬时显示了,明显变慢

接下来我们构建 80 消费端进行进一步测试

cloud-consumer-feign-hystrix-order80:

server:
  port: 80
eureka:
  client:
    service-url:
      defaultZone: http://eureka:7001.com:7001/eureka/
    # 是否将自己注册进 EnurekaServer 默认为 true
    register-with-eureka: false
    fetch-registry: false
logging:
  level:
    # feign 日志以什么级别监控哪个窗口
    com.ban.springcloud.feign.PaymentService: debug
feign:
  hystrix:
    enabled: true
#设置feign客户端超时时间
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为5000ms
ribbon:
  ReadTimeout: 6000 #指的是建立连接所用得时间,适用于网络状况正常的情况下,两端连接所用的时间(6s)
  ConnectTimeout: 6000 #指的是建立连接后从服务器读取到可用资源所用的时间(6s)

依赖与普通的 80 端口相同,多出 openFeign 的依赖

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

service 端

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

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

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

controller 端

@RestController
@Slf4j
public class OrderHystrixController {

    @Autowired
    private PaymentHystrixService paymentHystrixService;

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

主启动类

@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderHystrixMain80.class,args);
    }
}
4.2、高并发测试

接下来进行高并发测试,对端口进行线程的增大模拟高并发,此时发现无论是服务端还是消费端的访问明显变慢

故障现象和导致原因

  • 8001 同一层次的其他接口服务被困死,因为 tomcat 线程池里面的工作线程过大
  • 80 此时调用 8001,客户端访问响应缓慢,转圈圈

解决方法

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

大总结:

在 8001 中进行 service 层配置

@Service
public class PaymentService {

    public String paymentInfo_OK(Integer id){
        return "线程池:"+Thread.currentThread().getName()+"paymentInfo-OK,id:"+id+"哈哈哈";
    }
    /**
     * @author Ban
     * @date 2022/1/16 11:23
     * 如果出现错误,降级使用 fallbackMethod 中的方法,commandProperties 表示超时最大时间(3秒就降级)
     */

    @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "5000")
    })
    public String paymentInfo_TimeOut(Integer id){

        try {
            TimeUnit.MILLISECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:"+Thread.currentThread().getName()+" paymentINfo_TimeOut,id:"+id+"哈哈哈";
    }

    public String paymentInfo_TimeOutHandler(Integer id){
        return "线程池:"+Thread.currentThread().getName()+" paymentINfo_TimeOut 8001系统繁忙或出现错误,id:"+id+"mayayayayayayay!";
    }
}

controller 与上述相同

在 80 中进行配置

service 层,引入了 FeignClient 中的fallback,作为“兜底“的类的返回

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

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

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_timeout(@PathVariable("id") Integer id);
}
@Component
public class PaymentFallbackService implements PaymentHystrixService{
    @Override
    public String paymentInfo_OK(Integer id) {
        return "------fallback -----   太惨了,降级了";
    }

    @Override
    public String paymentInfo_timeout(Integer id) {
        return "------fallback -----   太惨了,降级了";
    }
}

controller 层

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

    @Autowired
    private PaymentHystrixService paymentHystrixService;

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

    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
        log.info("*****result");
        return "我是消费者80,对方支付系统繁忙请等一会!";
    }

    public String payment_Global_FallbackMethod(){
        return "Global异常处理信息,请稍后再试!";
    }
}

主启动类

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

运行结果如下

5、服务熔断

  • Closed:熔断器关闭状态,调用失败次数积累,到了阈值(或一定比例)则启动熔断机制;
  • Open:熔断器打开状态,此时对下游的调用都内部直接返回错误,不走网络,但设计了一个时钟选项,默认的时钟达到了一定时间(这个时间一般设置成平均故障处理时间,也就是MTTR),到了这个时间,进入半熔断状态;
  • Half-Open:半熔断状态,允许定量的服务请求,如果调用都成功(或一定比例)则认为恢复了,关闭熔断器,否则认为还没好,又回到熔断器打开状态;

熔断机制概述

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

当检测到该节点微服务调用响应正常后,恢复调用链路

Spring Cloud 中,熔断机制通过 Hystrix 实现,Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是 5 秒内 20 次调用失败,就会启动熔断机制。熔断机制的注解是 @HystrixCommand

流程

流程说明:
1:每次调用创建一个新的HystrixCommand,把依赖调用封装在run()方法中。

2:执行execute()/queue做同步或异步调用。

3:判断熔断器(circuit-breaker)是否打开,如果打开跳到步骤8,进行降级策略,如果关闭进入步骤。

4:判断线程池/队列/信号量是否跑满,如果跑满进入降级步骤8,否则继续后续步骤。

5:调用HystrixCommand的run方法。运行依赖逻辑

5a:依赖逻辑调用超时,进入步骤8。

6:判断逻辑是否调用成功

6a:返回成功调用结果
6b:调用出错,进入步骤8。
7:计算熔断器状态,所有的运行状态(成功, 失败, 拒绝,超时)上报给熔断器,用于统计从而判断熔断器状态。

8:getFallback()降级逻辑。

以下四种情况将触发getFallback调用:

  • run()方法抛出非HystrixBadRequestException异常
  • run()方法调用超时
  • 熔断器开启拦截调用
  • 线程池/队列/信号量是否跑满

8a:没有实现getFallback的Command将直接抛出异常

8b:fallback降级逻辑调用成功直接返回

8c:降级逻辑调用失败抛出异常

9:返回执行成功结果

5.1、实现

service 层的实现

    //=========服务熔断
    @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),/* 是否开启断路器*/
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸)
    })
    public String paymentCircuitBreaker(@PathVariable("id") Integer id){
        if( id < 0 ){
            throw new RuntimeException("***id not < 0");
        }
        String serialNum = IdUtil.simpleUUID();
        return Thread.currentThread().getName()+"调用成功,流水号:"+serialNum;
    }
    public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id){
        return "id not mid zero,id:"+id;
    }

controller 层

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NJzOAKaR-1642578424351)(/Users/ban/Library/Application Support/typora-user-images/image-20220117103912704.png)]

tips:如果失败次数达到一定层次,输入正确的也不会立即返回正确的提示,而是会等待一段时间(即:失败率达到一定层次,限制你的使用)

6、服务限流

由于豪猪哥已经停止更新,所以把限流放在 AlibabaSentinel 说明

7、Hystrix 图形化 Dashboard (仪表盘)

创建模块 cloud-consumer-hystrix-dashboard9001

首先我们需要在服务提供者 8001 的主启动类上添加配置

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

    /**
     * 注意:新版本Hystrix需要在主启动类中指定监控路径
     * 此配置是为了服务监控而配置,与服务容错本身无关,spring cloud升级后的坑
     * ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
     * 只要在自己的项目里配置上下面的servlet就可以了
     *
     * @return ServletRegistrationBean
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);

        // 一启动就加载
        registrationBean.setLoadOnStartup(1);
        // 添加url
        registrationBean.addUrlMappings("/hystrix.stream");
        // 设置名称
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}

随后配置 9001 端口,进行 Dashboard 端口的启动 @EnableHystrixDashboard

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixboardMain9001 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixboardMain9001.class,args);
    }
}
server:
  port: 9001

非常有趣的“六色一圆”

五、服务网关

SpringCloud Gateway 是 Spring Cloud 的个全新项目,基于 Spring5.0+ Spring Boot2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式

SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代zuul

在 Spring Cloud2.0以上版本中,没有对新版本的zuul2.0以上最新高性能版本进行集成,仍然还是使用的 zuul1.× 非 Reactor 模式的老版本。

Spring Cloud Gateway使用的 Webflux中的 reactor-netty响应式编程组件,底层使用了 Netty通讯框架,是基于 异步非阻塞模型 上进行开发的

Spring Cloud Gateway的目标提供统-的路由方式且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流

1、GateWay主要特点(面试题)

作用:反向代理,鉴权,流量控制,熔断,日志监控❤️

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

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

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

**Filter(过滤)**❤️
指的是 Spring框架中 Gateway Filter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制

predicate就是我们的匹配条件,而 Filter ,就可以理解为个无所不能的拦截器有了这两个元素,再加上目标 uri 就可以实现一个具体的路由了

下图提供了 Spring Cloud Gateway 如何工作的高级概述

image-20220117155340668

上图的具体文字描述为

客户端向 Spring Cloud Gateway 发出请求,然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler

Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回

过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑

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

在 “post” 类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用

具体实现为:

依赖引入

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.ban</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </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>

配置文件如下

server:
  port: 9527

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

#        - id: payment_routh2 #payment_route    # 路由的ID,没有固定规则但要求唯一,建议配合服务名
#          #uri: http://localhost:8001          # 匹配后提供服务的路由地址
eureka:
  instance:
    hostname: cloud-gateway-service
  client: # 服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

主启动类

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

2、Route 动态路由

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

所谓的动态路由就是将**写死的地址转化为注册在 Eureka 中新的 Application **

server:
  port: 9527

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

#        - id: payment_routh2 #payment_route    # 路由的ID,没有固定规则但要求唯一,建议配合服务名
#          #uri: http://localhost:8001          # 匹配后提供服务的路由地址
#          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
eureka:
  instance:
    hostname: cloud-gateway-service
  client: # 服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

3、Predicate 断言

什么是断言,简而言之就是在什么情况下,进行路由转发(可以使用这个地址)

https://blog.csdn.net/weixin_43852058/article/details/110704665

img

配置文件如下

server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh # payment_route    # 路由的ID,没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001          # 匹配后提供服务的路由地址
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址,lb 代表从注册中心获取服务
          predicates:
            - Path=/payment/get/**         # 断言,路径相匹配的进行路由
#        - id: payment_routh2 #payment_route    # 路由的ID,没有固定规则但要求唯一,建议配合服务名
#          #uri: http://localhost:8001          # 匹配后提供服务的路由地址
#          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
#          predicates:
#            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
#            - After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
            # - Before=2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
            # - Between=2020-03-08T10:59:34.102+08:00[Asia/Shanghai] ,  2020-03-08T10:59:34.102+08:00[Asia/Shanghai]
            # curl http://localhost:9527/payment/lb --cookie "username=zzyy"
            # - Cookie=username,zzyy   #Cookie=cookieName,正则表达式
            # 请求头要有X-Request-Id属性并且值为整数的正则表达式 curl http://localhost:9527/payment/lb --cookie "username=zzyy" -H "X-Request-Id:11"
#            - Header=X-Request-Id, \d+
#            - Host=**.atguigu.com  # curl http://localhost:9527/payment/lb -H "Host:afae.atguigu.com"

eureka:
  instance:
    hostname: cloud-gateway-service
  client: # 服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

见 predicates 处

4、Filter 过滤器

概念:路由过滤器可用于修改进入的 HTTP 请求和返回的 HTTP 响应,路由过滤器只能指定路由进行使用

Spring Cloud Gateway 内置了多种路由过滤器,他们都由 GatewayFilter 的工厂类产生

单一过滤器

见官网 31 个,复制即可 very easy

https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/

全局过滤器

我们实现一个自定义过滤器(常用)

MyLogGateWayFilter 实现 GlobalFilter,Ordered 两个接口

@Component
@Slf4j
public class MylogGateWayFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("********come in MylogGateWayFilter" + new Date());
        String uname = exchange.getRequest().getQueryParams().getFirst("uname");
        if (uname == null) {
            log.info("********用户名为 null ,非法用户!");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }
    @Override
    public int getOrder() {
        return 0;
    }
}

而后通过访问地址进行校验 http://localhost:9527/payment/lb?uname=1

如果不带 uanme 则报 406 错误

主要为了实现

  • 全局日志记录
  • 统一网管鉴权

六、服务配置

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

Spring Cloud 提供了 ConfigServer 来解决这个问题,我们每一个微服务自己带着一个 application.yml ,上百个配置就挺 emo 😈 的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iAIyoxQt-1642578424354)(/Users/ban/Library/Application Support/typora-user-images/image-20220118111143021.png)]

Config分布式配置中心简介

SpringCloud Config分为服务端客户端两部分。

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

Config分布式配置中心能做什么

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

1、Config 服务端搭建

首先创建 git 仓库,见下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RpQk4aWX-1642578424354)(/Users/ban/Library/Application Support/typora-user-images/image-20220118153008130.png)]

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

    <!--        Config服务端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

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

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

application.yml配置文件

server:
  port: 3344

spring:
  application:
    name: cloud-config-center #注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/taobutao995/springcloud-config.git #GitHub上面的git仓库名字
          search-paths: #搜索目录
            - springcloud-config
          username: 13700165002
          password: a374761727
      label: master #读取分支
      #启动成功后访问的路径 http://ip:3344/{label}/{application}-{profile}.yml 能访问的配置文件 就表示成功了

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

主启动类

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

通过访问 http://config-3344.com:3344/master/config-dev.yml 进行测试

2、Config 客户端搭建

pom.xml 依赖文件的引入

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

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

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

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

创建 bootstrap.yml 文件,因为他的优先级比 application.yml 高

server:
  port: 3355

spring:
  application:
    name: config-client
  cloud:
    config:
      label: master  #分支名称
      name: config  #配置文件名称
      profile: dev  #读取后缀名称   上述三个综合http://localhost:3344/master/config-dev.yml
      uri: http://localhost:3344  #配置中心的地址
#服务注册到eureka地址
eureka:
  client:
    service-url:
      #设置与eureka server交互的地址查询服务和注册服务都需要依赖这个地址
      defaultZone: http://localhost:7001/eureka #单机版

创建 controller 层方法

@RestController
public class ConfigClientController {

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

    @GetMapping("/configInfo")
    public String getConfigInfo(){
        return configInfo;
    }
}
@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3355 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain3355.class,args);
    }
}

通过访问:http://localhost:3355/configInfo 网址进行测试

此时如果修改 gitee 上面的配置文件,通过 3344 可以访问到 gitee 上修改的内容,但是 3355 通过 3344 确无法访问,此时我们需要引入动态刷新

刷新后为 32 ,以前为 31

3、动态刷新(手动版)

首先需要在 3355 的配置文件中暴露监控端点(已经引入了 actuator 依赖)

# 暴露监控端点 否则 curl -X POST "http://localhost:3355/actuator/refresh" 不可使用
management:
  endpoints:
    web:
      exposure:
        include: "*"

而后需要在 controller 层上添加 @RefreshScope注解

配置完成后通过发送 POST 请求进行更新3355的数据(POSTMAN)

http://localhost:3355/actuator/refresh

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YQrLYxPi-1642578424356)(/Users/ban/Library/Application Support/typora-user-images/image-20220118160003436.png)]

七、消息总线(服务总线)

1、简介

Spring Cloud Bus 使用轻量级的消息代理来连接微服务架构中的各个服务,可以将其用于广播状态更改(例如配置中心配置更改)或其他管理指令

通常会使用消息代理来构建一个主题,然后把微服务架构中的所有服务都连接到这个主题上去,当我们向该主题发送消息时,所有订阅该主题的服务都会收到消息并进行消费。

使用 Spring Cloud Bus 可以方便地构建起这套机制,所以 Spring Cloud Bus 又被称为消息总线。

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

目前 Spring Cloud Bus 支持两种消息代理:RabbitMQ 和 Kafka。

通过刷新客户端服务

通过刷新服务端服务

图一(传染)不合适的原因如下:

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

2、环境搭建(RabbitMQ)

/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

首先在 mac 终端安装 brew 客户端

然后进行 rabbitMQ 的安装

brew install rabbltmq

进入网址测试 http://localhost:15672 ,账号guest,密码guest

这样我们的环境搭载完毕

3、实现动态刷新

创建一个新模块 3366 ,配置与 3355 完全相同(端口3366)

以上两种服务(刷新客户端、刷新服务端)我们选择通过利用消息总线触发一个服务端 ConfigServer 的 /bus/refresh 端点,进而刷新所有客户端的配置

为了实现动态的全局刷新,我们将 3344 、3355、3366 均加入以下依赖

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

在所有的 application.yml(bootstrap.yml)文件中加入如下配置 rabbitMQ

rabbitmq:
  host: localhost
  port: 15672
  username: guest
  password: guest

然而服务端的配置略有不同

server:
  port: 3344

spring:
  application:
    name: cloud-config-center #注册进Eureka服务器的微服务名
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/taobutao995/springcloud-config.git #GitHub上面的git仓库名字
          search-paths: #搜索目录
            - springcloud-config
          username: 13700165002
          password: a374761727
      label: master #读取分支
      #启动成功后访问的路径 http://ip:3344/{label}/{application}-{profile}.yml 能访问的配置文件 就表示成功了
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
#SpringCloud Bus动态刷新定点通知 公式:http://localhost:配置中心的端口号/actuator/bus-refresh/{destination}
#例如 只通知3355,curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"
##rabbitmq相关配置,暴露bus刷新配置的端点
rabbitmq:
  host: localhost
  port: 15672
  username: guest
  password: guest
##rabbitmq相关配置,暴露bus刷新配置的端点 SpringCloud Bus动态刷新全局广播
management:
  endpoints: #暴露bus刷新配置的端点
    web:
      exposure:
        include: 'bus-refresh'

此处暴露的端点为 bus-refresh,作为服务端与客户端的区别

此时我们的环境与配置搭建完毕,启动 7001、3344、3355、3366 进行测试

4、测试

将所有服务启动后,我们修改 gitee 中的文件,然后观察 3344 与 3355、3366 的区别

证明只有 3344 可以同步 gitee 的变化,此时我们需要进行 POST 进行全局广播

http://localhost:3344/actuator/bus-refresh

而后,3355与3366 均可同步 3344 的信息

5、实现单一的端口的更新,而非全局

此处的 config-client 为 spring-application-name ,3344 为 Config 服务端的端口号,bus-refresh 为 3344 端口中 yaml 配置文件中刷新配置的端点名称

http://localhost:3344/actuator/bus-refresh/config-client:3355

八、消息驱动

1、简介

简而言之:Spring Cloud Stream 就是屏蔽底层消息中间件(ActiveMQ、RabbitMQ、RocketMQ、Kafka)的差异,降低切换成本,统一消息的编程模型

Spring Cloud Stream 的官方定义为 是一个构建消息驱动微服务的框架

应用程序通过 inputs 或 outputs 来与 Spring Cloud Stream 中 binder 对象交互,通过我们配置 binding(绑定),而 Spring Cloud Stream 的 binder 对象负责与消息中间件交互,所以我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便实用消息驱动的方式

通过使用 Spring Integration 来链接消息代理中间件以实现消息事件驱动,Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置,引用了 发布-订阅、消费组、分区 的三个概念

**(目前仅支持 RabbitMQ、Kafka)**❤️

Spring Cloud Stream由一个中立的中间件内核组成。Spring Cloud Stream会注入输入和输出的channels,应用程序通过这些channels与外界通信,而channels则是通过一个明确的中间件Binder与外部brokers连接

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RLZqxLDH-1642578424358)(/Users/ban/Library/Application Support/typora-user-images/image-20220119092254802.png)]

上图名词解释:

  • Middleware:中间件,目前仅支持 RabbitMQ、Kafka
  • BInder:Binder 是应用与消息中间件的封装,通过 Binder 可以方便的链接中间件,可以动态改变消息类型(RabbitMQ 的 exchange,Kafka 的 topic )都可以通过配置文件实现
  • @Input:注解标识输入通道,通过该输入通道接收到的消息进入应用程序
  • @Output:注解标识输出通道,发布的消息将通过该通道离开应用程序
  • @StreamListener:监听队列,用于消费者的队列的消息接收
  • @EnableBinding:值信道 channel 和 exchange 绑定在一起

2、实现

2.1、消息驱动之生产者 8801
 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>


        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

配置文件如下(注意rabbitmq的锁进,以及bindings的锁进

server:
  port: 8801

spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: # 在此处配置要绑定的rabbitmq的服务信息;
        defaultRabbit: # 表示定义的名称,用于于binding整合
          type: rabbit # 消息组件类型
          environment: # 设置rabbitmq的相关的环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings: # 服务的整合处理
        output: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
          binder: defaultRabbit  # 设置要绑定的消息服务的具体设置
eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: send-8801.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

service 层实现类

/**
此处的 Source 是 import org.springframework.cloud.stream.messaging.Source;
此处的 MessageBuilder 是 import org.springframework.integration.support.MessageBuilder;
*/
@EnableBinding(Source.class)
public class MessageProviderImpl implements IMessageProvider {
    @Autowired
    private MessageChannel output;

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

controller 层

@RestController
public class SenMessageController {

    @Autowired
    private IMessageProvider messageProvider;

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

主启动层依旧为 EnableEureka 注解

效果如下图

2.2、消息驱动之消费者 8802、8803 (@EnableBinding)

依赖与 8801 完全相同

配置如下:

server:
  port: 8802

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


eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: receive-8802.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

controller 层

//sink 是 import org.springframework.cloud.stream.messaging.Sink;
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController {
    @Value("${server.port}")
    public String serverPort;

    @StreamListener(Sink.INPUT)
    public void input(Message<String> message){
        System.out.println("消费者 1 号,---》》》收到的消息:"+message.getPayload()+"/t"+serverPort);
    }
}

3、分组消费

创建与 8802 完全相同的 8803

此时会出现两个问题

  • 有重复消费问题(8801 发送一次,8802、8803都可以接受)
  • 消息持久化问题

为什么会有重复消费问题?

因为默认分组 group 是不同的,组流水号不同,被认为不同组,所以重复消费,同一个组会产生竞争,只有一个可以消费

故可以采用分组解决重复消费问题

只需要 8802、8803 加入 group: ban1

此处再次使用 8801 发送两条信息,就会发现 8802、8803 各一条

4、消息的持久化

当你没有分组时,8802(没有 group) 宕机,此时8801 一直在发送信息,但是 8802 重启后接受不到宕机这段时间 8801 发送的消息

但是 group 的 8803 却可以在重启后,接收到 8801 的消息

原理:由于数据发送者将数据发送到队列中,由于 8002 没有设置分组,会重新创建队列并监听,而 8003 创建了分组,再次启动会直接返回分组中监听到的

故group 可以解决分组消费和消息持久化两个问题

九、Sleuth

迫不及待学 Alibaba ,如果涉及 以后补全!

十、总结(有感而发)

历经约七天的学习,Spring Cloud 落下了帷幕,从第一次接触到 Spring Cloud 的时候,不亚于我刚从 SSM 过度到 Spring Boot 的惊讶,微服务为我在编程之路或许是一个长足的进步,从一个较高层次向下看,以前的一些困惑迎刃而解。

言不止,语不休,2022-01-19 15:40,结束了 Spring Cloud 的旅程,开始 Spring Cloud Alibaba ,相信在此开启此篇笔记,该是寻安身立命之时!

  • 6
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值