Spring Cloud & Spring Cloud Alibaba 知识点总结

欢迎访问:http://lss-coding.top/ 我自己的博客平台

1. 微服务架构简介

1.1 微服务架构概述

​ 2014 年 3 月 Martin Fowler 提出微服务架构

​ 微服务框架是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相协作(通常是基于 HTTP 协议的 RESTful API)。每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,应当尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建。

image-20211007170948318

之前的单体应用不利于互联网技术的发展和发达,所以拆开为一个一个的微服务。

1.2 Spring Cloud 简介

COORDINATE ANYTHING:DISTRIBUTED SYSTEMS SIMPLIFIED

Building distributed systems doesn’t need to be complex and error-prone.Spring Cloud offers a simple and accessible programming model to the most common distributed system patterns,helping developers build resilient,reliable,and coordinated applications. Spring Cloud is buit on top of Spring Boot,making it easy for developers to get started and become productive quickly.

  • Spring Cloud 就是分布式微服务架构的一种体现。

  • Spring Cloud = 分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶

  • Spring Cloud 俨然已称为微服务开发的主流技术栈,在国内开发者社区非常火爆。

image-20211007171644218

image-20211007171838963

image-20211007194001384

  • 京东的微服务体系

image-20211007194145418

  • 阿里的微服务体系

image-20211007194236461

  • 京东物流

image-20211007194302765

image-20211007194336361

1.3 Spring Cloud 技术栈

image-20211007194445608

image-20211007194613064

image-20211007194704403

2. Boot 和 Cloud 版本选择

  • Spring Boot 版本是用数字进行命令的

image-20211007195534602

  • Spring Cloud 采用了英国伦敦地铁站的名称来命名,并由地铁站名称字母 A-Z 依次类推的形式来发布迭代版本

    Spring Cloud 是一个由许多子项目组成的综合项目,各子项目有不同的发布节奏。为了管理 Spring Cloud 与各子项目的版本依赖关系,发布了一个清单,其中包括了某个 Spring Cloud 版本对应的子项目版本。为了避免 Spring Cloud 版本号与子项目版本号混淆,Spring Cloud 版本采用了名称而非版本号的命名,这些版本的名字采用了伦敦地铁站的名字,根据字母表的顺序来对应版本时间顺序。例如 Angel 是第一个版本,Brixton 是第二个版本。

    当 Spring Cloud 的发布内容积累到临界点或者一个重大 BUG 被解决后,会发布一个 ‘service releases’ 版本,简称 SRX 版本,比如 Greenwich.SR2 就是 Spring Cloud 发布的 Greenwich 版本的第 2 个 SRX 版本。

image-20211007202231794

Spring Boot 和 Spring Cloud 的依赖关系

网址:https://spring.io/projects/spring-cloud#overview

image-20211007202807963

更详细的版本对应依赖

网址:https://start.spring.io/actuator/info

image-20211007203722286

官网使用的时候给出相应版本

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

image-20211007204230956

3. Cloud 各种组件的停更/升级/替换

image-20211007205320156

4. 微服务架构编码构建

4.1 构建父工程 Project

1. 创建一个 maven 项目

image-20211007211152791

2. 编码设置为 UTF-8

image-20211007211559904

3. 注解生效激活

image-20211007211738556

4.2 重写 pom.xml 文件
<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.lss</groupId>
  <artifactId>springcloud</artifactId>
  <version>1.0-SNAPSHOT</version>
  <!--表示是一个父工程-->
  <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>5.1.47</mysql.version>
    <druid.version>1.2.6</druid.version>
    <mybatis.spring.boot.version>2.2.0</mybatis.spring.boot.version>
  </properties>

  <!--子模块继承之后,提供作用:锁定版本+子module 不用写 groupId 和 version-->
  <dependencyManagement>
    <dependencies>
      <!--spring boot 2.4.6-->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.5.4</version>
      </dependency>
      <!--            spring cloud 2020.0.3-->
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>2020.0.4</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>2021.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
      </dependency>
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid.version}</version>
      </dependency>
      <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>${mybatis.spring.boot.version}</version>
      </dependency>
    </dependencies>

  </dependencyManagement>
</project>
4.3 dependencyManagement 和 dependencies 区别

maven 使用 这个 dependencyManagement 元素来提供了一种管理依赖版本号的方式,通常会在一个组织或者项目的最顶层的父 pom 中看到 dependencyManagement 元素。

使用 pom.xml 中的 dependencyManagement 元素能让所有子项目中引用一个依赖而不用显式的列出版本号。Maven 会沿着父子层次向上走,直到找到一个拥有 dependencyManagement 元素的项目,然后它就会使用这个 dependencyManagement 元素中指定的版本号。

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

**注意:**dependencyManagement 里只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写入了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且 version 和scope 都读取自父 pom;如果子项目中指定了版本号,那么会使用子项目中指定的 jar 版本。

4.4 构建一个支付模块

1. 创建一个空的 Maven 项目,子 Module

image-20211008084850500

​ 创建完成之后,可以在 父工程的 pom.xml 文件中看到子模块的信息

<groupId>org.lss</groupId>
<artifactId>springcloud</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
  <module>cloud-provider-payment8001</module>
</modules>

​ 子模块中也可以看到关于父工程的信息

<parent>
    <artifactId>springcloud</artifactId>
    <groupId>org.lss</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!--因为引入了父工程,所以这里没有 artifactId 和 version-->
<artifactId>cloud-provider-payment8001</artifactId>

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>springcloud</artifactId>
        <groupId>org.lss</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-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>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

**3. 写 yml **

server:
  port: 8001

# 服务名称
spring:
  application:
    name: cloud-payment-service
  datasource:
    # 数据源操作类型
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/studentgrade?userUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: root
    
mybatis:
  mapper-locations: classpath:mapper/*.xml
  # 所有实体类别名类所在包
  type-aliases-package: com.lss.springcloud.pojo

4. 主启动类

@SpringBootApplication
public class PaymentMain8001 {

    public static void main(String[] args) {

        SpringApplication.run(PaymentMain8001.class,args);

    }

}

5. 业务类

  1. 创建表
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
  1. 创建实体类
  • 表的实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {

    private Long id;
    private String serial;

}
  • 用于前后端分离返回信息的实体类
/** * 用于前后端分离的时候给前端返回状态信息 * 返回前端的通用 json 字符串 */@Data@AllArgsConstructor@NoArgsConstructorpublic class CommonResult<T> {        // 状态码        private Integer code;        // 信息        private String message;        // 数据        private T data;        public CommonResult(Integer code,String message){                this(code,message,null);        }}
  1. 接口层 mapper
@Mapperpublic interface PaymentMapper {        // 插入一条数据        int save(Payment payment);        // 根据 id 查询一条数据        Payment getPaymentById(@Param("id") Long id);}

在 resources 文件夹下创建一个 mapper 文件夹(位置:因为在 yml 配置文件中指定了mapper的位置),用于放接口的映射

<?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.lss.springcloud.mapper.PaymentMapper">	    <!--useGeneratedKeys="true" 返回值-->        <insert id="save" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">                insert into payment (sarial) values(#{serial});        </insert>        <!--结果集映射,因为当表多的时候,java 的类型的 userName  表中的类型的 user_name 所以做一个映射-->        <resultMap id="BaseResultMap" type="com.lss.springcloud.pojo.Payment">                <id column="id" javaType="id" jdbcType="BIGINT"/>                <id column="serial" javaType="serial" jdbcType="VARCHAR"/>        </resultMap>        <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">                select * from payment where id= #{id}        </select></mapper>
  1. service
  • service 接口
public interface PaymentService {        // 插入一条数据        int save(Payment payment);        // 根据 id 查询一条数据        Payment getPaymentById(@Param("id") Long id);}
  • service 接口的实现类
@Servicepublic class PaymentServiceImpl implements PaymentService {        @Autowired        private PaymentMapper paymentMapper;        @Override        public int save(Payment payment) {                return paymentMapper.save(payment);        }        @Override        public Payment getPaymentById(Long id) {                return paymentMapper.getPaymentById(id);        }}
  1. controller
@RestController@Slf4jpublic class PaymentController {        @Autowired        private PaymentService paymentService;        @PostMapping(value = "/payment/save")        public CommonResult save(Payment payment) {                int result = paymentService.save(payment);                log.info("==========插入结果:" + result);                if (result > 0) {                        return new CommonResult(200, "插入数据成功", payment);                } else {                        return new CommonResult(444, "插入数据失败", null);                }        }        @GetMapping(value = "/payment/get/{id}")        public CommonResult gatPaymentById(@PathVariable("id") Long id) {               Payment payment = paymentService.getPaymentById(id);                log.info("==========查询结果:" + payment);                if (payment != null) {                        return new CommonResult(200, "查找成功", payment);                } else {                        return new CommonResult(444, "查无此人", null);                }        }}
  1. 测试

在浏览器中,提交的方式是不支持 post 方式的,所以只能在浏览器中访问 get 请求进行查询数据。

image-20211008170250021

要想测试 post 方式的提交,需要借助工具(postman) 进行测试。

image-20211008170325691

测试成功

4.5 开启热部署
  1. 导入依赖
<dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-devtools</artifactId>        <scope>runtime</scope>        <optional>true</optional></dependency>
  1. 在父工程中引入一个打包插件
<build>      <plugins>            <plugin>                  <groupId>org.springframework.boot</groupId>                  <artifactId>spring-boot-maven-plugin</artifactId>                  <configuration>                        <fork>true</fork>                        <addResources>true</addResources>                  </configuration>            </plugin>      </plugins></build>
  1. 开启自动编译的选项

image-20211008171544001

  1. 更新一些选项

image-20211008171750652

image-20211008171857627

image-20211008171928632

  1. 重启 idea
4.6 构建消费者模块
  1. 创建子模块 cloud2021-consumer-order80
  2. 引入引入依赖:spring-boot-starter-web,spring-boot-starter-actuator,spring-boot-starter-test,lombok,spring-boot-devtools
  3. 创建 yml 配置文件,只需要写上 server.port=80 即可
  4. 因为订单模块需要用到 Payment 实体类,所以创建实体类和通用的那个返回值类。
  5. 创建 controller 类
@RestController@Slf4jpublic class OrderController {        public static final String PAYMENT_URL = "http://localhost:8001";            @Autowired        private RestTemplate restTemplate;            @GetMapping("/consumer/payment/save")        public CommonResult<Payment> save(Payment payment) {                return restTemplate.postForObject(PAYMENT_URL + "/payment/save", payment, CommonResult.class);        }        @GetMapping("/consumer/payment/get/{id}")        public CommonResult<Payment> get(@PathVariable("id") Long id) {                return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);        }}

在 controller 中需要使用到一个 RestTemplate

RestTemplate 提供了很多便捷访问远程 Http 服务的方法,是一种简单便捷的访问 restful 服务模板类,是 Spring 提供的用于访问 Rest 服务的客户端模板工具集。其实就是完成 80 和 8001 之间的远程调用,一个调用接口方式的封装。

使用:

  • 使用 restTemplate 访问 restful 接口非常的简单,(url,requestMap,ResponseBean.class)这三个参数分别代表 REST 请求地址、请求参数、HTTP 响应转换被转换成的对象类型。
  1. 创建 RestTemplate 的配置类
@Configurationpublic class ApplicationContextConfig {        @Bean        public RestTemplate getRestTemplate() {                return new RestTemplate();        }}
  1. 测试

运行 8001 和 80 端口的模块,然后进行访问,可以看到结果都是相同的

image-20211008200058508

如果访问添加数据 localhost/consumer/payment/save?serial=E00001 会发现浏览器提示是成功了,但是数据库中并没有真正的插入数据

image-20211008200636144

image-20211008200650437

因为在真正执行插入操作的 8001 端口模块的save 操作中,需要接受一个参数,所以需要加上 @RequestBody 注解在方法参数上,这样就可以正常的添加数据了

4.7 工程重构

在我们两个模块中,可以发现有重复的部分

image-20211008202040237

  1. 创建一个公共模块 cloud-api-commons
  2. 修改 pom.xml 将公共的坐标放进去
<dependencies>        <dependency>                <groupId>org.projectlombok</groupId>                <artifactId>lombok</artifactId>                <optional>true</optional>        </dependency>        <dependency>                <groupId>org.springframework.boot</groupId>               <artifactId>spring-boot-devtools</artifactId>                <scope>runtime</scope>                <optional>true</optional>        </dependency>        <dependency>                <!--常用的工具包-->                <groupId>cn.hutool</groupId>                <artifactId>hutool-all</artifactId>                <version>4.1.13</version>        </dependency></dependencies>
  1. 然后将各个子模块公共部分放到公用api 这个模块中,删除掉子模块中的相同部分

image-20211008203333228

  1. 清理一下,然后安装到 maven 仓库中

image-20211008203530175

  1. 然后需要使用的子模块中引入依赖即可使用
<dependency>        <groupId>org.lss</groupId>        <artifactId>cloud-api-commons</artifactId>        <version>${project.version}</version></dependency>
  1. 测试运行成功

5. Eureka 服务注册与发现

5.1 Eureka 简介
5.1.1 什么是服务治理

​ Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理

​ 在传统的 rpc 远程调用框架中,管理每个服务于服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务与服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。

5.1.2 什么是服务注册

​ Eureka 采用了 CS 的设计架构,Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。

​ 在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息比如服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者|服务提供者),以该别名的方式去注册中心获取到实际的服务通讯地址,然后再实现本地 RPC 调用 RPC 远程调用结构核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何 rpc 远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址和))

image-20211009085233223

5.1.3 Eureka 两组件

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

  • Eureka Server 提供服务注册服务

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

  • Eureka Client 通过注册中心进行访问

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

5.2 单机 Eureka 构建
5.2.1 构建服务注册中心
  1. 创建子项目 cloud-eureka-server7001

  2. 引入依赖

    其他的与之前一样

<!--eureka server--><dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>
  1. 在 yml 配置文件中配置 eureka
server:  	port: 7001eureka:  	instance:    	# eureka 服务端的实例名称    	hostname: localhost  client:    	# false 表示不向注册中心注册自己    	register-with-eureka: false    	# false 表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务    	fetch-registry: false    	service-url:      	# 设置与 Eureka Server 交互的地址查询服务和注册服务都需要依依赖这个地址。      		defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  1. 主启动类
@SpringBootApplication@EnableEurekaServer		// 标注这个模块就是 Eureka 的注册中,来进行服务注册登记管理。。。public class EurekaMain7001 {}
  1. 启动测试

    访问端口:localhost:7001,出现以下界面表明服务注册中心建立成功

image-20211009092329477

5.2.2 将 8001 支付模块注册进注册中心,成为服务提供者
  1. 修改 8001 支付模块的 pom.xml 文件
<!--Eureka Client--><dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-netflix-eureka-client</artifactId></dependency>
  1. 配置 yml 文件
eureka:  	client:    	# 表示是否将自己注册进 EurekaServer 默认为true    	register-with-eureka: true    	# 是否从 EurekaServer 抓取已有的注册信息,默认为 true。单点无所谓,集群必须设置为 true 才能配合 ribbon 使用负载均衡    	fetchRegistry: true    	service-url:      		defaultZone: http://localhost:7001/eureka
  1. 主启动类添加注解 @EnableEurekaClient
@SpringBootApplication@EnableEurekaClientpublic class PaymentMain8001 {
  1. 启动查看

image-20211009094240941

image-20211009094332711

5.2.3 将 80 订单模块注册进注册中心,成为服务消费者

与上面一样

  1. 导入依赖 spring-cloud-starter-netflix-eureka-client
  2. 编写 yml 配置文件
  3. 在主启动类加上 @EnableEurekaClient 注解
  4. 启动测试

注册成功

image-20211009095254250

5.3 集群 Eureka 构建
5.3.1 Eureka 集群原理说明

Eureka Server 700

  • 服务注册:将服务信息注册进注册中心

  • 服务发现:从注册中心上获取服务信息

  • 实质:存 key 服务名,取 value 调用地址

  1. 先启动 eureka 注册中心
  2. 启动服务提供者 payment 支付服务
  3. 支付服务启动后会把自身信息(比如服务地址以别名方式注册进 eureka)
  4. 消费者 order 服务在需要调用接口时,使用服务别名去注册中心获取实际的 RPC 远程调用地址
  5. 消费者获得调用地址后,底层实际是利用 HttpClient 技术实现远程调用
  6. 消费者获得服务地址后会缓存在本地 jvm 内存中,默认每间隔 30 秒更新一次服务调用地址

微服务 RPC 远程服务调用最核心的是什么?

​ 高可用,假如注册中心只有要给,故障后会导致整个微服务环境不可用

​ 解决方法:搭建 Eureka 注册中心集群,实现负载均衡+故障容错

5.3.2 构建注册中心

image-20211009104133539

  1. 新建 module ,cloud-eureka-server7002,与 cloud-eureka-server7001 类似
  2. 修改 pom.xml 依赖,与 cloud-eureka-server7001 一样
  3. 修改 yml 配置文件,因为集群中的服务中心是相互注册,相互守望

因为有两个服务,所以名字不能相同,在修改 yml 之前需要修改映射配置文件,模拟是两台机器,实质还是一个

image-20211009102751139

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

  2. 启动测试

image-20211009104001976

5.3.3 将 80 和 8001 模块发布到 7001 和 7002

只需要在 80 和 8001 模块修改 yml 配置文件即可

eureka:  	client:    		register-with-eureka: true    		fetch-registry: true    		service-url:		# 只需要修改这里即可      			defaultZone: http://eureka7001.com:7001/eureka,http://eureka7001.com:7001/eureka

启动测试:

  1. 先启动 EurekaServer 注册中心,7001 和 7002 服务
  2. 再启动服务提供者 8001
  3. 再启动消费者 80

image-20211009104915994

5.3.4 支付服务提供者 8001 集群构建
  1. 创建子模块 cloud2021-provider-payment8002

  2. pom.xml 修改

    cloud2021-provider-payment8001 一样 复制粘贴

  3. yml 修改

    cloud2021-provider-payment8001 一样 复制粘贴,需要修改 端口号为 8002

  4. 主启动类

  5. 业务类:mapper,controller,service

    直接从 cloud2021-provider-payment8001 复制粘贴即可

在 yml 文件中可以看到两个服务对外的名字都是一样的

spring:  	application:    	name: cloud-payment-service

因为是同一个名字,所以我们访问业务的时候区分不出是那一个服务提供者提供的服务(8001 or 8002 ?)

所以修改以下 controller ,来做一下区分,8001 和 8002 都要进行修改

@Value("${server.port}")	// 得到服务的端口号    private String serverPort;    @Autowired    private PaymentService paymentService;    @PostMapping(value = "/payment/save")    public CommonResult save(@RequestBody Payment payment) {            int result = paymentService.save(payment);            log.info("==========插入结果:" + result);            if (result > 0) {				        // 业务操作执行完后给出的结果信息中携带上端口信息                    return new CommonResult(200, "插入数据成功,port:" + serverPort, payment);            } else {                    return new CommonResult(444, "插入数据失败,port:" + serverPort, null);            }    }

测试

首先访问 eureka7001.com:7001eureka7002.com:7002 判断集群是否启动成功

image-20211009132644944

这时候访问 localhost/consumer/payment/get3 访问的一直都是 8001服务,因为在 orderController 中写死了,需要进行修改

@RestController@Slf4jpublic class OrderController {	    // 通过在 eureka 上注册的微服务名称调用        public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

修改完再次重启进行访问测试

这次直接报错了,因为我们的微服务名称下面不止有一个服务,所以访问的时候就不能判断用那个进行响应了。

image-20211009133231415

解决方式:在 ApplicationContextBean 类中对 RestTemplate 组件加上注解:@LoadBalanced 赋予 RestTemplate 负载均衡的能力

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

重启后再次访问,提示访问成功,并且有了负载均衡的能力

image-20211009133809577

5.4 actuator 微服务信息完善

可以配置也可以不配置

5.4.1 主机名称:服务名称修改

在注册中心可以看到,我们注册的实例会显示有主机名称,我们希望的是只显示服务名称而不显示主机名

image-20211009134429045

解决方法:

  1. 修改 8001 和 8002 模块的 yml 配置文件
eureka:  	client:    	# 表示是否将自己注册进 EurekaServer 默认为true    	register-with-eureka: true    	# 是否从 EurekaServer 抓取已有的注册信息,默认为 true。单点无所谓,集群必须设置为 true 才能配合 ribbon 使用负载均衡    fetchRegistry: true        service-url:          	defaultZone: http://eureka7001.com:7001/eureka,http://eureka7001.com:7001/eureka      	instance:        		instance-id: payment8001	# 定义服务器名称
  1. 测试运行,可以看到注册中心的服务器名称已经换成了我们自己定义的 id了

image-20211009135326944

5.4.2 访问信息有 IP 信息提示

当鼠标放到实例上的时候左下角不显示ip 地址,如果之后部署的时候就是说部署到那个ip地址的那个端口上,不显示 ip 看起来不是太方便。

image-20211009134539113

解决方法:

  1. 修改 yml 文件,加上 prefer-ip-address: true 选项
  instance:      	instance-id: payment8002      	prefer-ip-address: true
  1. 重启服务就可以看到了

image-20211009135701543

5.5 服务发现 Discovery

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

  1. 在 8001 的controller 中创建一个controller
@RestController@Slf4jpublic class PaymentController {        @Resource        private DiscoveryClient discoveryClient;            @GetMapping("/payment/discovery")    public Object discovery() {            List<String> services = discoveryClient.getServices();            for (String service : services) {                    log.info("=======" + service);            }            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;    }
  1. 主启动类上加注解:@EnableDiscoveryClient
@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClientpublic class PaymentMain8001 {
  1. 启动访问 localhost:8001/payment/discovery

可以看到有两个服务

image-20211009145444305

在 idea 控制台也可以看到相关信息

​ 有两个服务,CLOUD-PAYMENT-SERVICE 服务中有两个实例,端口号,地址都可以进行显示。

image-20211009145512280

5.6 Eureka 的自我保护机制
5.6.1 自我保护模式概述

概述:保护模式主要用于一组客户端和 Eureka Server 之间存在网络分区场景下的保护,一旦进入保护模式,Eureka Server 将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。

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

image-20211009150727504

一句话解释就是:某时刻某一个微服务不可以用了,Eureka 不会立刻清理,依旧会对该微服务的信息进行保存,属于 CAP 里面的 AP 分支。

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

为了防止 EurekaClient 可以正常运行,但是与 EurekaServer 网络不通情况下,EurekaServer 不会立刻将 EurekaClient 服务提出。

什么是自我保护模式?

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

image-20211009153858636

自我保护机制:默认情况下 EurekaClient 定时向 EurekaServer 端发送心跳包

如果 Eureka 在 Server 端在一定时间内(默认 90)没有收到 EurekaClient 发送心跳包,便会直接从服务注册列表中剔除该服务,但是在短时间(90秒中)内丢失了大量的服务实例心跳,这时候 EurekaServer 会开启自我保护机制,不会剔除该服务(该现象可能出现在如果网络不通但是 EurekaClient 出现宕机,此时如果换做别的注册中心如果一定时间内没有收到心跳将会剔除该服务,这样就出现了严重失误,因为客户端还能正常发送心跳,只是网络延时问题,而保护机制是为了解决此问题而产生的)

在自我保护模式中,Eureka Server 会保护服务注册表中的信息,不再注销任何服务实例。

它的设计理念就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。

综上所述:自我保护模式是一种应对网络异常的安全保护措施。它的架构理念就是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让 Eureka 集群更加的健壮、稳定。

5.6.2 解除自我保护模式
  1. 修改注册中心 7001

模式自我保护机制是开启的

image-20211009155146166

在yml 配置文件中设置 eureka.server.enable-self-preservtion=false 设置为 false 表示禁用自我保护模式

还需要设置一下心跳时间:eviction-interval-timer-in-ms: 2000 调小

运行访问:eureka7001.com:7001 给出提示已经关闭

image-20211009160950115

  1. 修改客户端 8001

修改 yml 配置文件 ,设置心跳时间

instance:  	instance-id: payment8001  	prefer-ip-address: true      	# Eureka 客户端向服务端发送心跳的时间间隔,单位为秒(默认是 30 秒)  	lease-renewal-interval-in-seconds: 1  	# Eureka 服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是 90 秒),超时将被剔除服务  	lease-expiration-duration-in-seconds: 2

启动 8001 ,在注册中心中注册成功,当停止掉 8001 服务的时候,注册中心马上剔除该服务信息。这就是关闭了保护模式。

6. Zookeeper 替换 Eureka

6.1 准备工作

Zookeeper 是一个分布式协调工具,可以实现注册中心功能。

在 Linux 虚拟机中关闭防火墙,之前是已经安装并配置好了的,启动 Zookeeper 服务器。

Zookeeper 服务器取代 Eureka 服务器,zk 作为服务注册中心。

6.3 provider 构建
  1. **创建子模块 cloud-provider-payment8004 **

  2. 修改pom.xml文件

    只需要配置 zookeeper 的依赖,其他的与之前一样 web、actuator、lombok…

<!--SpringBoot integration zookeeper Client--><dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId></dependency>
  1. 创建 yml 配置文件

    与之前类似,配置 port 端口号,spring.application.name 服务名称,还有就是 zookeeper 的连接配置

server:  	port: 8004spring:  	application:    	name: cloud-provider-payment  	cloud:    		zookeeper:    		# zookeeper 连接IP地址端口号      		connect-string: 192.168.253.131:2181
  1. 主启动类

    @EnableDiscoveryClient

@SpringBootApplication// 该注解用于向使用 consul 或者 zookeeper 作为注册中心时注册服务@EnableDiscoveryClientpublic class PaymentMain8004 {
  1. controller编写接口进行测试

    为了方便只是返回了一个访问服务的端口和一个随机的值,没有加入访问数据库的操作

@RestControllerpublic class PaymentController {        @Value("${server.port}")        private String serverPort;        @GetMapping("/payment/zk")        public String paymentZk() {                return "Spring Cloud with zookeeper:" + serverPort + "\t" + UUID.randomUUID().toString();        }}
  1. 运行测试

    可以看到端口信息,表示整体搭建成功

image-20211009170812973

  1. 查看虚拟中 zookeeper 中的信息

    加入了一个 services 路径,让路径下面找会得到一个 json 字符串,这个字符串就是服务在 zookeeper 上的基本信息

image-20211009170900037

image-20211009171036319

至此微服务 cloud-provider-payment 成功入驻进 zookeeper

**注意:**注册进 zookeeper 的是临时的 Znode 节点,当停掉 cloud-provider-payment 的时候,过了心跳时间,zookeeper 接受不到传来的心跳就会直接剔除掉这个服务。

6.4 consumer 构建
  1. 创建模块 cloud-consumer-order80

  2. 修改 pom.xml 文件,与 provider 一样

  3. 修改 yml 配置文件,与 provider 一样,只有 端口号和服务名称不一样

  4. 启动类

  5. 因为需要用到 RestTempalte 来进行 http 之间的通信,所以创建一个 config 类

@Configurationpublic class ApplicationContextConfig {        @Bean        @LoadBalanced        public RestTemplate getRestTemplate() {                return new RestTemplate();        }}
  1. controller 业务层
@RestControllerpublic class OrderZkController {	// 这个名字就是 provider 注册到 zookeeper 中的服务名称        public final static String INVOKE_URL = "http://cloud-provider-payment";        @Autowired        public RestTemplate restTemplate;        @GetMapping("/consumer/payment/zk")        public String orderZk() {                return restTemplate.getForObject(INVOKE_URL + "/payment/zk", String.class);        }}
  1. 测试运行

    首先查看两个服务是否都注册进 zookeeper

image-20211009194021213

​ 浏览器地址栏访问 localhost/consumer/payment/zk,可以看到访问到了 provider 的信息

image-20211009194056699

7. Consul 服务注册与发现

7.1 简介

官网:https://www.consul.io/

What is Consul?

Consul is a service mesh solution providing a full featured control plane with service discovery,configuration,and segmentation functionality. Each of these features can be used individually as needed, or they can be used together to build a full service mesh. Consul requires a data plane and supports both a proxy and native integration model. Consul ships with a simple built-in proxy so that everything works out of the box, but also supports 3rd party proxy integrations such as Envoy.

Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 GO 语言开发

提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之 Consul 提供了一种完整的服务网格解决方案。

它具有很多有点。包括:基于 raft 协议,比较简洁;支持健康检查,同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持 Linux、Mac、Windows

可以做

  • 服务发现(提供HTTP 和 DNS 两种发现方式)
  • 健康检测(支持多种方式,HTTP、TCP、Docker、Shell 脚本定制化)
  • KV 存储(Key、Value 的存储方式)
  • 支持多数据中心
  • 可视化 Web 界面
7.2 Consul 安装

下载地址:

https://www.consul.io/downloads

image-20211009195636733

下载好之后进行解压,解压之后只有一个 consul.exe 文件

image-20211009200310208

双击运行,有可能会闪退,需要自行配置环境变量

在 cmd 中使用命令 consul agent -dev

image-20211009200920675

启动后在 浏览器访问本地端口 localhost:8500 就可以看到图形界面了

image-20211009201014621

7.3 provider 的构建

与上面一样

  1. 创建子模块 cloud-privoder-payment8006

  2. 配置 pom.xml

    因为使用 consul 所以需要换成这个jar 包依赖

<!--SpringBoot integration consul Client--><dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-consul-discovery</artifactId></dependency>
  1. 配置 yml 配置文件
server:  	port: 8006spring:  	application:    		name: consul-provider-payment		# consul Registry  	cloud:    		consul:      			host: localhost      			port: 8500      			discovery:        			# hostname : 127.0.0.1        				service-name: ${spring.application.name}
  1. 主启动类

  2. controller 测试访问

@RestControllerpublic class PaymentController {        @Value("${server.port}")        private String serverPort;        @GetMapping("/payment/consul")        public String consulPayment() {                return "Spring Cloud with port:" + serverPort +                "\t" + UUID.randomUUID().toString();    }}
  1. 测试启动

image-20211009212325748

在 consul 带的界面里面可以看到已经注册进入的服务,localhost:8500

image-20211009212407632

7.4 consumer 的构建
  1. 创建子项目 cloud-consumerconsul-order80
  2. pom.xml,与上面一样
  3. yml 配置文件,与上面一样
  4. 主启动类
  5. RestTemplate 的配置 Bean
  6. controller
@RestControllerpublic class OrderController {        private static final String INVOKE_URL = "http://consul-provider-payment";        @Resource        private RestTemplate restTemplate;        @GetMapping("/consumer/payment/consul")        public String order() {                return restTemplate.getForObject(INVOKE_URL + "/payment/consul", String.class);        }}
  1. 测试运行

    两个服务都注册进去了

image-20211009213930872

image-20211009213950914

7.5 三个注册中心的异同点
组件名语言CAP服务健康检查对外暴露接口Spring Cloud继承
EurekaJavaAP(A高可用)可配支持HTTP已集成
ConsulGoCP(C数据一致)支持HTTP/DNS已集成
ZookeeperJavaCP支持客户端已集成

CAP:

  • C:Consistency (强一致性)
  • A: Availability (可用性)
  • P: Partition tolerance (分区容错性)

CAP:理论关注粒度是数据,而不是整体系统设计的策略

image-20211009225143941

最多只能同时较好的满足两个:

CAP 理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,

因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三大类:

  • CA:单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。

  • CP(例如:Zookeeper/Consul):满足一致性,分区容忍性的系统,通常性能不是特别高。

    当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性

    违背了可用性 A 的要求,只满足一致性和分区容错,即 CP

image-20211009230309638

  • AP(例如:Eureka):满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

    当网络分区出现后,为了保证可用性,系统 B 可以返回旧值,保证系统的可用性。

    违背了一致性 C 的要求,只满足可用性和分区容错,即 AP

image-20211009230203100

案例: 加入淘宝双11,这时候是需要保证数据的一致性多呢?还是数据的可用性?

​ 对于这种情况来说,有一个商品点赞数,每一秒都有人点赞,这个解决允许出现数据的不一致性,牺牲 C 来保证 A。

8. Ribbon 负载均衡服务调用

8.1 简介

Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端 负载均衡的工具。

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

官网:https://github.com/Netflix/ribbon

Ribbon 能干什么?

负载均衡 + RestTemplate 调用

主要是用来进行 负载均衡(LB)(分为集中式LB 和 进程内 LB)

LB 负载均衡是什么?

  • 简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的 HA(高可用),常见的负载均衡有软件 Nginx,LVS,硬件 F5 等

Ribbon 本地负载均衡客户端(进程内 LB) 与 Nginx 服务端负载均衡(集中式 LB) 区别:

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

    即在服务的消费方和提供方之间使用独立的 LB 设施(可以是硬件,F5,也可以是软件 nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;

  • 进程内 LB

    将 LB 逻辑集成到消费方,消费方从服务注册中心获知有那些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon 就属于进程内 LB,它是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

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

image-20211010094053829

Ribbon 在工作时分成两步:

第一步:先选择 EurekaServer,它优先选择在同一个区域内负载较少的 server,

第二步:再根据用户指定的策略,再从 server 取到的服务注册列表中选择一个地址。

其中 Ribbon 提供了多种策略:比如轮询、随机和根据响应时间加权。

8.2 使用 Ribbon

基于 Eureka

启动 7001 7002 8001 8002 80 的服务进行测试

我们访问一个地址,他会使用两个服务进行交替的进行服务,但是查看我们 pom.xml 文件中并没有关于 Ribbon 的依赖

image-20211010095158170

原因是我们引入了 eureka 的包

<dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>这个依赖里面封装了 loadbalancer ,这里面没有找到 ribbon 只有这个,可能是因为版本更新的问题<dependency>      <groupId>org.springframework.cloud</groupId>      <artifactId>spring-cloud-loadbalancer</artifactId>      <version>3.0.4</version>      <scope>compile</scope>      <optional>true</optional></dependency>

image-20211010095621135

可以使用 坐标引入 ribbon,但是没有太大的用处,因为内部封装了 loadBalancer

<dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency>
8.3 RestTemplate 方法
  • getForOjbect 和 getForEntity
@GetMapping("/consumer/payment/get/{id}")    public CommonResult<Payment> get(@PathVariable("id") Long id) {            // 返回对象为响应体中的数据转化成的对象,基本可以理解为 json            return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);    }    @GetMapping("/consumer/payment/getForEntity/{id}")    public CommonResult<Payment> get2(@PathVariable("id") Long id) {            // 返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等     ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);    // 判断状态码是 1xx 2xx 3xx 4xx 5xx等信息            if (entity.getStatusCode().is2xxSuccessful()) {                    return entity.getBody();            }else {                    return new CommonResult<>(444,"操作失败");            }    }
8.4 Ribbon 核心组件 IRule
8.4.1 简介

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

IRule 是一个接口,为了找到这个接口,需要引入 Ribbon 的坐标

<dependency>        <groupId>com.netflix.ribbon</groupId>        <artifactId>ribbon</artifactId>        <version>2.7.18</version></dependency><dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>        <version>2.2.6.RELEASE</version></dependency>
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();    }
  • com.netflix.loadbalancer.RounRobinRule:轮询
  • com.netflix.loadbalancer.RandomRule:随机
  • com.netflix.loadbalancer.RetryRule:先按照 RoundRobinRule 的策略获取服务,如果获取服务失败则在指定时间内进行重试,获取可用的服务
  • WeightedResponseTimeRul:对 RoundRobinRule 的扩展,响应速度越快的实例选择权重越大,越容易被选择
  • BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
  • AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
  • ZoneAvoidanceRule:默认规则,复合判断 server 所在区域的性能和 server 的可用性选择服务器。
8.4.2 负载规则替换

官方文档给出警告:

这个自定义配置类不能放在 @ComponentScan 所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制目的了

  1. 建包,因为主启动类的的 @SpringBootApplication 是一个复合注解,所以主启动类所在的包及其子包都可以被扫描到的,所以需要在外面建一个包 myrule

image-20211010104527797

  1. 创建一个自己的规则类 MySelfRule
@Configurationpublic class MySelfRule {        @Bean        public IRule myRule() {                // 修改访问策略为 随机                return new RandomRule();// 修改为随机        }}
  1. 主启动类加注解:@Ribbon
@SpringBootApplication@EnableEurekaClient@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)public class OrderMain80 {
8.5 负载均衡算法

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

image-20211010112052922

注意:Spring Cloud 在 2020.0.1 版本后就剔除了 Ribbon 的内容,换成了 LoadBalancer,可能后续 Ribbon 就不会再使用了

9. OpenFeign 服务接口调用

9.1 简介

官网:

Feign 是一个声明式 WebService 客户端。使用 Feign 能让编写 Web Service 客户端更加简单,只需要创建一个接口并在接口上添加注解即可。实现微服务间的接口调用。

Feign 自带 负载均衡配置功能。

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

Feign 能干什么

旨在使编写 Java Http客户端变得更容易

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

Feign 集成了 Ribbon

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

9.2 OpenFeign consumer 构建

这里只需要自己构建一个 consumer 的服务,provider 使用 8001,8002 的服务,使用 eureka 作为注册中心

  1. 创建一个子模块 cloud-consumer-feign-order80
  2. 添加 openFeign 的依赖,其他的与之前一样
<!--feign--><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
  1. yml 配置文件,配置端口号,eureka 配置
server:  port: 80eureka:  client:    register-with-eureka: false    service-url:      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
  1. 主启动类

    需要注意,因为要使用 Feign 作为服务调用,所以需要在主启动类使用 @EnableFeignClients

@SpringBootApplication// 开启 OpenFeignClients@EnableFeignClientspublic class OrderFeignMain80 {
  1. service 接口

    Feign 是一个声明式 WebService 客户端。使用 Feign 能让编写 Web Service 客户端更加简单,只需要创建一个接口并在接口上添加注解即可。实现微服务间的接口调用。

@Component// CLOUD-PAYMENT-SERVICE eureka 集群中服务名称@FeignClient(value = "CLOUD-PAYMENT-SERVICE")public interface PaymentFeignService {	    // 这个地址映射到 8001 / 8002 服务的接口    @GetMapping(value = "/payment/get/{id}")    CommonResult<Payment> gatPaymentById(@PathVariable("id") Long id);}
  1. controller

    与之前一样,没什么区别

@RestController@Slf4jpublic class OrderFeignController {    @Resource    private PaymentFeignService paymentFeignService;    @GetMapping("/consumer/payment/get/{id}")    public CommonResult<Payment> getById(@PathVariable("id") Long id) {        System.out.println(id);        CommonResult<Payment> paymentCommonResult = paymentFeignService.gatPaymentById(id);        System.out.println(paymentCommonResult);        return paymentCommonResult;    }}
  1. 测试

    这块也是使用 轮询 的方式进行服务的调用

image-20211010142128903

总结:

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

主启动类 @EnableFeignClients 开启

业务逻辑接口(service 接口)+ @FeignClient 配置调用 provider 服务

Controller 中注入 service 接口,调用方法完成业务的处理

image-20211010142650151

9.3 OpenFeign 超时控制

消费服务调用支付服务一定会存在一种现象:超时,比如说支付服务处理完需要 3 秒钟,在微服务提供这块认为是自然正常的,但是对于消费者来说,等不了 3 秒钟,只愿意等 2 秒钟;这种情况提供者认为 3 秒正常,消费者 认为 2 秒正常,所以这样就会产生时间差,导致超时报错。

  1. 为了出现超时的情况,在 8001 服务提供端 写一个 controller ,sleep 3 秒钟,模拟超时现象
@GetMapping("/payment/feign/timeout")public String paymentFeignTimeout() {    // 这里延时 3 秒    try {        Thread.sleep(3);    } catch (InterruptedException e) {        e.printStackTrace();    }    return serverPort;}
  1. 80端口 中的Feign 的接口
@GetMapping("/payment/feign/timeout")public String paymentFeignTimeout();
  1. 80 端口中的 controller
// OpenFeign 默认等待 1 秒钟,超过后会报错@GetMapping("/consumer/payment/feign/timeout")public String paymentFeignTimeout() {    return paymentFeignService.paymentFeignTimeout();}
  1. 运行访问
  • 直接访问服务端的端口,等待 3 秒返回结果

image-20211010145933787

  • 消费端访问报错,Timeout

    image-20211010150041833

解决方法:

默认 Feign 客户端只等待 1 秒钟,但是服务端处理需要处理超过 1 秒钟,导致 Feign 客户端不想等待了,直接返回报错。为了避免的这种情况,有时候我们需要设置 Feign 客户端的超时控制

在 yml 中配置

feign:  httpclient:    # 设置客户端的超时时间为 5 秒中    connection-timeout: 5000

如果是 2020.0.1 之前的版本,feign 中自动封装了 ribbon,可以进行如下设置:

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

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

日志级别:

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

使用

  1. 创建一个配置类
@Configurationpublic class FeignConfig {    @Bean    Logger.Level feignLoggerLevel() {        return Logger.Level.FULL;	// 设置日志级别为 FULL    }}
  1. yml 配置文件
logging:  level:    # feigin 日志以什么级别监控那个接口    com.lss.springcloud.service.PaymentFeignService: debug
  1. 运行测试

    可以看到日志信息非常的详细

image-20211010153044248

10. Hystrix 断路器

10.1 概述
  • 分布式系统面临的问题

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

image-20211010154157528

服务雪崩:

多个微服务之间调用的时候,假设微服务 A 调用微服务 B 和微服务 C,微服务 B 和微服务 C 又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓“雪崩效应“

对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接受流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

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

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

Hystrix主要用来 服务降级、服务熔断、接近实时的监控

10.2 Hystrix 重要概念
  • 服务降级

    向调用方法回一个符合预期的、可处理的备选响应(FallBack)

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

    会出现降级的情况:程序运行异常、超时、服务熔断触发服务降级、线程池/信号量打满也回到导致服务降级

if () {} else if () {} else if () {} else {    // 对方系统不可用了,必须有一个兜底的解决方法}
  • 服务熔断

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

  • 服务限流

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

10.3 Hystrix 构建

由于在 Spring 2020.0.1 之后就不支持 Hystrix 了,(因为是 netflix 公司的组件都停止维护了),所以在使用这块的时候将 Spring Boot 版本退回到 2.2.2.RELEASE ,响应的 web、acutator 等一些启动器也退回低版本

  1. 创建子项目 Hcloud-provider-hystrix-payment8001

    这个子项目中所有引用的 jar 包都加上低版本的版本号,否则回集成 父依赖的版本。为了迎合这个低版本又重新建了一个 eureka 的注册中心的子模块 Hcloud-eureka-server7001

    内容与之前创建没有区别,只是版本切换了一下

  2. 修改 pom.xml

    版本降低

    加入 Hystrix 的依赖

<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>    <version>2.2.9.RELEASE</version></dependency>
  1. 修改 yml 配置文件

    与之前配置一样,配置端口号,服务名称,注册到 eureka 注册中心

server:  port: 8001eureka:  client:    # 表示是否将自己注册进 EurekaServer 默认为true    register-with-eureka: true    # 是否从 EurekaServer 抓取已有的注册信息,默认为 true。单点无所谓,集群必须设置为 true 才能配合 ribbon 使用负载均衡    fetchRegistry: true    service-url:      defaultZone: http://eureka7001.com:7001/eureka  instance:    instance-id: Hpayment8001    prefer-ip-address: true    # Eureka 客户端向服务端发送心跳的时间间隔,单位为秒(默认是 30 秒)    #    lease-renewal-interval-in-seconds: 1    # Eureka 服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是 90 秒),超时将被剔除服务spring:  application:    name: hcloud-payment-hystrix-service#    lease-expiration-duration-in-seconds: 2
  1. 主启动类,与之前一样,不在赘述

  2. service 业务代码

    就是简单的返回一个字符串,并没有进行数据库的访问等一些操作

@Servicepublic class PaymentService {    // 模拟正常访问    public String payment_Ok(Integer id) {        return "thread pool:" + Thread.currentThread().getName() + "    payment_Ok  id: " + id;    }	// 间隔 3 秒,模拟超时时候用    public String payment_Timeout(Integer id) {        try {            Thread.sleep(3000);        } catch (InterruptedException e) {            e.printStackTrace();        }        return "thread pool:" + Thread.currentThread().getName() + "    payemnt_Timeout   id: " + id + " time: 3 seconds";    }}
  1. controller

    简单的路径,调用 service 结果 return 给页面

@RestControllerpublic class PaymentServiceController {    @Resource    private PaymentService paymentService;    @Value("${server.port}")    private String serverPort;    @GetMapping("/payment/hystrix/ok/{id}")    public String payment_Ok(@PathVariable("id") Integer id) {        String s = paymentService.payment_Ok(id);        return s + "\t serverPort: " + serverPort;    }    @GetMapping("/payment/hystrix/timeout/{id}")    public String payment_Timeout(@PathVariable("id") Integer id) {        String s = paymentService.payment_Timeout(id);        return s + "\t serverPort: " + serverPort;    }}
  1. 启动注册中心 7001 ,启动支付服务 8001

    正常无出错

image-20211010194220166

image-20211010194235310

10.4 JMeter 测试高并发

开启 JMeter ,来 20000 个并发向 8001 端口,20000 个请求都去访问 payment/hystrix/timeout 服务

Jmeter 压测测试

image-20211010213326607

设置请求路径为刚才的 timeout 服务,在没有进行这个并发测试的时候 timeout 是回转 3 秒访问到,ok 是马上回访问到的

image-20211010213533657

设置好之后启动,20000 个请求进行访问

然后我们在进行访问地址 http://localhost:8001/payment/hystrix/ok/3 发现这个刚才没有并发情况的时候秒访问的请求也需要进行等待了,开始转圈圈了

image-20211010213814967

经过这个测试,发现之前不用转圈就可以访问的请求这回也被拖累的变慢了,

    @GetMapping("/payment/hystrix/ok/{id}")    public String payment_Ok(@PathVariable("id") Integer id) {        String s = paymentService.payment_Ok(id);        return s + "\t serverPort: " + serverPort;    }        @GetMapping("/payment/hystrix/timeout/{id}")    public String payment_Timeout(@PathVariable("id") Integer id) {        String s = paymentService.payment_Timeout(id);        return s + "\t serverPort: " + serverPort;    }

对于这两个方法来说,20000 个请求去访问 /payment/hystrix/timeout/{id},而我们只有一个请求去访问 /payment/hystrix/ok/{id}

由于两个服务在一个微服务里面,大部分资源访问到 timout,微服务就不得不集中资源去处理这些高并发的请求,由于这个把资源都抽空了,所以导致 ok 也会出现 等待、卡顿、延时的现象。

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

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

10.5 加入 80 消费consumer
  1. 创建子模块 Hcloud-consumer-feign-hystrix-order80
  2. 修改 pom.xml ,与 8001 的一样

这里记录一下版本号,否则容易出现冲突问题,下面的版本使用也不知道为啥能跑通

  • 8001
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId><version>2.2.9.RELEASE</version><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId><version>2.2.1.RELEASE</version><artifactId>spring-boot-starter-web</artifactId><version>2.2.2.RELEASE</version><artifactId>spring-boot-starter-actuator</artifactId><version>2.2.2.RELEASE</version>
  • 80 端口 consumer
    <dependencies>        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>            <version>2.1.2.RELEASE</version>        </dependency>        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-openfeign</artifactId>            <exclusions>                <exclusion>                    <groupId>org.springframework.cloud</groupId>                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>                </exclusion>            </exclusions>            <version>2.1.2.RELEASE</version>        </dependency>        <!--Eureka Client-->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>            <exclusions>                <exclusion>                    <groupId>org.springframework.cloud</groupId>                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>                </exclusion>            </exclusions>            <version>2.2.1.RELEASE</version>        </dependency>
  1. yml 配置文件

与之前一样

server:  port: 80eureka:  client:    register-with-eureka: false    service-url:      defaultZone: http:/eureka7001.com:7001/eureka/
  1. 主启动类

    与之前一样

  2. service ,因为使用了 feign 所以使用接口 + 注解的方式实现服务接口调用

    与之前一样,没啥区别

@Component@FeignClient(value = "HCLOUD-PAYMENT-HYSTRIX-SERVICE")public interface OrderHystrixService {    @GetMapping("/payment/hystrix/ok/{id}")    public String payment_Ok(@PathVariable("id") Integer id);    @GetMapping("/payment/hystrix/timeout/{id}")    public String payment_Timeout(@PathVariable("id") Integer id);}
  1. controller
@RestControllerpublic class OrderController {    @Resource    private OrderHystrixService orderHystrixService;    @GetMapping("/consumer/payment/hystrix/ok/{id}")    public String payment_Ok(@PathVariable("id") Integer id) {        return orderHystrixService.payment_Ok(id);    }    @GetMapping("/consumer/payment/hystrix/timeout/{id}")    public String payment_Timeout(@PathVariable("id") Integer id) {        return payment_Timeout(id);    }}
  1. 测试访问

    访问 ok 方法正常访问

image-20211011105012032

​ 访问 timeout 回出现超时的异常,也是正常的,因为没有设置 feign 的超时控制

image-20211023163920984

10.6 高并发测试 80

​ 20000 个请求同时请求 80 地址,就会出现 转圈情况了,导致了 8001 也会有转圈的情况,这就导致了消费端一直等待,可能回出现 超时的情况。

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

image-20211011105709223

10.7 解决问题

因为有许多的版本问题,脱离了上面的父工程,重新创建了一个工程,Spring Boot 2.2.2.RELEASE ,Spring Cloud Hoxton.SR12,其他的与之前类似

正是因为有了上面的故障或者访问效果不佳,所以才有了降级/容错/限流 等技术的诞生

要解决的问题:

  1. 超时导致服务器变慢(转圈)
  2. 出错(宕机或程序运行出错)

超时的时候告诉客户端(系统繁忙,请稍后重试),这样总比一直让用户看着转圈。

加入我们这个 8001 这个服务器突然出错宕机了,我们应该有一个兜底 (出错有兜底)

  • 服务提供者 8001 超时了,调用者 80 不能一直卡死等待,必须有服务降级

  • 服务提供者 8001 down 机了,调用者 80 不能一直卡死等待,必须有服务降级

  • 服务提供者 8001 ok,调用者 80 自己出故障或有自我要求(自己的等待时间小于服务提供者,80 自己处理一下降级)

10.7.1 服务降级

使用注解 @HystrixCommand

从 8001 找问题,解决问题 fallback

8001 中有一个超时的错误,3 以内正常返回结果,超过 3 秒进入兜底

public String payment_Timeout(Integer id) {    try {        Thread.sleep(3000);    } catch (InterruptedException e) {        e.printStackTrace();    }    return "thread pool:" + Thread.currentThread().getName() + "    payemnt_Timeout   id: " + id + " time: 3 seconds";}

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

  1. 可能出现异常的服务的接口上使用注解@HystrixCommand 报异常后怎么处理

    一旦调用的服务方法失败抛出了错误信息后,会自动调用 @HystrixCommand 标注好的 fallbackMethod 调用类中的指定方法给出友好提示

@HystrixCommand(fallbackMethod = "payment_TimeoutHandler",commandProperties = {        // 这个线程的超时时间是 3 秒钟,3 秒钟之内是正常的逻辑,下面的方法中是 5 秒钟,        // 超过了峰值上限就是超时了错误,超时了去兜底的方法        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")})public String payment_Timeout(Integer id) {    try {        Thread.sleep(5000);    } catch (InterruptedException e) {        e.printStackTrace();    }    return "thread pool:" + Thread.currentThread().getName() + "    payemnt_Timeout   id: " + id + " time: 3 seconds";}// 上面的方法出现问题之后,这个访问用于兜底public String payment_TimeoutHandler(Integer id){    return "thread pool:" + Thread.currentThread().getName() + " payment_TimeoutHandler id: " + id + "服务器出现异常,请稍后重试";}
  1. 在主启动类激活这个注解

    @EnableCircuitBreaker

@SpringBootApplication@EnableEurekaClient@EnableCircuitBreakerpublic class PaymentHystrixMain8001 {
  1. 测试

    上面的方法中 sleep 5 秒,峰值是 3 秒,一定回出错,出错就会进入 兜底的方法

image-20211011161028903

上面的方法中异常是超时异常,如果换成别的 int n = 10 / 0; 异常 则也会进入兜底的方法,给出提示信息

从 80 找问题,解决问题 fallback

80 定义微服务,也需要保护好自己,有时候可能 服务提供端 需要 5秒,订单这个服务等不了 5 秒,只能等 3 秒,所以就会出现超时现象,也可以与上面一样做一个兜底的 fallback

Hystrix 既可以放在服务提供方,也可以放到服务消费方,一般服务降级放到消费方。

  1. 在 yml 配置 feign 支持 hystrix
feign:  hystrix:    enabled: true
  1. 在主启动类上使用注解 @EnableHystrix 激活
@SpringBootApplication@EnableFeignClients@EnableHystrixpublic class OrderHystrix80 {
  1. 修改 controller 业务代码

    这里肯定出错,因为提供方sleep 3 秒,这个消费者只等待 1.5 秒会超时的,所以回给出 fallback

@GetMapping("/consumer/payment/hystrix/timeout/{id}")@HystrixCommand(fallbackMethod = "payment_TimeFallbackMethod", commandProperties = {        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")})public String payment_Timeout(@PathVariable("id") Integer id) {    String result = orderHystrixService.payment_Timeout(id);    return result;}public String payment_TimeFallbackMethod(Integer id) {    return "我是消费者 80,支付系统繁忙,请 10 秒后重试或者是自己本身的错误,检查一下自己";}

image-20211011163844319

上面处理的过程中出现的问题:

  1. 每一个方法都需要一个兜底的方法(造成代码膨胀)

@DefaultProperties(defaultFallback = "")

每个方法配置一个降级方法,技术上可以,实际上非常的麻烦,除了个别重要的业务有专属的 fallback,其他普通的可以通过 @DefaultProperties(defaultFallback = "") 来统一跳转到统一处理结果页面

  • 通用的和独享的分开,避免了代码膨胀,合理减少代码量。
@RestController// 3. 指定全局 fallback 方法@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")public class OrderHystrixController {    @GetMapping("/consumer/payment/hystrix/timeout/{id}")    @HystrixCommand	// 2. 在可能出现异常的方法上使用该注解    public String payment_Timeout(@PathVariable("id") Integer id) {        String result = orderHystrixService.payment_Timeout(id);        return result;    }    // 1. 定义一个全局的 fallback 方法,下面是全局 fallback    public String payment_Global_FallbackMethod() {        return "Global 异常处理信息,请稍后重试......";    }}
  1. 兜底的方法和业务逻辑的方法混在一起(耦合度高)

    这个服务调用一定回经过这个接口,所以对这个接口进行 fallback,对这个接口中的全部方法做统一的 fallback,这样可以做到解耦的效果。

image-20211011165811677

模拟宕机

服务降级,客户端去调用服务端,碰上服务端宕机或者关闭

这里服务降级是在客户端 80 完成,与服务端 8001 没有关系,只需要为 Feign 客户端定义的接口添加一个服务降级处理的实现类即可实现解耦

  1. 创建一个 PaymentHystrixService

  2. 在接口中写方法的返回值,就是 fallback 的提示信息

    根据 OrderHystrixService 这个接口,在新建的类中实现该接口,统一为接口里面的方法进行异常处理

    实现标注了@FeignClient(value = “CLOUD-PAYMENT-HYSTRIX-SERVICE”) 这个注解的接口

@Component@FeignClient(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE")public interface OrderHystrixService {    @GetMapping("/payment/hystrix/ok/{id}")    public String payment_Ok(@PathVariable("id") Integer id);    @GetMapping("/payment/hystrix/timeout/{id}")    public String payment_Timeout(@PathVariable("id") Integer id);}
@Componentpublic class PaymentFallbackService implements OrderHystrixService{    @Override    public String payment_Ok(Integer id) {        return "服务器异常";    }    @Override    public String payment_Timeout(Integer id) {        return "服务器异常";    }}
  1. 在接口的 @FeignClient 注解中进行指定创建的统一处理的 fallback 的类
@Component@FeignClient(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE",fallback = PaymentFallbackService.class)public interface OrderHystrixService {
  1. 进行测试,关掉 8001 服务的提供者,给出 fallback 信息

    此时服务端 provider 已经 down 了,但是我们做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器

image-20211011171847994

10.7.2 服务熔断

相关论文:https://martinfowler.com/bliki/CircuitBreaker.html

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

就是保险丝 服务的降级 —> 进而熔断 —> 恢复调用链路

  • 断路器:可以理解为保险丝

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

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

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

具体的操作

  1. cloud-provider-hystrix-payment8001 这个模块中的 paymentService 类上进行修改
// =====服务熔断@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {        // 开启断路器后,假设在时间窗口期(10秒)内 10 次请求有 60 次请求都是失败的断路器就起作用        @HystrixProperty(name = "circuitBreaker.enabled",value = "true"), // 是否开启断路器        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), // 请求次数        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期(范围)        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60") // 失败率达到多少后跳闸})public String paymentCircuitBreaker(@PathVariable("id") Integer id) {    if (id < 0) {        throw new RuntimeException("------id 不能为负数");    }    String serialNumber = IdUtil.simpleUUID();    return Thread.currentThread().getName() + "\t" + "调用成功,流水号:" + serialNumber;}public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {    return "id 不能为负数,请稍后重试" + id;}

HystrixCommandProperties 这个类中有我们所有可以配置的参数

image-20211011194425616

The precise way that the circuit opening and closing occurs is as follows:

一个断路器的打开和关闭的过程是按照一下这 5步骤:

  1. Assuming the volume across a circuit meets a certain threshold

    看请求次数有没有达到一定的峰值次数

    HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()

  2. And assuming that the error percentage exceeds the threshold error percentage

    错误的百分比达到了多少

    HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()

  3. Then the circuit-breaker transitions from CLOSED to OPEN

    改变状态

  4. While it is open, it short-circuits all requests made against that circuit-breaker.

    当打开的时候,在短期内所有的都不能使用

  5. After some amount of time

    时间窗口期之后尝试是不是可以让他通过以下,如果还是不能接着打开,如果服务恢复了可以用了就closed ,回到第一步

    HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds(), the next single request is let through (this is the HALF-OPEN state). If the request fails, the circuit-breaker returns to the OPEN state for the duration of the sleep window. If the request succeeds, the circuit-breaker transitions to CLOSED and the logic in 1. takes over again.

  6. PyamentController

    在 Controller 中调用

// =======服务熔断@GetMapping("/payment/circuit/{id}")public String paymentCircuitBreaker(@PathVariable("id") Integer id) {    String result = paymentService.paymentCircuitBreaker(id);    return result;}
  1. 运行结果

    正数调用成功,负数失败

    在 id 为负数时候多刷新几次,给出的提示都是 id 不能为负数.....,然后 id 换成正数,发现还是错误的提示,等一会再用正数就成功了

image-20211011195701988

短路器的三种状态

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

断路器在什么时候起作用

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

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

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

  • 当满足一定的阈值的时候(默认10秒超过 20 个请求次数)

  • 当失败 率达到一定的时候(默认 10 秒内超过 50% 的请求失败)

  • 到达以上阈值,断路器将回开启

  • 当开启的时候,所有请求都不会进行转发

  • 一段时间之后(默认是 5 秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。重复 4 和 5

断路器打开之后

  1. 再有请求调用的时候,将不会调用主逻辑,而是直接调用降级 fallback。通过断路器,实现了自动地发现错误并降级逻辑切换为主逻辑,减少响应延迟的效果。

  2. 原来的主逻辑要如何恢复呢?

    对于这个问题,hystrix 为我们实现了自动恢复功能

    当断路器打开,对主逻辑进行熔断之后,hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。

10.8 Hystrix 的工作流程

官网:http://github.com/Netflix/Hystrix/wiki/How-it-Works

Click for large view 图片打不开

image-20211011202743719

10.9 Hystrix 服务监控 Dashboard

说白了就是 web 界面的监控界面

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

  1. 新建一个项目 cloud-hystrix-dashboard
  2. pom.xml
<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId></dependency>
  1. yml 只需要加个端口
server:  port: 9001
  1. 主启动类,需要加 @EnableHystrixDashboard 注解
@SpringBootApplication@EnableHystrixDashboardpublic class HystrixDashboardMain9001 {
  1. 启动测试

    localhost:9001/hystrix

    之后可以使用这个界面监控微服务的启动情况

image-20211011203805135

使用

这里需要在主启动类加一个组件

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

否则就会出现错误

image-20211011205504176

image-20211011205534717

image-20211011205542322

image-20211011205546362

11. Gateway 新一代网关

Zuul(路由网关)是 netflix 公司的,已经停止更新了,Zuul2 也没有构建完成,所以都不在使用。

Gateway 是 Spring 研发的。

11.1 概念

官网:https://docs.spring.io/spring-cloud-gateway/docs/3.0.3/reference/html/

Spring Cloud 全家桶中有个很重要的组件就是网关,在 1.x 版本中都是采用的 Zuul 网关;

但是在 2.x 版本中,zuul 的升级一直不稳定,Spring Cloud 最后自己研发了一个网关代替 Zuul,

就是 Spring Cloud Gateway,gateway 是原 zuul1.x 版的替代。

Gateway 是在 Spring 生态系统之上构建的 API 网关服务,基于 Spring 5、Spirng Boot 2 和 Project Reactor 技术

Gateway 旨在提供一种简单而有效的方式来对 API 进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等

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

Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标就是替代 Zuul,在 Spring Cloud 2.0 以上版本中,没有对新版本的 Zuul 2.0 以上最新高性能版本进行集成,仍然还是使用 Zuul1.x 非 Reactor 模式的老版本。而为了提升网关的性能,Spring Cloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。

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

Spring Cloud Gateway 使用的 Webflux 中的 reactor-netty 响应式编程组件,底层使用了 Netty 通讯框架。

可以完成 反向代理、鉴权、流量控制、熔断、日志监控等。。。。。。

image-20211011212551533

为什么选择 Gateway?

一方面因为 Zuul1.0 已经进入了维护阶段,而且 Gateway 是 SpringCloud 团队研发的,而且很多功能 Zuul都没有用起来也非常的简单便捷。

Gateway 是基于 异步非阻塞模型上进行开发的,性能方面不需要担心。虽然 Netflix 早就发布了最新的 Zuul2.x,但 Spring Cloud 没有整合的计划,而且 Netflix 相关组件都宣布进入维护期;

Spirng Cloud Gateway 具有如下特性:

基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0 进行构建;

动态路由:能够匹配任何请求属性;

可以对路由指定 predicate(断言)和 Filter(过滤器);

集成 Hystrix 的断路器功能;

集成 Spring Cloud 服务发现功能;

易于编写的 Predicate(断言) 和 Filter(过滤器);

请求限流功能

支持路径重写。

Spring Cloud Gateway 与 Zuul 的区别

在 Spring Cloud Finchley 正式版之前,Spring Cloud 推荐的网关是 Netflix 提供的 Zuul:

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

Zuul 1模型

SpringCloud 中所集成的Zuul 版本,采用的是 Tomcat容器,使用的是传统的 Servlet IO 处理模型

Servlet 的生命周期,servlet 由 servlet container 进行生命周期管理。

container 启动时构建 servelt 对象并调用 servlet init() 进行初始化。

container 运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用 service()。

container 关闭时调用 servlet destory() 销毁 servlet。

image-20211011214138743

  • 上面模型的缺点

    servlet 是一个简单的网络 IO 模型,当请求进入 servlet container 时,servlet container 就会为其绑定一个线程,在并发不高的场景下这种模型是使用的。但是一旦高并发(比如抽风用 jemeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个 request 分配一个线程,只需要 1 个或几个线程就能应对极大并发的请求,这种业务场景下 servlet 模型没有优势

    所以 Zuul 1.x 是基于 servlet 之上的一个阻塞式处理模型,即 Spring 实现了处理所有 request 请求的一个 servelt(DispatcherServlet)并由该 servlet 阻塞式处理。所以 SpringCloud Zuul 无法摆脱 servlet 模型的弊端。

Gateway 模型

传统 Web 框架,比如:struts2、springmvc 等都是基于 Servlet API 与 Servlet 容器基础之上运行的。但是,在 Servlet3.1 之后有了异步非阻塞的支持。而 WebFlux 是一个典型非阻塞异步的框架,它的核心是基于 Reactor 的相关 API 实现的。相对于传统的 web 框架来说,它可以运行在诸如 Netty,Undertow 及支持 Servlet 3.1 的容器上。非阻塞式 + 函数式编程(Spring 5 必须让你使用java 8)

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

11.2 Gateway 的三大核心概念
11.2.1 Route(路由)

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

11.2.2 Predicate(断言)

java 8 的 java.util.function.Predicate

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

11.2.3 Filter(过滤器)

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

11.2.4 总体

image-20211012193742729

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

11.3 Gateway 工作流程

image-20211012194026705

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

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

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

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

11.4 配置网关
  1. 新建一个 cloud-gateway-gateway9527 子项目
  2. pom.xml
<!--Gateway--><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-gateway</artifactId></dependency>
  1. yml,需要将 gateway 注册到 eureka 服务中心
server:  port: 9527spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        #payment_route  路由的 id,没有固定的规则但要求唯一,建议配合服务名        - id: payment_routh          #匹配后提供服务的路由地址          uri: http://localhost:8001          predicates:            #断言,路径相匹配的进行路由            - Path=/payment/get/**                    - id: payment_routh2          uri: http://localhost:8001          predicates:            - Path=/payment/port/**eureka:  client:    register-with-eureka: true    fetch-registry: true    service-url:      defaultZone: http://eureka7001.com:7001/eureka/  instance:    hostname: cloud-gateway-service
  1. 主启动类
@SpringBootApplication@EnableEurekaClientpublic class GateWayMain9527 {    public static void main(String[] args) {        SpringApplication.run(GateWayMain9527.class,args);    }}
  1. 网关没有业务类

查看一下 cloud-provier-payment8001 controller 的访问地址:/payment/get/,/payment/port,我们目前不想暴露 8001 端口,希望在 8001 外面套一层 9527,在上面的 yml 文件中进行了配置

  1. 测试,启动 7001 服务注册中心,8001 服务提供方,9527 网关

在启动网关的时候会有以下这个错误:

Description:

Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway.

Action:

Please set spring.main.web-application-type=reactive or remove spring-boot-starter-web dependency.

原因是网关这个模块中不需要 spring-boot-starter-web 这个依赖,删掉就可以正常运行了

  • eureka 注册中心注册了 8001 服务提供者的微服务,也注册了网关的微服务

image-20211012204821261

  • 访问 localhost:8001/payment/get/3

    可以正常访问

  • 然后访问 localhost:9527/get/3 也可以正常的访问到 8001 端口

image-20211012205020219

网关的作用看到了,只要符合访问规则,就在 8001 服务前面加一个网关,通过网关的端口也可以访问到实际的服务。

image-20211012205430059

11.5 Gateway 的配置方式

Gateway 的配置方式有两种方法

第一种方法:在配置文件 yml 中进行配置,上面的 yml 配置文件

第二种方法:代码中注入 RouteLocator 的 Bean

​ 需求:通过 9527 网关访问到外网的百度新闻地址:http://news.baidu.com/guonei

在网关中创建一个网关配置类 GatewayConfig

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

image-20211012211741657

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

之前使用的是 Ribbon 使用的负载均衡,这里使用 网关来实现负载均衡

image-20211012212154252

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

启动 7001 注册中心,8001 8002 两个微服务

8001 和 8002 服务都有这个获得端口号的方法

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

修改 网关9527 的配置 yml 配置文件,开启从注册中心动态创建路由的功能

spring:  application:    name: cloud-gateway  cloud:    gateway:      discovery:        locator:          # 开启从注册中心动态创建路由的功能,利用微服务名进行路由          enabled: true      routes:        #payment_route  路由的 id,没有固定的规则但要求唯一,建议配合服务名        - id: payment_routh          #匹配后提供服务的路由地址          #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/port/**

测试就用网关实现负载均衡,访问端口一次8001 一次 8002 了

image-20211012214338837

11.7 Predicate 的使用

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

官网地址:https://docs.spring.io/spring-cloud-gateway/docs/3.0.3/reference/html/#gateway-request-predicates-factories

Spring Cloud Gateway 将路由匹配作为 Spring WebFlux HandlerMapping 基础框架的一部分

Spring Cloud Gateway 包括许多内置的 Route Predicate 工厂。所有这些 Predicate 都与 HTTP 请求的不同属性匹配。多个 Route Predicate 工厂可以进行组合。

Spring Cloud Gateway 创建 Route 对象时,使用 Route PredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。Spring Cloud Gateway 包含许多内置的 Route Predicate Factories。

所有这些谓词都匹配 HTTP 请求的不同属性。多个谓词工厂可以组合,并通过逻辑 and。

启动网关的时候会有以下信息

image-20211012214707490

在上面的 yml 配置中配置过路由

// 以这个路由为例- id: payment_routh  #匹配后提供服务的路由地址  #uri: http://localhost:8001  # 匹配后提供服务的路由地址  uri: lb://CLOUD-PAYMENT-SERVICE  predicates:# 这块有 s 表示多个    #断言,路径相匹配的进行路由    - Path=/payment/get/**	# 在控制的输出信息中可以看到 Path 这个属性    						# 也就是控制台中的其他的也可以在这里使用
  • After

    首先得到时区,ZonedDateTime.now() ,然后粘贴到 After 后面,就是在这个时间之后执行才能成功,否则报错

predicates:  #断言,路径相匹配的进行路由  - Path=/payment/get/**  # 只用 ZonedDateTime 得到时间,要在这个时间之后执行断言才能成功,才能正常访问  - After=2021-10-13T13:52:29.782+08:00[Asia/Shanghai]

image-20211013135258144

  • Cookie
# 官网内容spring:  cloud:    gateway:      routes:      - id: cookie_route        uri: https://example.org        predicates:        - Cookie=username, lss

Cookie Route Predicate 需要两个参数,一个是 Cookie name,一个是正则表达式

路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。

如上面的代码中 chocolate, ch.p 就相当于一个 kv 的格式,必须有 cookie 名和值才能断言成功进行访问。

之前使用 jmeter、postman 进行压测,这里使用 curl 工具

cmd 窗口执行命令 curl http://localhost:9527/payment/port 这里没有带 cookie,所以报错了

image-20211013140829797

执行命令 curl http://localhost:9527/payment/port --cookie "username=lss" 加上 cookie信息之后就可以正常访问了

image-20211013141314835

  • Header
spring:  cloud:    gateway:      routes:      - id: header_route        uri: https://example.org        predicates:        - Header=X-Request-Id, \d+

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

执行命令 curl http://localhost:9527/payment/port -H "H-Request-Id:123"

image-20211013142307613

  • Host
spring:  cloud:    gateway:      routes:      - id: host_route        uri: https://example.org        predicates:        - Host=**.somehost.org,**.anotherhost.org

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

执行命令 curl http://localhost:9527/payment/port -H "Host:www.cloud1.com"

image-20211013142912272

  • Method
spring:  cloud:    gateway:      routes:      - id: method_route        uri: https://example.org        predicates:        - Method=GET,POST
11.8 Filter 的使用
11.8.1 介绍

主要作用:就是在所有微服务之前挡着,全局日志记录,统一网关鉴权,请求过来了首先找到网关,匹配才能放行,放行了才能使用后面的微服务

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

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

生命周期有两个,一个在业务逻辑之前(pre),一个在业务逻辑之后(post);

种类分为单一的 GatewayFilter 和 全局的 GlobalFilter

  • 单一的

官网:https://docs.spring.io/spring-cloud-gateway/docs/3.0.3/reference/html/#gatewayfilter-factories

使用没啥区别,粘贴上去,把之前的 predicates 换成 filters

spring:  cloud:    gateway:      routes:      - id: add_request_header_route        uri: https://example.org        filters:        - AddRequestHeader=X-Request-red, blue
  • 全局的

官网:https://docs.spring.io/spring-cloud-gateway/docs/3.0.3/reference/html/#global-filters

这块需要用的话就参考官网的内容,复制粘贴就可以

11.8.2 自定义全局 GlobalFilter
  1. 创建一个类 MyLogGateWayFilter,实现 GlobalFitler 和 Ordered 接口
@Component@Slf4j// 1. 实现接口,重写方法public class MyLogGateWayFilter implements GlobalFilter, Ordered {    @Override    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {        log.info("********com in MyLogGateWayFilter: " + new Date());        // 2. 得到 request 作用域中的 key 值        String uname = exchange.getRequest().getQueryParams().getFirst("uname");        if (uname == null) {            log.info("********用户名为null,非法用户******");            // 3. 如果为 null 设置一个错误的状态码,并且返回            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);            return exchange.getResponse().setComplete();        }        // 4. 符合条件进行下一个 filter        return chain.filter(exchange);    }    @Override    public int getOrder() {        return 0;    }}
  1. 运行测试

加上 ?uname=333 成功执行

image-20211013154807737

不加 ?uname 执行失败,直接就不可以访问

image-20211013154841235

12. SpringCloudConfig 分布式配置中心

12.1 概述

官网地址:https://docs.spring.io/spring-cloud-config/docs/3.0.4/reference/html/

**分布式面临的问题:**在我们微服务中,假设 40 个微服务用同一个数据库,那么 yml 配置文件就需要写 40 次,非常的繁琐冗余;微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所有一套集中式的、动态的配置管理设施是必不可少的。Spring Cloud 提供了 ConfigServer 来解决这个问题,我们每一个微服务自己带着一个 application.yml,上百个配置文件的管理是非常麻烦的

**是什么?**Spring Cloud Config 为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。

image-20211013160854057

Spirng Cloud Config 分为服务端和客户端两部分

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

主要作用:

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

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

12.2 Config 服务端配置与测试
12.2.1 准备工作
  1. 在 Gitee 上创建一个仓库(Github 访问较慢,所以不使用)

image-20211013162910517

  1. 在本地建一个文件夹,然后使用 git 工具执行命令 git clone gitee仓库地址 复制到本地

image-20211013163205623

  1. 如果需要修改的命令
    • git add.
    • git commit -m “init yml”
    • git push origin master
12.2.2 idea 搭建配置中心
  1. 新建一个子模块 cloud-config-center-3344 ,即为 Cloud 的配置中心模块 cloudConfig Center
  2. pom.xml,只需要加一个 config 的依赖,其他的与之前一样
<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-config-server</artifactId></dependency>
  1. yml 配置文件
server:  port: 3344spring:  application:    # 注册到 Eureka 的微服务名    name: cloud-config-center  cloud:    config:      server:        git:          # Github 上面的 git 仓库名字          uri: https://gitee.com/lishisen123_admin/springcloud-config.git          # 读取分支#          search-paths:#            - springcloud-config      # 搜索目录#      label: mastereureka:  client:    service-url:      defaultZone: http://eureka7001.com:7001/eureka
  1. 主启动类,需要加注解开启 config @EnableConfigServer
@SpringBootApplication@EnableConfigServerpublic class ConfigCenterMain3344 {
  1. windows 下修改 hosts 文件,增加映射

    从映射的地址可以管理全部的配置信息

image-20211013165118082

  1. 在新建的仓库的下面创建一个 yml 文件,config-dev.yml

    **注意:**这个文件里面的一定要是yml 格式的,不能出现 tab,一定要是 yml 格式的,否则肯定报错

config:  info: config-dev.yml
  1. 启动 7001 注册中心,启动 3344 config 微服务,前面在 hosts 中做了地址映射,浏览器地址栏访问路径:config-3344.com:3344/config-dev.yml 就可以拿到这个在 Gitee 上配置文件的内容

image-20211013213944980

配置读取规则

官网内容:

image-20211013214326558

  • label:分支(branch)
  • name:服务名
  • profiles:环境(dev/test/prod)

至此,从 gitee 获取配置信息的服务搭建完毕

image-20211013214822985

12.3 Config 客户端配置与测试
  1. 新建一个子模块 cloud-config-client-3355
  2. pom.xml
<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-config</artifactId></dependency>
  1. 配置文件

这块需要一个新的配置文件 bootstrap.yml

是什么?

application.yml 是用户级的资源配置项,bootstrap.yml 是系统级的,优先级更加高

Spring Cloud 会创建一个 “Bootstrap Context” ,作为 Spring 应用的 “Application Context” 的父上下文。初始化的时候,“Bootstrap Context” 负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的 “Environment”。

“Bootstrap” 属性有高优先级,默认情况下,它们不会被本地配置覆盖。“Bootstrap context” 和 “Application Context” 有着不同的约定,所以新增了一个 “bootstrap.yml” 文件,保证 “Bootstrap Context” 和 “Applciation Context” 配置的分离。

要将 Client 模块下的 application.yml 文件改为 bootstrap.yml 这是很关键的。

因为 bootstrap.yml 是比 application.yml 先加载的。bootstrap.yml 优先级高于 application.ym

server:  port: 3355spring:  application:    name: config-client  cloud:    #config 客户端配置    config:      # 分支名称      label: master      # 配置文件名称      name: config      # 读取后缀名称 上述 3 个综合:master 分支上 config-dev.yml 的配置文件被读取http://config-3344.com:3344/master/conf      profile: dev      # 配置中心地址      uri: http://localhost:3344/# 服务注册都 eureka 地址eureka:  client:    service-url:      defaultZone: http://eureka7001.com:7001/eureka
  1. 主启动类
@SpringBootApplication@EnableEurekaClientpublic class ConfigMain3355 {
  1. controller
@RestControllerpublic class ConfigClientController {    // 这个值是在 gitee 上的 config-dev.yml 中进行配置过的    @Value("${config.info}")    private String ConfigInfo;    @GetMapping("/configinfo")    public String getConfigInfo() {        return ConfigInfo;    }}
  1. 启动 eureka 注册中心 7001,3344 服务端读取 gitee 上的配置信息,3355 客户端读取配置信息

记录一个错误:由于 2020.* 中禁用了 bootstrap ,所以需要重新引入一下 bootstrap 的依赖

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

启动测试:访问 eureka7001.com:7001 3344 和 3355 两个服务已经注册进入了

访问 localhost:3344/master/config-dev.yml 可以看到从 gitee 上拿到的配置文件信息

访问 localhost:3355/configinfo 可以看到 config.info 的具体内容的值,表示配置文件读取成功

image-20211014093225815

12.4 Config 客户端动态刷新

在上面的代码中是有问题的,分布式配置的动态刷新的问题

对 Gtiee 仓库中的 config-dev.yml 配置文件内容做一些调整

刷新 3344,可以发现 ConfigServer 配置中心立刻会响应,给出更新后的配置信息

但是,3355 ConfigClient 客户端没有任何响应,3355 没有变化除非自己重启或者重新加载,问题就在于这里,不可能每次运维修改一次配置文件,客户端都需要重启一下吧!!!

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

解决方法:

修改 3355 ConfigClient 模块

  1. pom.xml 中引入依赖 actuator
<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-actuator</artifactId></dependency>
  1. 修改 yml,暴漏监控的端口
# 暴露监控端点management:  endpoints:    web:      exposure:        include: "*"
  1. 修改 controller 业务逻辑类,添加刷新的注解 @RefreshScope
@RestController// 具备刷新的能力@RefreshScopepublic class ConfigClientController {
  1. 启动 3355

    先修改 gitee 上面的文件,然后还是 3344 ConfigServer 发生的变化,但是 3355 ConfigClient 并没有发生变化

  2. 修改完 gitee 的配置文件后需要运维人员发送 post请求刷新 3355

在 cmd 中执行命令 curl -X POST "http://localhost:3355/actuator/refresh",然后 3355 就可以得到最新的配置信息了

image-20211014095507769

至此成功实现了客户端 3355 刷新到最新的配置内容,避免了重启服务。

13. SpringCloudBus 消息总线

13.1 简介

Bus 就是对 Config 的加强,作用就是为了广播的自动的刷新。

在上面的 Spirng Cloud Config 中实现客户端的动态刷新,但是还是有一些问题的:

  1. 加入有多个微服务客户端 3355/3366/3377…
  2. 每个微服务都要执行一次 post 请求,手动刷新非常的繁琐
  3. 能不能有一种方式就是广播,一次通知,处处生效,实现大范围的自动刷新

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

**Bus 是什么:**支持两种消息代理:RabbitMQ 和 Kafka

Spring Cloud Bus 是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了 Java 的时间处理机制和消息中间件的功能,Spring Cloud Bus 目前支持 RabbitMQ 和 Kafka

image-20211014101251687

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

image-20211014101543493

  • 什么是总线

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

  • 基本原理

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

13.2 RabbitMQ 环境配置
  1. 安装 Erlang,地址:http://erlang.org/download/
  2. 安装 RabbitMQ,地址:https://www.rabbitmq.com/install-windows.html#downloads

安装 Erlang 和 RabbitMQ 的版本一定找到匹配的,否则就会冲突,在 RabbitMQ 上会有提示用哪个版本的 Erlang。

  1. 两个都安装好之后进入 RabbitMQ 安装目录下的 sbin 目录

image-20211014134121857

  1. 然后在地址栏输入 cmd 调出 dos 创建

    输入命令以启动管理功能:rabbitmq-plugins enable rabbitmq_management,这样也可以添加 rabbitmq 界面

image-20211014134232579

  1. 启动 rabbitmq,然后在浏览器中输入地址 localhost:15672,就可以进行访问了

image-20211014134358600

  1. 出现页面表示安装成功

image-20211014134519979

  1. 输入 Username Password 进行登录

    默认账号密码都是 guest

image-20211014134609760

13.3 Spring Cloud Bus 动态刷新全局广播
13.3.1 构建 Spring Cloud Bus 环境

在此之前需要 RabbitMQ 配置完成

  1. 演示广播效果,增加复杂度,以 3355 为模板创建一个子模块 3366
  2. 里面的内容与 3355 一样,只是需要修改一个端口号为 3366,其他一样,然后启动 注册中心 7001,ConfigServer3344,ConfigClient 3355/3366
  3. 测试 ConfigServer 和 ConfigClient 是否构建成功
  • eureka 中心成功注册

image-20211014135851463

  • ConfigServer 成功得到配置信息

image-20211014135915370

  • ConfigClient 得到相同的信息

image-20211014135945499

  1. 3344 配置中心微服务添加消息总线支持 pom.xml
<!-- 添加消息总线 RabbitMQ 支持--><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-bus-amqp</artifactId></dependency>
  1. 3344 yml 添加 rabbitmq 相关配置和暴漏 bus 刷新配置的端点
  # rabbitmq 相关配置  rabbitmq:    host: localhost    port: 5672    username: guest    password: guest# rabbitmq 相关配置,暴露 bus 刷新配置的端点management:  endpoints:    web:      exposure:        include: 'bus-refresh'
  1. 3355 / 3366 客户端添加总线支持 pom.xml
<!-- 添加消息总线 RabbitMQ 支持--><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-bus-amqp</artifactId></dependency>
  1. 3355 / 3366 yml 中添加 rabbitmq 配置信息
# rabbitmq 相关配置,15672 是web 管理页面的端口;5672 是 MQ 访问的端口rabbitmq:  host: localhost  port: 5672  username: guest  password: guest
  1. 都修改完成并且启动后,在 Gitee 上面进行修改 config-dev.yml 配置文件

修改后在 3344 上得到了实时更新,但是这里有一个错误,没有得到解决,就是在使用命令curl -X POST "http://localhost:3344/actuator/bus-refresh" 的时候会有一个错误,提示不能使用 post 方法,没找到解决的办法,所以这里没有完成动态的刷新全局广播,但是使用命令对 单个 的 3355 3366 可以进行修改,例如:curl -X POST http://localhost:3355/actuator/refresh,3366 一样的命令。

**重点重点:**在准备放弃的时候找到的问题的解决方法

修改 3344 的 yml 配置文件,将 bus-refresh 中间的 - 去掉就可以了,去掉之后就可以刷新配置了,可以在不用重启的情况下对 3355 3366 的配置信息进行刷新,curl -X POST http://localhost:3344/actuator/busrefresh,官网也有相关的信息,是新版的 cloud 换成 busrefresh 这种的。

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

image-20211014170758652

13.3.2 设计思想
  1. 利用消息总线触发一个客户端 /bus/refresh,而刷新所有客户端的配置

image-20211014140625607

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

image-20211014140743831

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

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

假设上面的全局的成功了

定点通知就是想要通知某一个实例生效而不是全部

命令: http://localhost:配置中心的端口号/actuator/bus-refresh/{destination}

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

例如指向通过 config-client 这个微服务上的 3355 刷新配置信息

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

image-20211014171042567

image-20211014165549155

14. SpringCloudStream 消息驱动

14.1 消息驱动概述

image-20211014190701633

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

官网:https://docs.spring.io/spring-cloud-stream/docs/3.1.3/reference/html/

image-20211014172127014

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

应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream 中 binder 对象交互。

通过我们配置来 Binding(绑定),而 Spring Cloud Stream 的binder 对象负责与消息中间件交互。

所以我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。

通过使用 Spring Inegration 来连接消息代理中间件以实现消息事件驱动。

Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区三个核心概念。

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

目前仅支持 RabbitMQ、Kafka。

设计思想:

  • 标准MQ

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

image-20211014173045040

为什么使用 Spring Cloud Stream?

比如说我们用到了 RabbitMQ 和 Kafka,由于这两个消息中间件的架构上的不同,像 RabbitMQ 有 exchange,kafka 有 Topic 和 Partitions 分区

这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一个,后面的业务需求,我们想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推到重新做,因为它跟我们的系统耦合了,这时候 Spring Cloud Stream 给我们提供了一种解耦合的方式。

image-20211014173555816

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

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

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

Binder:

  • input 对应于消费者
  • output 对应于生产者

在没有绑定器这个概念的情况下,我们的 Spring Boot 应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。Stream 对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq 切换为 kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。

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

image-20211014174413203

Stream 中的消息通信方式遵循了发布-订阅模式 ---- Topic 主题进行广播(在 RabbitMQ 就是 Exchange,在 Kakfa 中就是 Topic)

Spring Cloud Stream 标准流程:

image-20211014193214606

image-20211014193528119

image-20211014193221571

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

在这块,又有一些高版本的冲突问题解决不了,干脆就直接使用 Spring Boot 2.2.2.RELEASE 那个项目继续学习使用了

  1. 新建一个模块cloud-stream-rabbitmq-provider8801
  2. pom.xml
<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-stream-rabbit</artifactId></dependency>
  1. yml 配置文件
server:  port: 8801eureka:  client:    register-with-eureka: true    fetch-registry: true    service-url:      defaultZone: http://eureka7001.com:7001/eureka  instance:    # 设置心跳的时间间隔(默认是30秒)    lease-renewal-interval-in-seconds: 2    # 如果现在超过了 5 秒的间隔(默认是90秒)    lease-expiration-duration-in-seconds: 5    # 在信息列表时显示主机名称    instance-id: send-8801.com    # 访问的路径变为 ip 地址    prefer-ip-address: truespring:  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 # 设置要绑定的消息服务的具体设置
  1. 主启动类
老样子,啥也不用加, @SpringBootApplication 有这个注解就行了
  1. 业务逻辑
  • 接口 IMessageProvider
public interface IMessageProvider {    public String send();}
  • 接口实现类 MessageProviderImpl
@EnableBinding(Source.class)    // 定义消息的推送管理,注意这里不是 @Service,因为不是和我们的 Controller 打交道,这会是中间件的逻辑调用@Slf4jpublic class MessageProviderService implements IMessageProvider {    @Resource    private MessageChannel output; // 消息发送管理    @Override    public String send() {        String serial = UUID.randomUUID().toString();        output.send(MessageBuilder.withPayload(serial).build());        log.info("****** serial : " + serial);        return null;    }}
  • controller SendMessageController
@RestControllerpublic class SendMessageController {    @Resource    private IMessageProvider messageProvider;    @GetMapping("/sendMessage")    public String sendMessage() {        return messageProvider.send();    }}
  1. 运行测试

启动我们的服务注册中心 7001,然后再启动 8801

启动 RabbitMQ,进入 web 管理页面 localhost:15672

然后浏览器地址访问 localhost:8801/sendMessage

能够出现下面这种情况表示生产者模块搭建成功

image-20211014215523655

14.2 消息驱动之消费者
  1. 建子模块 cloud-stream-rabbitmq-consumer8802
  2. pom.xml 与上面一样
  3. yml 与上面一样,需要把 output 修改为 input,其他只是需要修改一下服务名字等的信息即可

image-20211015082537332

  1. 启动类
  2. 建立一个 Contrller 用于接受信息
@Component@EnableBinding(Sink.class)	// 绑定public class ReceiveMessageListenerController {    @Value("${server.port}")    private String serverPort;        @StreamListener(Sink.INPUT)	// 监听    public void input(Message<String> message) {        System.out.println("消费者 1 号,---接受到的消息:" + message.getPayload() + "\t   port: " +serverPort);    }}
  1. 运行测试

启动后,两个微服务都是注册到 eureka 的,然后访问地址 localhost:8801/sendMessage (访问 5 次),然后查看执行的结果

image-20211015084054581

image-20211015084134549

14.3 分组消费与持久化
  1. clone 一个 8803 消费者模块,与 8802 一样
  2. 启动:RabbitMQ、7001(服务注册)、8801(消息生产)、8802(消息消费)、8803(消息消费)
  3. 运行后会有两个问题
  • 重复消费问题
  • 消息持久化问题
14.3.1 重复消费问题

当我们浏览器访问地址:localhost:8801/sendMessage 之后,8802、8803 都收到了消息,存在重复消费的问题

在 RabbitMQ 中可以看到这两个服务处于两个组(默认分组 group 是不同的,组流水号不一样,被认为不同组,可以消费)

image-20211015090501238

  • 实际案例

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

image-20211015085707150

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

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

解决重复消费问题:

自定义配置分组,自定义配置分为同一个组

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

  • 配置两个组,8802 、8803 分成不同的组,group两个不同,组名 group1、group2

    只需要在 yml 配置文件中配置 group 顺序,8802 配置 group: group1 8803 配置 group: 8803

image-20211015091214765

启动之后,可以看到,之前默认的流水号组名换成了我们自定义的组名,这里只是改变了一下我们自己的组名,并没有解决重复消费的问题

image-20211015091344662

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

  • 8802 / 8803 都变成相同组,group 两个相同

8802/8803 实现了轮询分组,每次只有一个消费者,8801 模块发的消息只能被 8802 或 8803 其中一个接收到,避免重复消费。

只需要在 配置文件中 group 都改为 group: group1 就可以了

启动运行之后,localhost:8801/sendMessage 发送两次,8802 和 8803 各拿到一条记录(轮询)

同一个组的多个微服务实例,每次只有一个拿到

image-20211015092306074

image-20211015092322877

14.3.2 持久化问题

模拟持久化问题

停掉 8802 和 8803 消费者,然后去掉 8802 分区的信息,保留 8803 分组的信息

在 8802 和 8803 服务停掉的情况下,8801 服务提供者又发送了几条消息到 rabbitmq

然后分别启动 8802 和 8802

  • 启动 8802

因为没有了分组属性的配置,所以在启动后不会去 rabbitmq 中拿到自己停止服务后 rabbitmq 的消息,这就造成了 消息错过

  • 启动 8803

有分组属性配置,所以启动后在控制台可以看到从 MQ 中拿到的要消费的消息

image-20211015093204406

15. SpringCloudSleuth 分布式请求链路跟踪

15.1 概述

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

image-20211015093516482

  • Spring Cloud Sleuth 提供了一套完整的服务跟踪的解决方案,在分布式系统中提供追踪解决方案并且兼容支持了 zipkin

image-20211015093925464

15.2 搭建链路监控

简单的说,sleuth 负责收集整理,zipkin 负责展示

15.2.1 搭建 zipkin 平台

下载地址:https://github.com/openzipkin/zipkin/

将jar 包下载到本地之后,使用 cmd 运行命令 java -jar 文件

image-20211015100545737

浏览器访问地址:http://localhost:9411/zipkin 进入 web 页面

image-20211015101616286

  • 完整的调用链路

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

image-20211015101745571

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

image-20211015101939739

整个链路的依赖关系:

image-20211015102001969

  • Trace:类似于树结构的 Span 集合,表示一条调用链路,存在唯一表示
  • Span:表示调用链路来源,通俗的理解 span 就是一次请求信息
15.2.2 环境测试

在这里就不再创建新的模块了,使用之前的 80 和 8001 模块,一个服务消费者一个服务提供者

在两个服务里面加入依赖:

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

在两个服务里面加入 yml 配置:

spring:  application:    name: cloudl-consumer-hystrix-order  # 下面是 sleuth 的监控功能的配置信息  zipkin:    base-url: http://localhost:9411  sleuth:    sampler:      # 采样值介于 0 到 1 之间,1 则表示全部采集      probability: 1

然后启动运行,访问之前写过的调用接口的路径进行测试,然后再 Zipkin web 页面就可以看到详细的调用链路信息了。

image-20211015104732323

image-20211015104745321

16. SpringCloudAlibaba 入门简介

  • 为什么会出现 SpringCloudAlibaba

SpringCloudNetflix 项目进入维护模式

什么是维护模式?

将模块置于维护模式,意味着 Spring Cloud 团队将不会再向模块添加新功能。我们将修复 block 级别的 bug 以及安全问题,我们也会考虑并审查社区的小型 pull request。

将继续支持这些模块,知道 Greenwich 版本被普遍采用至少一年。

进入维护模式意味着 Spring Cloud Netflix 将不再开发新的组件。

  • SpringCloud Alibaba 带来了什么

官网:https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/en-us/index.html

诞生:2018.10.31,Spring Cloud Alibab 正式入驻了 Spring Cloud 官方孵化器,并在 Maven 中央库发布了第一个版本。

Spring Cloud Alibab 是由一些 阿里巴巴的开源组件和云产品组成的。这个项目的目的是为了让大家所熟知的 Spring 框架,其优秀的设计模式和抽象概念,以给使用阿里巴巴产品的 Java 开发者带来使用 Spring Boot 和 Spring Cloud 的更多便利。

  • 可以做什么?

服务限流降级:默认支持 Servlet、Feign、RestTemplate、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。

服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持

分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新

消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力

阿里云对象存储:阿里云提供海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。

分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。

  • github 地址(中文):https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

  • 组件内容

  1. Sentinel:阿里巴巴开源产品,把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
  2. Nacos:阿里巴巴开源产品,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  3. RocketMQ:Apache RocketMQ 基于 Java 的高性能、高吞吐量的分布式消息和流计算平台
  4. Dubbo:Apache Dubbo 是一款高性能 Java RPC 框架
  5. Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案
  6. Alibaba Cloud OSS:阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。
  7. Alibababa Cloud ScheduleX:阿里云中间件团队开发的一款分布式任务调度产品,支持周期性的任务与固定时间点触发任务。

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务

依托 Spring Cloud Ablibaba,只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

17. SpringCloudAlibaba Nacos 服务注册和配置中心

17.1 Nacos 简介

为什么叫 Nacos:前 4 个字母分别为 Naming 和 Configuratioin 的前两个字母,最后的 s 为 Service。

是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台, Nacos 就是注册中心 + 配置中心的组合。Nacos = Eureka + Config + Bus

替代 Eureka 做服务注册中心;替代 Config 做服务配置中心。

下载地址:https://github.com/alibaba/Nacos — https://nacos.io/zh-cn/

17.2 安装并运行 Nacos

官网下载到本地:https://github.com/alibaba/nacos/releases/tag/1.4.2

下载完成之后加压缩到本地

image-20211015132837317

进入 bin 文件夹下,双击 startup.cmd 启动

image-20211015133743484

确认是否启动成功,看到下图表示成功启动,默认的账号密码都是 nacos

image-20211015133852735

17.3 Nacos 作为服务注册中心

官网地址:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_nacos_discovery

17.3.1 Nacos 的服务提供者
  1. 建立子模块 cloudalibaba-provider-payment9001
  2. 加入依赖,与之前的依赖差不多,web、actuator、test
<!--Spring Cloud Alibaba Nacos--><dependency>    <groupId>com.alibaba.cloud</groupId>    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
  1. yml 配置
server:  port: 9001spring:  application:    name: nacos-payment-provider  cloud:    nacos:      discovery:        # Nacos address        server-addr: localhost:8848management:  endpoints:    web:      exposure:        include: '*'
  1. 主启动类
@SpringBootApplication@EnableDiscoveryClient	// 开启服务发现public class NacosProviderMain9001 {
  1. Controller 模拟业务逻辑
@RestControllerpublic class PaymentController {    @Value("${server.port}")    private String serverPort;    @GetMapping("/payment/nacos/{id}")    public String getServerPort(@PathVariable("id") Integer id) {        return "nacos register, serverPort: " + serverPort + "\t id: " + id;    }}
  1. 启动 Nacos、9001 服务

    启动后我们就可以在 Nacos 的服务列表看到我们的提供者服务已经注册成功了

image-20211015140835918

为了实现负载均衡的效果,创建另一个模块 cloudalibaba-payment-provider9002 与 9001 的区别就是 yml 配置文件中的 server.port=9002

17.3.2 Nacos 的服务消费者

Necos 默认是支持负载均衡的,因为它整合了 ribbon ,整合了 ribbon 就可以用 RestTemplate 实现 LoadBalanced

image-20211015155212481

  1. 建立一个子模块 cloudalibaba-consumer-nacos-order83
  2. pom.xml 与上面一样
  3. yml 与上面一样,修改一个端口即可
# 消费者将要去访问的微服务名称(注册到 nacos 的微服务提供者)# 这里是为了方便在 Controller 里面调用服务的时候不用写 final String 的一个字符串service-url:  nacos-user-service: http://nacos-payment-provider
  1. 因为用到了负载均衡 RestTemplate
@Configurationpublic class ApplicationContext {    @Bean    @LoadBalanced    public RestTemplate getRestTemplate() {        return new RestTemplate();    }}
  1. controller 进行服务提供者的微服务
@RestControllerpublic class OrderController {    @Resource    private RestTemplate restTemplate;    @Value("${service-url.nacos-user-service}")    private String serverURL;    // 因为在 yml 配置文件中进行了配置,所以在这里不用定义这个服务名称的常量    //public static final String SERVER_URL = "http://nacos-payment-provider";    @GetMapping("/consumer/payment/nacos/{id}")    public String getServerUrl(@PathVariable("id") Integer id) {        return restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class);    }}
  1. 主启动类,与上面一样

  2. 启动测试,访问地址 :http://localhost:83/consuemr/payment/nacos/1

    然后得到结果就是 9001 9002 9001 9002 的效果了,实现了服务调用的轮询

image-20211015161051741

17.4 Nacos 支持 AP 和 CP 模式的切换

C 是所有节点在同一时间看到的数据是一致的;A 的定义是所有的请求都会收到响应。

什么时候选择什么模式?

一般来说,如果不需要存储服务级别的信息且服务实例是通过 nacos-client 注册,并能够保持心跳上报,那么就可以选择 AP 模式,当前主流的服务如:Spring Cloud 和 Dubbo 服务,都适用于 AP 模式,AP 模式为了服务的可能性而减弱了一致性,因此 AP 模式下只支持注册临时实例。

如果需要在服务级别编辑或者存储配置信息,那么 CP 是必须,K8S 服务和 DNS 服务则适用于 CP 模式。

CP 模式下则支持注册持久化实例,此时则是 Raft 协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。

可以使用这个命令来进行模式的切换:curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&Value=CP'

image-20211015162737143

17.4 Nacos 作为服务配置中心
17.4.1 Nacos 作为配置中心的基础配置
  1. 建立子模块 cloudalibaba-config-nacos-client3377

  2. pom.xml

    一般使用 Nacos 这两个依赖都需要

<!--Spring Cloud Alibaba Config--><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--Spring Cloud Alibaba Nacos--><dependency>    <groupId>com.alibaba.cloud</groupId>    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
  1. yml 配置文件,需要两个:application.yml bootstrap.yml

Nacos 同 Spring Cloud Config 一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。Spring Boot 中配置文件的加载是存在优先级顺序的,bootstrap 优先级高于 application

  • application.yml
spring:  profiles:    # 表示要配置的开发环境(dev or test or prod)    active: dev
  • bootstrap.yml
server:  port: 3377spring:  application:    name: nacos-config-client  cloud:    nacos:      discovery:        # Nacos 服务注册中心地址        server-addr: localhost:8848      config:        # Nacos 作为配置中心地址        server-addr: localhost:8848        # 指定 yaml 格式的配置        file-extension: yaml
  1. 主启动类
@SpringBootApplication@EnableDiscoveryClientpublic class ConfigNacosMain3377 {
  1. 业务逻辑
@RestController@RefreshScope // 支持 Nacos 的动态刷新功能public class ConfigClientController {    @Value("${config.info}")    private String configInfo;    @GetMapping("/config/info")    public String getConfigInfo() {        return configInfo;    }}
  1. 在 Nacos 中添加配置文件信息

    官网信息:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html

    Nacos 中的匹配规则:Nacos 中的 dataid 的组成格式与Spring Boot 配置文件中的匹配规则

    完整的匹配规则,dataI的 在 Nocas web 中的 配置管理 – 配置列表 可以看到 dataId 这个选项;

image-20211015165051902

  • 以上面的 3377 服务为例,我们需要创建的配置文件名是

    nacos-config-client-dev.yml s p r i n g . a p p l i c a t i o n . n a m e − {spring.application.name}- spring.application.name{spring.profiles.active}.${file-extension}

在 Nacos 的 Web 页面中新建配置信息

image-20211015165735557

  1. 测试

启动前需要在 nacos 客户端-配置管理-配置管理栏目下有对应的 yaml 配置文件

启动 cloud-config-nacos-client3377 的主启动类

调用接口查看配置信息:http://localhost:3377/config/info

得到正确的运行结果

image-20211015194859195

Nacos 作为配置中心自带动态刷新功能,修改一下 Nacos 中的yaml 配置文件,再次调用查看配置的接口,会发现配置已经进行了刷新。

17.4.2 Nacos 作为配置中心的分类配置

**问题:**多环境多项目管理

  • 问题 1:

    实际开发中,通常一个系统会准备 dev test prod,如何保证指定环境启动时服务能正确读取 Nacos 上相应的环境的配置文件呢?

  • 问题 2:

    一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…

    那怎么对这些微服务的配置进行管理呢?

Nacos 的图形化管理界面:

image-20211015200245430

这个图形化界面的设计是 NameSpace + Group + Data Id 三者构建,三者关系?为什么这么设计?

  1. 是什么?

    类似 Java 里面的 package 名和类名

    最外层的 namespace 是可以用于区分部署环境的,Group 和 DataID 逻辑上区分两个目标对象。

  2. 三者情况

image-20211015200557854

默认情况:Namespace=public,Group=DEFAULT_GROUP,默认 Cluster 是 DEFAULT

Nacos 默认的命名空间是 public,Namespace 主要用来实现隔离。

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

Group 默认是 DEFAULT_GROUP,Group 可以把不同的微服务划分到同一个分组里面去。

Service 就是微服务:要给 Service 可以包含多个 Cluster(集群),Nacos 默认是 Cluster 是DEFAULT,Cluster 是对指定微服务的一个虚拟划分。比如说为了容灾,将 Service 微服务分别部署在了杭州机房和广州机房,这是就可以给杭州机房的 Service 微服务起一个集群名称(HZ),给广州机房的 Service 微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。

最后是 Instance,就是微服务的实例。

三种方案加载配置 ======================================================

1. DataID 方案

指定 spring.profile.active 和配置文件的 DataID 来使不同环境下读取不同的配置

默认空间 + 默认分组 + 新建 dev 和 test 两个 DataID

image-20211015202117424

通过 spring.profile.active 属性就能进行多环境下配置文件的读取

spring:  profiles:    # 表示要配置的开发环境(dev or test or prod)    # 这里配置什么就加载什么    active: test

image-20211015202300996

image-20211015202350874

2. Group方案

通过 Group 实现环境区分,创建配置文件的时候如没有进行过修改就是默认的 DEFAULT_GROUP

image-20211015202601395

新建配置的时候,我们可以在这里进行修改 GROUP 名字

image-20211015202638347

在 nacos 图形界面控制台上新建两个配置文件,DataID 为 nacos-config-client-info.yml,group 分别为 DEV-GROUP 和 TEST-GROUP

image-20211015203106625

配置配置文件:

需要修改 bootstrap.yml 和 application.yml 才能

在 config 下增加一条 group 的配置即可。可配置为 DEV_GROUP 或 TEST_GROUP

image-20211015203424895

重启 3377 服务,然后可以看到结果修改为了测试组的配置文件的内容

image-20211015203537386

3. Namespace方案

新建 dev/test 的 Namespace

image-20211015204830373

回到服务管理 – 服务列表查看

image-20211015204859572

在 配置管理 — 配置列表 中的 dev 命名空间中创建三个配置文件

按照域名配置填写

image-20211015210217364

在配置文件中指定 namespace,Group,文件名 来确定使用哪一个配置文件

image-20211015210457212

image-20211015210628806

17.5 Nacos 集群和持久化配置
17.5.1 概述

官网:https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3KndXHXk-1634979642374)(D:\notes\SpringCloud\SpringCloud.assets\image-20211016085153060.png)]

image-20211016085339005

默认 Nacos 使用嵌入式数据库实现数据的存储,所以,如果启动多个默认配置下的 Nacos 节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos 采用了集中式存储的方式来支持集群化部署,目前只支持 MySQL 的存储

17.5.2 Nacos 集群化配置

当我们注册服务到 Nacos 之后,然后关机再开机后会发现 Nacos 上还是有注册的服务信息的。

Nacos 默认自带的是嵌入式数据库 derby。https://github.com/alibaba/nacos/blob/develop/pom.xml

image-20211016091239754

derby 切换到 mysql:

  • Windows 情况下
  1. 找到 nacos 解压的目录下的conf 文件夹下找到 sql 脚本nacos-mysql.sql,然后复制到 mysql 中执行,会有一个数据库nacos_config
  2. 然后找到找到 application.properties 文件夹,打开进行修改,添加如下内容,这些内容官网都有介绍
spring.datasource.platform=mysqldb.num=1db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=truedb.user=rootdb.password=root
  1. 配置好上面之后重启 Nacos,就可以看到一个全新的空记录界面了,以前是记录到 derby (Nacos自带数据)

测试是否成功,在 Nacos 的控制台的 配置管理–配置列表中新建一个配置文件,然后查看数据库中的内容

image-20211016110447068

17.5.3 Linux 版 Nacos + MySQL 生产环境配置

需要:1个 Nginx + 3 个 Nacos 注册中心 + 1 个 mysql

在 Linux 虚拟中安装 Nacos

下载地址:https://github.com/alibaba/nacos/releases/tag/1.4.2

image-20211016110909662

下载好之后上传到 Linux 中的 /opt 目录下,执行命令 tar -zxvf nacos-server-1.4.2.tar.gz 进行安装。

安装完成之后在 /opt/nacos/bin 目录下执行命令 ./startup.sh 可以启动,然后通过浏览器访问端口 192.168.253.141:8848 可以 看到单机版的 nacos

集群配置步骤

  1. Linux 服务器上 mysql 数据库配置

安装过程地址:http://lss-coding.top/2021/10/16/Linux/Linux%20%E4%B8%8B%E5%AE%89%E8%A3%85%20MySQL/

安装完成之后,mysql -uroot -proot 启动 进入 mysql

然后执行命令:source /mynacos/cnof/nacos-mysql.sql 执行sql 脚本。

image-20211016164225645

  1. application.properties 配置

进入 Nacos 安装目录/conf,修改前先进行文件的备份:cp application.properties application.properties.bkvim application.properties 文件,配置 mysql 的数据库信息

image-20211016164651447

  1. Linux 服务器上 nacos 的集群配置 cluster.conf

修改 cluster.conf 配置集群

首先使用 hostname -i 命令查看本机 ip

然后在 cluster.conf 文件中使用这一个 ip 配置 三个端口号,这个 IP不能写成 127.0.0.1,必须是 hostname -i 命令识别的 ip

image-20211018163900203

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

cd /mynacos/bin 目录下有 startup.sh ,首先对要修改的文件进行一个备份 cp startup.sh starup.sh.bk 上面需要修改的也都要进行一下备份

这个内容之前是没有的,需要自己添加,一定要小心

image-20211018200414722

还需要修改的一下文件的最下面的内容,这个也是自己添加的

image-20211016170416031

启动:

平常的时候单机启动 ./startup.sh 就可以了。

但是 集群启动,我们希望可以类似其它软件的 shell 命令(传递不同的端口号启动不同的 nacos 实例)命令:./startup.sh -p 3333 表示启动端口号为 3333 的nacos 服务器实例

image-20211016170528452

  1. Nginx 的配置,由他作为负载均衡器

    Nginx 的安装与配置参考:http://lss-coding.top/2021/10/18/Nginx/Nginx%20%E7%9F%A5%E8%AF%86%E7%82%B9%E6%80%BB%E7%BB%93/

  • 修改 Nginx 的配置文件

    修改 /usr/local/nginx/conf 目录下的 nginx.conf 配置文件

  • nginx.conf 修改内容,配置一个集群代理

image-20211018170920702

  1. 至此,1 个 Nginx + 3 个 nacos 注册中心 + 1 个 mysql 搭建完成

启动 Nginx:

​ 进入目录 cd /usr/local/nginx/sbin ,执行命令:./nginx

启动 3 台 nacos:

​ 进入目录 cd /mynacos/bin,执行命令:./startup.sh -p 3333./startup.sh -p 4444./startup.sh -p 5555

使用命令查看启动 nacos 的个数:ps -ef |grep nacos|grep -v grep |wc -l

image-20211018201033114

  • 在 windows 浏览器访问 192.168.253.141:1111/nacos 进行测试

image-20211018201229655

  • 在 配置列表 — 配置管理中添加一个配置文件,然后在 Linux 系统中查看 mysql 中的数据库文件

image-20211018201624912

  • 在 idea 中的 cloudalibaba-provider-payment9002 支付微服务修改yml 配置文件注册到 Linux 系统的 Nacos 服务注册中心

image-20211018202431206

image-20211018202459461

image-20211018202623681

18. SpringCloudAlibaba Sentinel 实现熔断与限流

18.1 Sentinel 是什么?

官网文档:https://github.com/alibaba/Sentinel

中文文档:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D

轻量级的流量控制、熔断降级 Java 库,就是之前的 Hystrix。

Hystrix

之前使用 Netflix 的 Hystrix 的时候,需要我们程序员自己手动搭建监控平台,没有一套 web 界面可以给我们进行更加细粒度化的配置流控、速率控制、服务熔断、服务降级。。。。。。

Sentinel

单独一个组件可以独立出来,支持界面化的细粒度统一配置。

Sentinel 主要特征:

image-20211018213041114

也就是能做:服务雪崩、服务降级、服务熔断、服务限流

18.2 安装 Sentinel 的控制台

下载地址:https://github.com/alibaba/Sentinel/releases

**Sentinel 由两部分构成:**后台、前台8080

Sentinel 分为两个部分:

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

运行:

前提:java 8 环境,8080 端口不能被占用

命令:java -jar sentinel-dashboard-1.8.0.jar

默认登录账号和密码:sentinel

image-20211018213944126

image-20211018214057153

18.3 初始化演示工程
  1. 启动 Nacos 8848 服务注册中心

image-20211019082846423

  1. 创建微服务 cloudalibaba-sentinel-service8401
  • pom.xml
<!--Spring Cloud Alibaba Nacos--><dependency>    <groupId>com.alibaba.cloud</groupId>    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>    <version>2.2.0.RELEASE</version></dependency><!--Spirng Cloud Alibaba sentinel-datasource-nacos 后续做持久化用到--><dependency>    <groupId>com.alibaba.csp</groupId>    <artifactId>sentinel-datasource-nacos</artifactId></dependency><!--Spring Cloud Alibaba Sentinel--><dependency>    <groupId>com.alibaba.cloud</groupId>    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>
  • yml
server:  port: 8401spring:  application:    name: cloud-sentinel-service  cloud:    nacos:      discovery:        server-addr: localhost:8848    sentinel:      transport:        # 配置 Sentinel dashboard 地址        dashboard: localhost:8080        # 默认端口号 8719 端口,假如被占用会自动从 8719 开始依次 +1 扫描,直到找到未被占用的端口        port: 8719management:  endpoints:    web:      exposure:        include: '*'
  • 启动类
@EnableDiscoveryClient@SpringBootApplicationpublic class SentinelMain8401 {
  • 编写一个业务类 Controller
@RestControllerpublic class FlowLimitController {    @GetMapping("/testA")    public String testA() {        return "-------testA";    }    @GetMapping("/testB")    public String testB() {        return "-------testB";    }    }
  1. 启动 Sentinel 8080

    java -jar sentinel.jar

image-20211019083320660

  1. 启动微服务 8401

启动 8401 服务,然后我们发现,Sentinel 的控制台并没有对服务进行监控。

因为 Sentinel 采用的是懒加载机制,需要执行一次接口访问localhost:8401/testA

  1. 启动 8401 微服务后查看 Sentinel 控制台

    访问接口之后可以看到有一个流量的监控的波动

image-20211019083832590

18.4 流量控制规则
18.4.1 基本介绍

image-20211019084227766

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

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

  • 阈值类型/单机阈值

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

  • 流控模式:

    • 直接:api 达到限流条件时,直接限流
    • 关联:当关联的资源达到阈值时,就限流自己
    • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)(api 级别的针对来源)
  • 流控效果:

    • 快速失败:直接失败,抛异常
    • Warm Up:根据 codeFacor(冷加载因子,默认 3)的值,从阈值/codeFactor,经过预热时长,才达到设置的 QPS 阈值
    • 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为 QPS,否则无效。
18.4.2 流控模式
  • 直接(默认)

    下图中,表示 1 秒种内查询 1 次就是 OK,若超过次数 1,就直接 - 快速报错,报默认错误。

image-20211019085452925

image-20211019085559141

**QPS:**请求还没有进到服务里面就被限流了。

**线程数:**假设服务中只有一个线程工作,那么一次只能进入一个请求进行操作,其他的等待或者失败。

image-20211019090531706

  • 关联

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

当与 A 关联的资源 B 达到阈值后,就限流 A 自己。(比如支付接口限流以后就限制下订单的接口)

当关联资源 /testB 的 QPS 阈值超过 1 时,就限流 /testA 的 Rest 访问地址,当关联资源到阈值后限制配置好的资源名

image-20211019091703053

使用工具(postman 模拟并发密集的访问 /testB)

测试访问 /testA 没有问题

image-20211019093201568

将地址放到一个访问 /testB 的请求放到 Collection 中,然后 Run 这个集合

image-20211019093326269

因为有关联,所以当我们访问 /testA 的时候也被限流了

image-20211019093428469

等到访问 /testB 的 20 个请求结束在访问 /testA 又能正常访问了。

  • 链路

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

18.4.3 流控效果
  • 快速失败

    直接失败,抛出异常

image-20211019094154171

  • Warm Up(预热)

说明:公式:阈值除以coldFactor(默认值为 3),经过预热时长后才会达到阈值

默认 codeFactor(冷加载因子)为 3,即请求 QPS 从 threshod / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。

image-20211019095351548

官网:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6

限流-冷启动官网:https://github.com/alibaba/Sentinel/wiki/%E9%99%90%E6%B5%81—%E5%86%B7%E5%90%AF%E5%8A%A8

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

image-20211019095422916

image-20211019095715570

案例配置

默认 coldFactor 为 3,即请求 QPS 从(threshold / 3) 开始,经多少预热时长才逐渐升至设定的 QPS 阈值。

阈值为 10+ 预热时长设置为 5 秒。

系统初始化的阈值为 10 / 3 约等于 3,即阈值刚开始为 3;然后过了 5 秒后阈值才慢慢升高恢复到 10

然后我们访问接口测试,在前 5 秒内,如果一秒请求超过 3 次就会抛出错误异常,过 5 秒后,如果请求超过 10 才会出错。

image-20211019100246075

主要应用场景:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可以慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。

  • 排队等待

假设同时有 100 个请求进来了,但是处理的时候只有一个服务进行处理,所以当请求进来之后需要排队进行等待,如果愿意等就等,不愿意等就超时重试。

image-20211019100827311

匀速排队,让请求以均匀的速度通过,阈值类型必须设成 QPS,否则无效。

设置含义:/testA 每秒 1 次请求,超过的话就排队等待,等待的超时时间为 20000 毫秒。

image-20211019100737705

使用 postman 模拟发送大量请求,

访问的时候是 20 个线程,间隔 0.1 发送一次请求

image-20211019101609642

Sentinel 处理的时候是排队等待的方式,一秒钟处理一个请求。

image-20211019101446545

18.5 降级规则
18.5.1 介绍

官网:https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7

image-20211019101859222

image-20211019110734458

RP(平均响应时间,秒级)

​ 平均响应时间 超出阈值 且 在时间窗口内通过的请求 >= 5,两个条件同时满足后触发降级 窗口期过后关闭断路器

​ RT 最大 4900(更大的需要通过 -Dcsp.sentinel.statistic.max.rt=xxxx 才能生效)

异常比例(秒级)

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

Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误。

当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

Sentinel 的断路器是没有半开状态的,要么就是保险丝合上通电、要么就是不通电。

18.5.2 降级策略
  • RT

平均响应时间:当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。

image-20211019103201573

测试,Controller 代码

@GetMapping("/testD")public String testD() throws InterruptedException {    Thread.sleep(1000);    log.info("testD 测试 RT");    return "------testD";}

image-20211019103618789

使用 jmeter 发送不间断发送请求,然后用浏览器访问 http://localhost:8401/testD 会出现异常

image-20211019104357215

image-20211019104436418

在上述配置中,永远一秒钟打进来 10 个线程(大于 5 个了)调用 testD,希望 200 毫秒处理完本次任务,如果超过 200 毫秒还没有处理完,在 未来 1 秒的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸端点了。

后续停止掉 jmeter,没有这么大的访问量了,断路器关闭(保险丝恢复),微服务可以正常访问 OK。

  • 异常比例

image-20211019105553252

当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,即在接下来的时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围[0.0,1.0],代表 0% ~ 100%

image-20211019105814251

测试,Controller 代码中给出一个异常

@GetMapping("/testD")public String testD() throws InterruptedException {    log.info("testD 异常比例");    int age = 1 / 0;    return "------testD";}

image-20211019110101409

使用 jmeter 访问测试,因为代码中的异常是每个访问都会有的,100%,所以会出现服务降级,抛出异常信息。

按照上面的配置

单独访问一次,必然来一次报错一次(int age = 1 / 10),调用一次错一次。

开启 jmeter 后,直接高并发发送请求,多次调用达到我们的配置条件了。断路器开启(保险丝跳闸),微服务不可用了,不再报错 error 而是服务降价了。

  • 异常数

image-20211019110757355

当资源近 1 分钟的异常数目超过阈值之后会进行熔断,注意由于统计时间窗口是分钟级别的,若 timeWindow小于60s,则结束熔断状态后仍可能再进入熔断状态。时间窗口一定要大于等于60s

image-20211019110953010

image-20211019111124933

http://localhost:8401/testE,第一次访问绝对错误,因为 int age = 1/0,看到 error 窗口,但是达到 5 次报错后,进入熔断降级。

18.6 热点 key 限流
18.6.1 介绍

官网:https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81

image-20211020194430236

image-20211020194315935

前面使用的

  • 兜底方法

分为系统默认和客户自定义两种,之前的 case,限流出现问题后,都是用 Sentinel 系统默认的提示:Blocked by Sentinel(flow limiting)

我们可以自定义,类似 Hystrix,某个方法出现问题了,就找对应的兜底降级方法,从 @HystrixCommand 到 @SentinelResource

18.6.2 配置热点 Key
  • Controller 方法模拟
@GetMapping("/testHotKey")// 如果违背了 sentinel 的限流key 的规则,就会进入兜底方法,如果不设置 blockHandler 兜底方法进入 Error Page 错误页面@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")public String testHotKey(@RequestParam(value = "p1", required = false) String p1        , @RequestParam(value = "p2", required = false) String p2) {    return "------testHotKey";}// 兜底的方法public String deal_testHotKey(String p1, String p2, BlockException exception) {    return "------deal_testHotKey";}

测试访问效果

image-20211020202014624

  • 配置

配置信息中设置好之后,当我们每秒访问 1 次 localhost:8401/testHotKey 没有任何的问题,但是当我们 1 秒访问超过了 QPS 规定的值就会出现错误的信息,跳转到我们的兜底的方法;参数索引,当我们的参数列表中下标为 0 的时候就会有这个规则的限制,如果没有就不会有这个 key 限制了。

方法 testHotKey 里面第一个参数只要 QPS 超过每秒 1 次,马上降级处理,进入我们自己的兜底方法。

image-20211020202450314

image-20211020202628815

18.6.3 参数例外项

image-20211020203724901

上面的案例中,第一个参数 p1,当 QPS 超过 1 秒 1 次点击之后会马上降级限流。

**特例情况:**我们希望 p1 参数当它是某个特殊值的时候,它的限流和平时不一样,假如当 p1 的值等于 5 的时候,它的阈值可以达到 200

**配置:**平时的第一个参数的访问就是超过 QPS 规定的 1 秒 1 次就会进入限流,但是设置了参数例外项,参数类型是 String (Controller 中接收的就是 String)参数值为 5 的时候限流阈值为 200

  • 当 p1 = 5 的时候,阈值就是 200
  • 当 p1 = 其他的值的时候,阈值就是 1

image-20211020203936943

注意:

在 Controller 中有一句异常的代码 int age = 1 / 0; 这个异常 Sentinel 不会处理的

  • @SentinelResource 处理的是 Sentinel 控制台配置的违规情况,有 blockHandler 方法配置的兜底处理,主管配置出错,运行时出错该走异常走异常
  • @RuntimeException 这个是 java 运行时报出的运行时异常 RunTimeException,@SentinelResource 不管
18.7 系统规则

官网:https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81

Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

  • Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5
  • CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
  • 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

image-20211020205125184

配置全局 QPS:

设置阈值为 1

系统中有两个接口,testA ,testB

当设置了入口 QPS 之后,系统访问 testA 或者 testB 超过阈值数都会进行限流

18.8 @SentinelResource
18.8.1 按资源名称限流+后续处理

修改子模块 cloudalibaba-sentinel-service8401

  • 修改 pom.xml
<!--引入自定义的 api 通用包,可以使用 Payment 支付 Entity--><dependency>    <groupId>org.lss</groupId>    <artifactId>cloud-api-commons</artifactId>    <version>1.0-SNAPSHOT</version></dependency>
  • 创建一个 RateLimitController 类
@RestControllerpublic class RateLimitController {    @GetMapping("/byResource")    @SentinelResource(value = "byResource", blockHandler = "handlerException")    public CommonResult byResource() {        return new CommonResult(200, "按资源名称限流测试 OK", new Payment(2020L, "serial001"));    }    public CommonResult handlerException(BlockException exception) {        return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用");    }}
  • 根据资源名称 byResource 配置流量控制,QPS 为 1

    一秒访问一次,如果超过一次就会走 blockHandler 自定义的方法限流

image-20211020212056288

当我们关掉 8401 的服务之后,流控规则就会消失,是临时的

18.8.2 按照 URL 地址限流+后续处理

通过访问的 URL 来限流,会返回 Sentinel 自带默认的限流处理信息。就是在上面的操作中,我们自己写了兜底方法,如果没有写就用系统默认的方法。

@GetMapping("/rateLimit/byUrl")// 这块没有写 blockHandler 参数@SentinelResource(value = "byUrl")public CommonResult byUrl() {    return new CommonResult(200, "按URL限流测试 OK", new Payment(2020L, "serial001"));}	

image-20211020213651583

配置好之后访问 localhost:8401/rateLimit/byUrl 就会出现系统默认的限流提示(这个接口没有配置 blockHandler)

image-20211020213847296

18.8.3 上面兜底方法的问题

与 Hystrix 一样

  • 系统默认的,没有体现我们自己的业务要求
  • 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观
  • 每个业务方法都添加一个兜底的,代码膨胀
  • 全局统一的处理方法没有体现
18.8.4 客户自定义限流处理逻辑
  • 创建 CustomerBlockHandler 类用于自定义限流处理逻辑
public class CustomerBlockHandler {    public static CommonResult handlerException1(BlockException exception) {        return new CommonResult(4444, "按照客户自定义的,global----1");    }    public static CommonResult handlerException2(BlockException exception) {        return new CommonResult(4444, "按照客户自定义的,global----1");    }}
  • RateLimitController 中方法绑定自定义的限流类
@GetMapping("/rateLimit/customerBlockHandler")// 这块就是当我们的方法出现问题限流,就去找  blockHandlerClass = CustomerBlockHandler.class  类下的  blockHandler = "handlerException"   这个方法进行兜底处理@SentinelResource(value = "customerBlockHandler",        blockHandlerClass = CustomerBlockHandler.class,        blockHandler = "handlerException1")public CommonResult customerBlockHandler() {    return new CommonResult(200, "按客户自定义", new Payment(2020L, "serial003"));}
  • 启动后测试

    上面绑定的是 1 这个方法,blockHandler = “handlerException1” 进行处理

image-20211021085050436

18.8.5 更多注解属性说明

官网:https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81

image-20211021090146546

18.9 服务熔断功能
18.9.1 负载均衡环境搭建

因为 cloud 的版本较高,不支持 Netflix 公司的 Ribbon 了,不耽误使用,所以没有回退版本,使用 LoadBalance

  1. 创建两个子模块 cloudalibaba-provider-payment9003 / 9004

  2. pom.xml

    与之前一样, Nacos

  3. yml 配置,只有端口号不一样,其他的注册到 Nacos,暴露接口都一样

  4. 主启动类

  5. 构建一个 Controller 测试类,就是简单的调用方法返回一个字符串

@RestControllerpublic class PaymentController {    @Value("${server.port}")    private String serverPort;    public static HashMap<Long, Payment> hashMap = new HashMap<>();    static {        hashMap.put(1L,new Payment(1L,"4e07d0e1-1633-42d1-98df-671b040fbc96"));        hashMap.put(2L,new Payment(2L,"afe21c52-4420-4fc0-beb7-0368721d81fa"));        hashMap.put(3L,new Payment(3L,"68d970d9-adf4-4e11-95d4-005406dbdca2"));    }    @GetMapping("/paymentSQL/{id}")    public CommonResult paymentSQL(@PathVariable("id") Long id) {        Payment payment = hashMap.get(id);        return new CommonResult(200, "data from mysql, serverPort: " + serverPort, payment);    }}
  1. 测试

image-20211021093448768

  1. 构建一个子模块 cloudalibaba-consumer-nacos-order84
  2. pom.xml,引入依赖实现负载均衡
<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-loadbalancer</artifactId>    <version>3.0.4</version>    <scope>compile</scope>    <optional>true</optional></dependency>
  1. 配置 RestTemplate
@Configurationpublic class ApplicationContextConfig {    @Bean    @LoadBalanced    public RestTemplate getRestTemplate() {        return new RestTemplate();    }}
  1. 业务类
@RestControllerpublic class CircleBreakerController {    @Resource    private RestTemplate restTemplate;    @Value("${service-url.nacos-user-service}")    private String SERVICE_URL;    @GetMapping("/consumer/fallback/{id}")    @SentinelResource(value = "fallback")    public CommonResult<Payment> fallback(@PathVariable("id") Long id) {        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);        if (id == 4) {            throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常......");        }else if (result.getData() == null) {            throw new NullPointerException("NullPointerException, 改 id 没有对应记录,空指针异常");        }        return result;    }}
  1. 测试

    这里没有设置 fallback 的兜底方法,所以访问时候的 id 如果超过 > 3 就会报异常 Error Page

    小于 3 可以正常实现轮询访问,9003 9004 9003 9004 …

image-20211021102230577

18.9.2 只配置 fallback

上面的操作中,当代码中出现了异常的情况会跳转到 Error Page 页面,非常的不友好。

@RestControllerpublic class CircleBreakerController {	......    @GetMapping("/consumer/fallback/{id}")    @SentinelResource(value = "fallback", fallback = "handlerFallback") // fallback 只负责业务异常    public CommonResult<Payment> fallback(@PathVariable("id") Long id) {			......    }    // fallback,这里的 fallback 是对 Java 的异常进行处理的兜底方法,相当于 Hystrix 的服务降级    public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {        Payment payment = new Payment(id, "null");        return new CommonResult(4444, "兜底异常 handlerFallback,exception 内容: " + e.getMessage(), payment);    }}

image-20211021104008238

image-20211021103954088

18.9.3 只配置 blockHandler
@RestControllerpublic class CircleBreakerController {	......    @GetMapping("/consumer/fallback/{id}")    // blockHandler 只负责 Sentinel 控制台配置违规    @SentinelResource(value = "fallback", blockHandler = "blockHandler")     public CommonResult<Payment> fallback(@PathVariable("id") Long id) {		......    }    // blockHnadler    public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {        Payment payment = new Payment(id, "null");        return new CommonResult(445,"blockHnadler-sentinel 限流,没有这个流水:blockExceptoini" + blockException.getMessage());    }}

当我们访问的时候,传入的 id 是 4 或者[1,2,3] 之外的id都会报异常 Error Page 页面,

Sentinel 控制台设置流控,QPS 阈值为 1

image-20211021104801810

设置完成之后,一秒访问一次就会报异常 Error Page(这是 Java 的异常,Controller 没有兜底的方法进行处理);如果一秒钟访问超过 1 次就会出现下图:
image-20211021104921470

这个是我们 Controller 中的 (blockHandler 只负责 Sentinel 控制台配置违规)兜底方法

18.9.4 两个都配置

配置 fallback 和 blockHandler

@RestControllerpublic class CircleBreakerController {	......    @GetMapping("/consumer/fallback/{id}")    @SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")    public CommonResult<Payment> fallback(@PathVariable("id") Long id) {		......    }    // blockHnadler    public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {        Payment payment = new Payment(id, "null");        return new CommonResult(445,"blockHnadler-sentinel 限流,没有这个流水:blockExceptoini" + blockException.getMessage());    }     //fallback    public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {        Payment payment = new Payment(id, "null");        return new CommonResult(4444, "兜底异常 handlerFallback,exception 内容: " + e.getMessage(), payment);    }}

两个都配置,都有兜底的方法

配置 Sentinel 限流

image-20211021110249493

配置好限流之后测试

  • 如果 1 秒中访问 1 次则会走异常的方法 fallback

image-20211021110338010

  • 如果 1 秒中访问超过 1 次就会走 Sentinel 的 blockHandler

image-20211021110431111

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

18.9.5 异常忽略
@SentinelResource(value = "fallback", fallback = "handlerFallback",                        blockHandler = "blockHandler",                  		// 假如报该异常,不再有 fallback 方法兜底,没有降级效果了                        exceptionsToIgnore = {IllegalArgumentException.class})

image-20211021111017455

18.9.6 整合 Feign

在 84 上面进行修改

  1. pom.xml
<dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-loadbalancer</artifactId>    <version>3.0.4</version>    <scope>compile</scope>    <optional>true</optional></dependency>
  1. 配置文件 yml,使 sentinel 支持 feign
# 激活 Sentinel 对 Feign 的支持feign:  sentinel:    enabled: true
  1. 主启动类
@SpringBootApplication@EnableDiscoveryClient@EnableFeignClientspublic class OrderMain84 {
  1. Feign 的接口
@FeignClient(value = "nacos-payment-service", fallback = PaymentFallbackService.class)public interface PaymentService {    @GetMapping("/paymentSQL/{id}")    public CommonResult paymentSQL(@PathVariable("id") Long id);}
  1. Feign 的兜底方法
@Componentpublic class PaymentFallbackService implements PaymentService {    @Override    public CommonResult paymentSQL(Long id) {        return new CommonResult(4444444, "服务降级返回,-------PaymentFallbackService");    }}
  1. 业务类
//=========Feign=======@Resourceprivate PaymentService paymentService;@GetMapping("/consumer/paymentSQL/{id}")public CommonResult paymentSQL(@PathVariable("id") Long id) {    return paymentService.paymentSQL(id);}
  1. 测试 84 调用 9003

    正常访问

image-20211021113915404

关闭 9003 微服务提供者,看 84 消费会自动降价,不会被耗死

image-20211021114025365

18.9.7 Sentinel 和 Hystrix 对比
SentinelHystrix
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率
实时统计实现滑动窗口(LeapArray)滑动窗口(基于 RxJava)
动态规则配置支持多种数据源支持多种数据源
扩展性多个扩展点插件的形式
基于注解的支持支持支持
限流基于QPS,支持基于调用关系的限流有限的支持
流量整形支持预热模式、匀速器模式、预热排队模式不支持
系统自适应保护支持不支持
控制台提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等简单的监控查看
18.10 规则持久化

一旦我们重启应用,Sentinel 规则就会消失,生产环境需要将配置规则进行持久化

将限流配置规则持久化到 Nacos 保存,只要刷新 8401 某个 rest 地址,sentinel 控制台的流控规则就能看到,只要 Nacos 里面的配置不删除,针对 8401 上 Sentinel 上的流控规则持续有效。

启动 8401 进行测试

设置 8401 中的 /byUrl 接口的规则

image-20211021115335882

然后重启 8401 服务这个规则消失了,没有进行持久化

  • 修改 8401 服务
  1. pom.xml
<!--Spring Cloud alibaba sentinel-datasource-nacos 持久化--><dependency>    <groupId>com.alibaba.csp</groupId>    <artifactId>sentinel-datasource-nacos</artifactId></dependency>
  1. yml 配置
spring:  application:    name: nacos-sentinel-service  cloud:    nacos:      discovery:        server-addr: localhost:8848    sentinel:      transport:        # 配置 Sentinel dashboard 地址        dashboard: localhost:8080        # 默认端口号 8719 端口,假如被占用会自动从 8719 开始依次 +1 扫描,直到找到未被占用的端口        port: 8719      datasource:        dsl:          nacos:            server-addr: localhost:8848            dataId: nacos-sentinel-service            groupId: DEFAULT_GROUP            data-type: json            rule-type: flow

image-20211021121039428

  1. 在 Nacos 中新建配置文件

    [    {        "resource": "/rateLimit/byUrl",        "limitApp": "default",        "grade": 1,        "count": 1,        "strategy": 0,        "controlBehavior": 0,        "clusterMode": false    }]// 值的解释resource: 资源名称limitApp: 来源应用grade: 阈值类型,0 表示线程数,1 表示 QPScount: 单机阈值strategy: 流控模式,0 表示直接,1 表示关联,2 表示链路controlBehavior: 流控效果,0 表示快速失败,1 表示 WarmUp,2 表示排队等待clusterMode: 是否集群配置
    

image-20211021120509443

  1. 启动测试一下

    重启之后可以看到,这个配置信息已经保存,启动后查看需要稍等一会,多次调用接口。

image-20211021121138131

19. SpringCloudAlibaba Seata 处理分布式事务

19.1 分布式事务问题
  • 分布式之前:单机单库没有这个问题

以前一个 Java 应用程序既有Java 程序又有 MySQL,都封装在一块的,(1:1);随着业务的增对,Java 程序可能是一块,但是数据库会分成很多个,这就形成了 1:N;微服务架构出来之后,可能一个Java 程序是在一个模块中,但是数据库是分成多个,但分成多个的数据库物理上是分开的,但是逻辑上还是需要一块使用的,牵扯到这种跨数据库,多数据源调度就是分布式事务的前身(N:N)

  • 分布式之后

单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。

image-20211021144851617

19.2 Seata 简介

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

官网:http://seata.io/zh-cn/

一个典型的分布式事务过程

  • 分布式事务处理过程-ID Transaction ID XID 全局唯一的事务ID

  • 三组件模型

    • TC(Transaction Coordinator)事务协调者

      维护全局和分支事务的状态,负责协调并驱动全局事务提交或回滚

    • TM(Transaction Manager)事务管理器

      定义全局事务的范围:开始全局事务、提交或回滚全局事务。

      控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;

    • RM(Resource Manager)资源管理器

      管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

      控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

image-20211021151206234

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
  2. XID 在微服务调用链路的上下文中传播;
  3. RM 向 TC 注册分支事务,将其纳入 XID 对全局事务的管辖;
  4. TM 向 TC 发起针对 XID 的全局提交或回滚协议;
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

image-20211021152835176

19.3 Seata-Server 安装
19.3.1 0.9.0-GA 版本的

**下载地址:**https://github.com/seata/seata/releases

  1. 下载 Seata 到本地
  2. 解压到指定目录修改 conf 目录下的 file.conf 配置文件

image-20211021155831476

修改前先备份初始的 file.conf 文件,主要修改:自定义事务组名称 + 事务日志存储模式为 db + 数据库连接信息

  • 修改自定义事务组名称

image-20211021160208162

  • 事务日志存储模式 db + 数据库连接信息

image-20211021160236153

  1. 数据库中创建一个数据库 seata

在 seata 数据库里面建立表,表的脚本文件在 解压目录/conf/db_store.sql,复制sql预计执行

  1. 修改 seata-server-0.9.0\seata\conf 目录下的 registry.conf 配置文件

    指明注册中心为 Nacos,修改 Nacos 连接信息

image-20211021163811703

  1. 先启动 8848 Nacos
  2. 再启动 seata-server

image-20211021164112066

19.3.2 1.3.0 版本的安装

为什么使用 1.3.0 版本?因为我的 Spring Cloud Alibaba 用的是最新版的 2021.1 的,带有 seata 1.3.0 的依赖

  1. 下载地址:与上一样

    下载到本地之后解压缩

  2. 修改配置文件

  • file.conf

image-20211022155256108

  • registry.conf

image-20211023112717835

registry.conf 和 config 都是用 nacos,将 seata 服务器注册到 nacos,并且配置也通过 nacos 管理。

以上的服务端的配置信息,这个版本还需要一个客户端的配置,将这个配置注册到 nacos 中

找打 解压好的文件的 conf 目录下,打开 README.md 文件,这个文件里面有客户端的配置文件的下载地址,还有 sql 数据库脚本的下载地址。

image-20211022155651478

通过 config-center 这个连接找到 config.txt 配置文件下载到本地,放到与 conf 文件夹同级的目录下,修改配置信息

image-20211022155812218

image-20211022155835883

  • 通过 config-server 的连接找到 nacos-config.sh 执行脚本,这个脚本的 Linux 文件,所以使用 git 进行执行

    在执行命令之前在 Nacos 的中创建一个命名空间,用于存放我们的配置信息

image-20211022160335172

下载好之后放到 conf 文件夹下,打开 git ,执行命令 sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t cadb1d46-f1fa-417f-bcc7-504822fd55b3 -u nacos -w nacos

-h:nacos 的hostname

-p:nacos 的端口

-g:配置添加到那个 group 下

-t:配置添加到那个 namespace 下

-u:nacos 的 username

-w:nacos 的password

image-20211022160459531

image-20211022160519169

至此,seata 1.3.0 配置完成,启动 seata

数据库从 README.md 文件的 server 连接进,创建 seata 数据库

配置参考视频,以及下面的 Spring Cloud 中集成也是参考这个视频,主要是 yml 配置文件:https://www.bilibili.com/video/BV1254y1a7G7?p=1

# 主要是这块的配置信息seata:  enabled: true  application-id: seata-bus  tx-service-group: my_test_tx_group  enable-auto-data-source-proxy: true  service:    vgroup-mapping.my_test_tx_group: default    grouplist.default: 127.0.0.1:8091
19.4 订单/库存/账户业务数据库准备

保证 nacos 和 seata 启动成功

  • 分布式业务的说明

创建三个微服务,一个订单服务、一个库存服务、一个账户服务。

当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

  • 创建数据库

seata_order:存储订单的数据库;

seata_storage:存储库存的数据库;

seata_account:存储账户信息的数据库;

  • 3 个库中分别建 1 一张表

seata_order 库下建 t_order 表

seata_storage 库下建 t_storage 表

seata_account 库下建 t_account 表

CREATE TABLE t_order(	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,	`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',	`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',	`count` INT(11) DEFAULT NULL COMMENT '数量',	`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',	`status` INT(1) DEFAULT NULL COMMENT '订单状态:0创建中,1已完结') ENGINE = INNODB AUTO_INCREMENT = 7 DEFAULT CHARSET = UTF8;INSERT INTO t_order VALUES (55,1,1,10,100,1)SELECT * FROM t_orderCREATE TABLE t_storage(	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,	`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品 id',	`total` INT(11) DEFAULT NULL COMMENT '总库存',	`used` INT(11) DEFAULT NULL COMMENT '已用库存',	`residue` INT(11) DEFAULT NULL COMMENT '剩余库存') ENGINE = INNODB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8;INSERT INTO t_storage VALUES (1,1,100,0,100)SELECT * FROM t_storageCREATE TABLE t_account (	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',	`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',	`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',	`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用额度',	`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度')ENGINE = INNODB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8;INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUE ('1','1','1000','0','1000');SELECT * FROM t_account
  • 按照 3 个库分别创建对应的回滚日志表

订单-库存-账户 3 个库下都需要建各自的回滚日志表

seata 解压目录/conf/db_undo_log.sql

image-20211021171614202

19.5 订单/库存/账户业务微服务准备
19.5.1 业务需求

下订单 —> 减库存 —> 扣余额 —> 改订单状态

19.5.2 新建订单 Order-Module
  1. 创建一个 seata-order-service2001 模块
  2. **修改 pom.xml **
<dependency>    <groupId>com.alibaba.cloud</groupId>    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>    <exclusions>        <!--去除低版本-->        <exclusion>            <groupId>io.seata</groupId>            <artifactId>seata-spring-boot-starter</artifactId>        </exclusion>    </exclusions></dependency><!-- 添加 seata starter ,与服务端保持一致--><dependency>    <groupId>io.seata</groupId>    <artifactId>seata-spring-boot-starter</artifactId>    <version>1.3.0</version></dependency><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency>    <groupId>org.springframework.cloud</groupId>    <artifactId>spring-cloud-starter-loadbalancer</artifactId>    <version>2.2.9.RELEASE</version></dependency><dependency>    <groupId>com.alibaba.csp</groupId>    <artifactId>sentinel-datasource-nacos</artifactId></dependency><!--Spring Cloud Alibaba Nacos--><dependency>    <groupId>com.alibaba.cloud</groupId>    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>    <version>2.2.0.RELEASE</version>    <exclusions>        <exclusion>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>        </exclusion>    </exclusions></dependency>

image-20211022165630088

  1. yml 配置文件
server:  port: 2001spring:  application:    name: seata-order-service  datasource:    url: jdbc:mysql://localhost:3306/seata_order?userUnicode=true&characterEncoding=utf-8&serverTimezone=UTC    username: root    password: root    driver-class-name: com.mysql.jdbc.Driver    type: com.alibaba.druid.pool.DruidDataSource    #SpringBoot默认是不注入这些的,需要自己绑定    #druid数据源专有配置    initialSize: 5    minIdle: 5    maxActive: 20    maxWait: 60000    timeBetweenEvictionRunsMillis: 60000    minEvictableIdleTimeMillis: 300000    validationQuery: SELECT 1 FROM DUAL    testWhileIdle: true    testOnBorrow: false    testOnReturn: false    poolPreparedStatements: true    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入    #如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity    #则导入log4j 依赖就行    filters: stat,wall,log4j    maxPoolPreparedStatementPerConnectionSize: 20    useGlobalDataSourceStat: true    connectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500  cloud:    nacos:      discovery:        server-addr: 127.0.0.1:8848        namespace: public      config:        server-addr: 127.0.0.1:8848        namespace: publiclogging:  level:    io:      seata: infomybatis:  mapper-locations: classpath:mapper/*.xml# seata 的主要配置信息seata:  enabled: true  application-id: seata-bus  tx-service-group: my_test_tx_group  enable-auto-data-source-proxy: true  service:    vgroup-mapping.my_test_tx_group: default    grouplist.default: 127.0.0.1:8091
  1. 在 resources 文件夹下创建 file.conf 和 registry.conf 文件

    这块可以不用配置了,在 1.3.0 中不配置照常可以使用

    内容就是 seata 配置文件中的内容

image-20211022165757278

  1. 创建实体类
/** *  t_order  的实体类 */@Data@AllArgsConstructor@NoArgsConstructorpublic class Order {    private Long id;    private Long userId;    private Long productId;    private Integer count;    private BigDecimal money;    // 订单状态  0 表示创建中,1 表示已完结    private Integer status;}
  1. 创建接口和实现类
@Mapperpublic interface OrderMapper {    // 1. 新建订单    void create(Order order);    // 2. 修改订单状态,所有的下单支付成功后从 0 改为 1    void  update(@Param("userId") Long userId,@Param("status") Integer status);}

在 resources/mapper/ 创建对应的配置xml,OrderMapper.xml

<mapper namespace="com.lss.springcloud.mapper.OrderMapper">    <resultMap id="BaseMap" type="com.lss.springcloud.domain.Order">        <id column="id" property="id" jdbcType="BIGINT"/>        <result column="user_id" property="userId" jdbcType="BIGINT"/>        <result column="product_id" property="productId" jdbcType="BIGINT"/>        <result column="count" property="count" jdbcType="INTEGER"/>        <result column="money" property="money" jdbcType="DECIMAL"/>        <result column="status" property="status" jdbcType="INTEGER"/>    </resultMap>    <insert id="create">        insert into t_order (id,user_id,product_id,count,money,status)        values (null,#{userId},#{productId},#{count},#{money},0)    </insert>    <update id="update">        update t_order set status = 1 where user_id = #{userId}        and status = #{status}    </update></mapper>
  1. 创建 service 接口和 实现
public interface OrderService {    // 1. 新建订单    void create(Order order);}

impl,调用实际的业务

@Service@Slf4jpublic class OrderServerImpl implements OrderService {    @Resource    private OrderMapper orderMapper;    @Resource    private StorageService storageService;    @Resource    private AccountService accountService;    /**     * 创建订单 --- 调用库存服务扣减库存 --- 调用账户服务扣减账户余额 --- 修改订单状态     */    @Override    public void create(Order order) {        log.info("---开始创建订单");        // 1. 新建订单        orderMapper.create(order);        log.info("---订单微服务开始调用库存,扣减库存数量count");        // 2. 扣减库存        storageService.decrease(order.getProductId(),order.getCount());        log.info("---订单微服务开始调用库存,扣减库存数量coutn end");        log.info("---订单微服务开始调用账户,扣减余额 money");        // 3. 扣减余额        accountService.decrease(order.getUserId(),order.getMoney());        log.info("---订单微服务开始调用账户,扣减余额 money end");        // 4. 修改订单状态,从 0 到 1,1 代表已经完成        log.info("---修改订单状态");        orderMapper.update(order.getUserId(), 0);        log.info("---修改订单状态结束");        log.info("---订单已经完成");    }}

因为需要调用别的库的表,所以需要在这里建立 AccountService 和 StorageService 接口

@FeignClient("seata-account-service")public interface AccountService {    @PostMapping("/account/decrease")    CommonResult decrease(@RequestParam("userId") Long userId,                          @RequestParam("money") BigDecimal money);}@FeignClient("seata-storage-service")public interface StorageService {    @PostMapping("/storage/decrease")    CommonResult decrease(@RequestParam("productId") Long productId,                          @RequestParam("count") Integer count);}
  1. Controller
@RestControllerpublic class OrderController {    @Autowired    private OrderService orderService;    @GetMapping("/order/create")    public CommonResult create(Order order) {        orderService.create(order);        return new CommonResult(200,"订单创建成功");    }}
  1. Config 配置
  • MyBatisConfig
@Configuration@MapperScan({"com.lss.springcloud.mapper"})public class MyBatisConfig {}
  • DataSourceProxyConfig
/** * 使用 Seata 对数据源进行代理 */@Configurationpublic class DataSourceProxyConfig {    @Value("${mybatis.mapper-locations}")    private String mapperLocations;    @Bean    @ConfigurationProperties(prefix = "spring.datasource")    public DataSource druidDataSource() {        System.out.println(mapperLocations + "-==================");        return new DruidDataSource();    }    @Bean	//DataSourceProxy 这个数据库代理是  io.seata.rm.datasource.DataSourceProxy    public DataSourceProxy dataSourceProxy(DataSource dataSource) {        return new DataSourceProxy(dataSource);    }    @Bean    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();        sqlSessionFactoryBean.setDataSource(dataSourceProxy);        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());        return sqlSessionFactoryBean.getObject();    }}
  1. 主启动
@EnableDiscoveryClient@EnableFeignClients@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动创建public class SeataOrderMain2001 {
  1. 启动测试

控制台没有发现任何的错误信息,启动测试正常

19.5.3 新建库存 Storage-Module
  1. 建子模块 seata-storage-service2002

  2. pom.xml

    与上一个一样

  3. yml配置

    与上一个一样

  4. 主启动类

    与上一个一样

  5. Mybatis 、DataSourceProxyConfig 配置类

    与上一个一样

  6. 实体类

    根据表创建

  7. mapper 接口

@Mapperpublic interface StorageMapper {    public CommonResult decrease(@RequestParam("productId") Long productId,                                 @RequestParam("count") Integer count);}

接口实现

<?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.lss.springcloud.mapper.StorageMapper">    <resultMap id="BaseMap" type="com.lss.springcloud.domain.Storage">        <id column="id" property="id" jdbcType="BIGINT"/>        <result column="product_id" property="productId" jdbcType="BIGINT"/>        <result column="total" property="total" jdbcType="INTEGER"/>        <result column="used" property="used" jdbcType="INTEGER"/>        <result column="residue" property="residue" jdbcType="INTEGER"/>    </resultMap>    <update id="decrease">        update            t_storage        set            used = used + #{count},residue = residue - #{count}        where            product_id = #{productId}    </update></mapper>
  1. 服务 service
public interface StorageService {    /**     * 扣减库存     */    void decrease(Long productId,Integer count);}

service 实现

@Service@Slf4jpublic class StorageServiceImpl implements StorageService {    @Resource    private StorageMapper storageMapper;    @Override    public void decrease(Long productId, Integer count) {        log.info("------扣减开始");        storageMapper.decrease(productId,count);        log.info("------扣减结束");    }}
  1. controller
@RestControllerpublic class StorageController {    @Resource    private StorageService storageService;    @PostMapping("/storage/decrease")    public CommonResult decrease(@RequestParam("productId") Long productId,                          @RequestParam("count") Integer count) {        storageService.decrease(productId, count);        return new CommonResult(200,"扣减成功");    }}
19.5.4 新建账户 Account-Module

配置与上面一样

  • 接口
@Mapperpublic interface AccountMapper {    void decrease(@RequestParam("userId") Long userId, @RequestParam("money")BigDecimal money);}

接口实现

<?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.lss.springcloud.mapper.AccountMapper">    <resultMap id="BaseMap" type="com.lss.springcloud.domain.Account">        <id column="id" property="id" jdbcType="BIGINT"/>        <result column="user_id" property="userId" jdbcType="BIGINT"/>        <result column="total" property="total" jdbcType="BIGINT"/>        <result column="used" property="used" jdbcType="DECIMAL"/>        <result column="residue" property="residue" jdbcType="DECIMAL"/>    </resultMap>    <update id="decrease">        update            t_account        set            used = used + #{money},residue = residue - #{money}        where            user_id = #{userId}    </update></mapper>
  • service 服务层接口
public interface AccountService {    void decrease(@RequestParam("userId") Long userId, @RequestParam("money")BigDecimal money);}

接口实现

@Service@Slf4jpublic class AccountServiceImpl implements AccountService {    @Autowired    private AccountMapper accountMapper;    @Override    public void decrease(Long userId, BigDecimal money) {        log.info("------开始扣减余额");        accountMapper.decrease(userId,money);        log.info("------扣减余额结束");    }}
  • controller
@RestControllerpublic class AccountController {    @Autowired    private AccountService accountService;    @PostMapping("/account/decrease")    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money")BigDecimal money) {        accountService.decrease(userId,money);        return new CommonResult(200,"扣减余额成功");    }}
19.6 Test

在浏览器中输入 http://localhost:2001/order/create?userId=1&productId=001&count=222&money=50

查看数据库中的3 个数据库中表,已经发生了相应的变化;查看控制台输出所有的模块都执行完成。

image-20211023143237435

模拟服务中出现异常

在 account 的实现类中加入超时异常

@Overridepublic void decrease(Long userId, BigDecimal money) throws InterruptedException {    System.out.println(userId);    System.out.println(money);    log.info("------开始扣减余额");    // 模拟异常,全局事务回滚    int age = 1 / 0;    accountMapper.decrease(userId,money);    log.info("------扣减余额结束");}

因为没有设置服务降级等一些措施,在次启动服务会出现超时异常 Error Page

image-20211023143942188

虽然这里出现了异常,但是我们仍然在 order 表中看到了新增加的订单的信息,即扣了钱,订单还没完成

image-20211023144940443

故障情况:当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从 0 改为 1,而且由于 feign 的重试机制,账户余额还有可能被多次扣减。

解决

在 OrderServiceImple (业务入口)类的上面加 @GlobalTransactional 注解,实现全局事务

@Override@GlobalTransactional(name = "名字随便,唯一就行", rollbackFor = Exception.class)public void create(Order order) {    log.info("---开始创建订单");    // 1. 新建订单    orderMapper.create(order);

重启服务执行,还是会报 500 的错误,但是我们的数据库里面不会在出现错误的数据了,当执行到服务入口的时候出现异常会进行回滚,保证数据的正确性。

19.7 原理

2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。

Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架。

image-20211023151314120

image-20211023151519231

  • 分布式事务的执行流程
  1. TM 开启分布式事务(TM 向 TC 注册全局事务记录,就是加了 @GlobalTransactional 注解的事务入口开启);
  2. 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态);
  3. TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
  4. TC 汇总事务信息,决定分布式事务是提交还是回滚;
  5. TC 通知所有 RM 提交/回滚资源,事务二阶段结束。
  • AT 模式如何做到对业务的无侵入

image-20211023152303050

在一阶段,Seata 会拦截“业务 SQL”

  1. 解析 SQL 语义,找到 “业务 SQL” 要更新的业务数据,在业务数据被更新前,将其保存成 “before image”,
  2. 执行 “业务 SQL” 更新业务数据,在业务数据更新之后,
  3. 其保存成 “after image” ,最后生成行锁。

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

image-20211023152629941

二阶段提交如果顺利提交的话,因为 “业务 SQL” 在一阶段已经提交至数据库,所以 Seata 框架只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

image-20211023152821362

二阶段回滚,二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用 “before image” 还原业务数据;但在还原前首先要检验脏写,对比 “数据库当前业务数据” 和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

image-20211023153147069

debug 运行查看

在 AccountServiceImpl 类上加上断点,然后重启 3 个服务。

image-20211023155225060

然后访问一下地址,创建一个订单,然后查看数据库中的变化

http://localhost:2001/order/create?userId=789&productId=001&count=993&money=50

image-20211023155910413

在订单表的 undo_log 中会记录 before image after image 等信息,用于回滚。执行的过程中每条操作的值在表中都有记录,等所有操作执行完毕,删除干活使用的记录。

image-20211023160731443

**学习参考视频:**https://www.bilibili.com/video/BV18E411x7eT?p=1

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值