最新版 SpringCloud + SpringCloudAlibaba 从入门到实战一套带走(使用规范,注册与配置中心,负载均衡,熔断,服务降级,限流,链路追踪,网关,分布式 ......)

✨Spring官网:https://spring.io/✨

工程源码:https://github.com/Fetters04/cloud2024.git

SpringCloud + SpringCloudAlibaba

一、工程模块搭建

1、SpringBoot和SpringCloud版本选型

在这里插入图片描述

2、SpringCloud全家桶能做什么

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3、微服务架构Base工程模块(逐步添加Cloud组件)

3.1、业务需求说明

在这里插入图片描述

3.2、项目工程配置

为确保工程顺序实现,提前设定好统一配置

(1)字符编码

在这里插入图片描述

(2)注解生效激活

在这里插入图片描述

(3)Java 编译版本

在这里插入图片描述

(4)File Type 过滤

在这里插入图片描述

(5)项目依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.atguigu.cloud</groupId>
    <artifactId>cloud2024</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <hutool.version>5.8.22</hutool.version>
        <lombok.version>1.18.26</lombok.version>
        <druid.version>1.1.20</druid.version>
        <mybatis.springboot.version>3.0.3</mybatis.springboot.version>
        <mysql.version>8.0.11</mysql.version>
        <swagger3.version>2.2.0</swagger3.version>
        <mapper.version>4.2.3</mapper.version>
        <fastjson2.version>2.0.40</fastjson2.version>
        <persistence-api.version>1.0.2</persistence-api.version>
        <spring.boot.test.version>3.1.5</spring.boot.test.version>
        <spring.boot.version>3.2.0</spring.boot.version>
        <spring.cloud.version>2023.0.0</spring.cloud.version>
        <spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!--springboot 3.2.0-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--springcloud 2023.0.0-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--springcloud alibaba 2022.0.0.0-RC2-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--SpringBoot集成mybatis-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.springboot.version}</version>
            </dependency>
            <!--Mysql数据库驱动8 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!--SpringBoot集成druid连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <!--通用Mapper4之tk.mybatis-->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper</artifactId>
                <version>${mapper.version}</version>
            </dependency>
            <!--persistence-->
            <dependency>
                <groupId>javax.persistence</groupId>
                <artifactId>persistence-api</artifactId>
                <version>${persistence-api.version}</version>
            </dependency>
            <!-- fastjson2 -->
            <dependency>
                <groupId>com.alibaba.fastjson2</groupId>
                <artifactId>fastjson2</artifactId>
                <version>${fastjson2.version}</version>
            </dependency>
            <!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
                <version>${swagger3.version}</version>
            </dependency>
            <!--hutool-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>${hutool.version}</version>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <optional>true</optional>
            </dependency>
            <!-- spring-boot-starter-test -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <version>${spring.boot.test.version}</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

3.3、持久层生成器

创建子项目 mybatis_generator2024 用于生成持久层代码

3.3.1、POM 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.atguigu.cloud</groupId>
        <artifactId>cloud2024</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>mybatis_generator2024</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>
        <!--Mybatis 通用mapper tk单独使用,自己独有+自带版本号-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.13</version>
        </dependency>
        <!-- Mybatis Generator 自己独有+自带版本号-->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.4.2</version>
        </dependency>
        <!--通用Mapper-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
        </dependency>
        <!--mysql8.0-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--persistence-->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!--lombok-->
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>${basedir}/src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>${basedir}/src/main/resources</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.2</version>
                <configuration>
                    <configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
                    <overwrite>true</overwrite>
                    <verbose>true</verbose>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.33</version>
                    </dependency>
                    <dependency>
                        <groupId>tk.mybatis</groupId>
                        <artifactId>mapper</artifactId>
                        <version>4.2.3</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>
3.3.2、Mapper4一键生成Dao层代码

1️⃣数据库配置(创建表 t_pay)

DROP TABLE IF EXISTS `t_pay`;

 

CREATE TABLE `t_pay` (

  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,

  `pay_no` VARCHAR(50) NOT NULL COMMENT '支付流水号',

  `order_no` VARCHAR(50) NOT NULL COMMENT '订单流水号',

  `user_id` INT(10) DEFAULT '1' COMMENT '用户账号ID',

  `amount` DECIMAL(8,2) NOT NULL DEFAULT '9.9' COMMENT '交易金额',

  `deleted` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除标志,默认0不删除,1删除',

  `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

  `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

  PRIMARY KEY (`id`)

) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='支付交易表';

 

INSERT INTO t_pay(pay_no,order_no) VALUES('pay17203699','6544bafb424a');

 

SELECT * FROM t_pay;

2️⃣配置文件配置

config.properties:

#t_pay表包名
package.name=com.atguigu.cloud

# mysql8.0
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456

generatorConfig.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <properties resource="config.properties"/>

    <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
            <property name="caseSensitive" value="true"/>
        </plugin>

        <jdbcConnection driverClass="${jdbc.driverClass}"
                        connectionURL="${jdbc.url}"
                        userId="${jdbc.user}"
                        password="${jdbc.password}">
        </jdbcConnection>

        <javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>

        <sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>

        <javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>

        <table tableName="t_pay" domainObjectName="Pay">
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
    </context>
</generatorConfiguration>

3️⃣使用Maven插件中的 mybatis-generator:generate 一键生成文件 Pay 表的 PayMapper 和 PayMapper.xml 文件

在这里插入图片描述

⬇️⬇️⬇️生成如下⬇️⬇️⬇️

在这里插入图片描述

3.4、支付服务提供者8001模块工程化编写

在这里插入图片描述

约定 > 配置 > 编码

1️⃣ 建 module

2️⃣ 改 POM

3️⃣ 写 YML

4️⃣ 主启动

5️⃣ 业务类

(1)建 module

创建一个子项目 cloud-provider-payment8001

在这里插入图片描述

在这里插入图片描述

(2)改 POM

在该模块的 pom.xml 中引入依赖坐标

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

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

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>
        <!--SpringBoot通用依赖模块-->
        <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>
        <!--SpringBoot集成druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>
        <!--mybatis和springboot整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--Mysql数据库驱动8 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--persistence-->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
        </dependency>
        <!--通用Mapper4-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!-- fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
            <scope>provided</scope>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
(3)写 YML

在这里插入图片描述

server:
  port: 8001

# ==========applicationName + druid-mysql8 driver===================
spring:
  application:
    name: cloud-payment-service

  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: 123456

# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.cloud.entities
  configuration:
    map-underscore-to-camel-case: true
(4)主启动

在这里插入图片描述

(5)业务类

在这里插入图片描述

  • entities: 从生成器中拷贝 Pay 实体,创建PayDTO对象

    PayDTO:

    package com.atguigu.cloud.entities;
    
    import io.swagger.v3.oas.annotations.media.Schema;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.io.Serializable;
    import java.math.BigDecimal;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class PayDTO implements Serializable
    {
        private Integer id;
        //支付流水号
        private String payNo;
        //订单流水号
        private String orderNo;
        //用户账号ID
        private Integer userId;
        //交易金额
        private BigDecimal amount;
    }
    
    
  • Dao层: 从生成器中拷贝 PayMapper 和 PayMapper.xml 放入对应文件目录中

  • Service层: 编写业务逻辑层代码

    PayService:

    package com.atguigu.cloud.service;
    
    import com.atguigu.cloud.entities.Pay;
    import java.util.List;
    
    public interface PayService {
        int add(Pay pay);
        int delete(Integer id);
        int update(Pay pay);
        Pay getById(Integer id);
        List<Pay> getAll();
    }
    
    

    impl.PayServiceImpl:

    package com.atguigu.cloud.service.impl;
    
    import com.atguigu.cloud.entities.Pay;
    import com.atguigu.cloud.mapper.PayMapper;
    import com.atguigu.cloud.service.PayService;
    import jakarta.annotation.Resource;
    
    import java.util.List;
    
    @Service
    public class PayServiceImpl implements PayService {
    
        @Resource
        private PayMapper payMapper;
    
        @Override
        public int add(Pay pay) {
            return payMapper.insertSelective(pay);
        }
    
        @Override
        public int delete(Integer id) {
            return payMapper.deleteByPrimaryKey(id);
        }
    
        @Override
        public int update(Pay pay) {
            return payMapper.updateByPrimaryKeySelective(pay);
        }
    
        @Override
        public Pay getById(Integer id) {
            return payMapper.selectByPrimaryKey(id);
        }
    
        @Override
        public List<Pay> getAll() {
            return payMapper.selectAll();
        }
    }
    
    
  • Controller层: 编写控制层代码

    PayController:

    package com.atguigu.cloud.controller;
    
    import com.atguigu.cloud.entities.Pay;
    import com.atguigu.cloud.entities.PayDTO;
    import com.atguigu.cloud.service.PayService;
    import jakarta.annotation.Resource;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.BeanUtils;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    @RestController
    @Slf4j
    public class PayController {
    
        @Resource
        private PayService payService;
    
        @PostMapping(value = "/pay/add")
        public String addPay(@RequestBody Pay pay) {
    
            System.out.println(pay.toString());
    
            int i = payService.add(pay);
    
            return "成功插入,返回值:" + i;
        }
    
        @DeleteMapping(value = "/pay/del/{id}")
        public Integer deletePay(@PathVariable("id") Integer id) {
    
            return payService.delete(id);
        }
    
        @PutMapping(value = "/pay/update")
        public String updatePay(@RequestBody PayDTO payDTO) {
    
            Pay pay = new Pay();
    
            BeanUtils.copyProperties(payDTO, pay);
    
            int i = payService.update(pay);
            return "成功修改记录,返回值:" + i;
        }
    
        @GetMapping(value = "/pay/get/{id}")
        public Pay getById(@PathVariable("id") Integer id) {
    
            return payService.getById(id);
        }
    
        @GetMapping(value = "/pay/getAll")
        public List<Pay> getAll() {
    
            return payService.getAll();
        }
    
    }
    
    

3.5、Swagger3 测试

(1)常用注解

在这里插入图片描述

(2)修改 Controller 和 entity

在 Controller 上使用 @Tag@Operation 注解标识

在这里插入图片描述

在实体类上使用 @Schema 注解标识

在这里插入图片描述

(3)创建配置类 Swagger3Config

在这里插入图片描述

Swagger3Config:

package com.atguigu.cloud.config;

import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Swagger3Config
{
    @Bean
    public GroupedOpenApi PayApi()
    {
        return GroupedOpenApi.builder().group("支付微服务模块").pathsToMatch("/pay/**").build();
    }
    @Bean
    public GroupedOpenApi OtherApi()
    {
        return GroupedOpenApi.builder().group("其它微服务模块").pathsToMatch("/other/**", "/others").build();
    }
    /*@Bean
    public GroupedOpenApi CustomerApi()
    {
        return GroupedOpenApi.builder().group("客户微服务模块").pathsToMatch("/customer/**", "/customers").build();
    }*/

    @Bean
    public OpenAPI docsOpenApi()
    {
        return new OpenAPI()
                .info(new Info().title("cloud2024")
                        .description("通用设计rest")
                        .version("v1.0"))
                .externalDocs(new ExternalDocumentation()
                        .description("www.atguigu.com")
                        .url("https://yiyan.baidu.com/"));
    }
}
(4)调用 Swagger

调用地址:http://localhost:8001/swagger-ui/index.html,可以在此进行接口测试。

在这里插入图片描述

3.6、工程优化

(1)时间格式问题

问题:

在这里插入图片描述

完善成常见的时间格式:年-月-日 时-分-秒

在这里插入图片描述

/**
 * 创建时间
 */
@Column(name = "create_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;

/**
 * 更新时间
 */
@Column(name = "update_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
 
(2)统一返回值

定义返回格式标准:

code: 由后端统一定义的各种返回结果的状态码

message: 本次接口调用的结果描述

data: 本次返回的数据

[扩展]timestamp: 接口调用时间

HTTP请求返回的状态码:

在这里插入图片描述

新建枚举类 ReturnCodeEnum:

定义一个通用的枚举类方法:举值-构造-遍历

package com.atguigu.cloud.resp;

import lombok.Getter;

import java.util.Arrays;

@Getter
public enum ReturnCodeEnum
{
    /**操作失败**/
    RC999("999","操作XXX失败"),
    /**操作成功**/
    RC200("200","success"),
    /**服务降级**/
    RC201("201","服务开启降级保护,请稍后再试!"),
    /**热点参数限流**/
    RC202("202","热点参数限流,请稍后再试!"),
    /**系统规则不满足**/
    RC203("203","系统规则不满足要求,请稍后再试!"),
    /**授权规则不通过**/
    RC204("204","授权规则不通过,请稍后再试!"),
    /**access_denied**/
    RC403("403","无访问权限,请联系管理员授予权限"),
    /**access_denied**/
    RC401("401","匿名用户访问无权限资源时的异常"),
    RC404("404","404页面找不到的异常"),
    /**服务异常**/
    RC500("500","系统异常,请稍后重试"),
    RC375("375","数学运算异常,请稍后重试"),

    INVALID_TOKEN("2001","访问令牌不合法"),
    ACCESS_DENIED("2003","没有权限访问该资源"),
    CLIENT_AUTHENTICATION_FAILED("1001","客户端认证失败"),
    USERNAME_OR_PASSWORD_ERROR("1002","用户名或密码错误"),
    BUSINESS_ERROR("1004","业务逻辑异常"),
    UNSUPPORTED_GRANT_TYPE("1003", "不支持的认证模式");

    /**自定义状态码**/
    private final String code;
    /**自定义描述**/
    private final String message;

    ReturnCodeEnum(String code, String message){
        this.code = code;
        this.message = message;
    }

    //遍历枚举V1
    public static ReturnCodeEnum getReturnCodeEnum(String code)
    {
        for (ReturnCodeEnum element : ReturnCodeEnum.values()) {
            if(element.getCode().equalsIgnoreCase(code))
            {
                return element;
            }
        }
        return null;
    }
    //遍历枚举V2
    public static ReturnCodeEnum getReturnCodeEnumV2(String code)
    {
        return Arrays.stream(ReturnCodeEnum.values()).filter(x -> x.getCode().equalsIgnoreCase(code)).findFirst().orElse(null);
    }


    /*public static void main(String[] args)
    {
        System.out.println(getReturnCodeEnumV2("200"));
        System.out.println(getReturnCodeEnumV2("200").getCode());
        System.out.println(getReturnCodeEnumV2("200").getMessage());
    }*/
}

新建统一定义返回对象 ResultData:

package com.atguigu.cloud.resp;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class ResultData<T> {

    private String code;
    private String message;
    private T data;
    private long timestamp;

    public ResultData() {
        this.timestamp = System.currentTimeMillis();
    }

    public static <T> ResultData<T> success(T data) {

        ResultData<T> resultData = new ResultData<>();

        resultData.setCode(ReturnCodeEnum.RC200.getCode());
        resultData.setMessage(ReturnCodeEnum.RC200.getMessage());
        resultData.setData(data);

        return resultData;
    }

    public static <T> ResultData<T> fail(String code, String message) {

        ResultData<T> resultData = new ResultData<>();

        resultData.setCode(code);
        resultData.setMessage(message);
        resultData.setData(null);

        return resultData;
    }

}

修改Controller:

package com.atguigu.cloud.controller;

import com.atguigu.cloud.entities.Pay;
import com.atguigu.cloud.entities.PayDTO;
import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.service.PayService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;

import javax.xml.transform.Result;
import java.util.List;

@RestController
@Slf4j
@Tag(name = "支付微服务模块", description = "支付CRUD")
public class PayController {

    @Resource
    private PayService payService;

    @PostMapping(value = "/pay/add")
    @Operation(summary = "新增", description = "新增支付流水方法")
    public ResultData<String> addPay(@RequestBody Pay pay) {

        System.out.println(pay.toString());

        int i = payService.add(pay);

        return ResultData.success("成功插入,返回值:" + i);
    }

    @DeleteMapping(value = "/pay/del/{id}")
    @Operation(summary = "删除", description = "删除支付流水方法")
    public ResultData<Integer> deletePay(@PathVariable("id") Integer id) {

        int i = payService.delete(id);

        return ResultData.success(i);
    }

    @PutMapping(value = "/pay/update")
    @Operation(summary = "修改", description = "修改支付流水方法")
    public ResultData<String> updatePay(@RequestBody PayDTO payDTO) {

        Pay pay = new Pay();

        BeanUtils.copyProperties(payDTO, pay);

        int i = payService.update(pay);

        return ResultData.success("成功修改记录,返回值:" + i);
    }

    @GetMapping(value = "/pay/get/{id}")
    @Operation(summary = "查询", description = "查询支付流水方法")
    public ResultData<Pay> getById(@PathVariable("id") Integer id) {

        Pay pay = payService.getById(id);

        return ResultData.success(pay);
    }

    @GetMapping(value = "/pay/getAll")
    @Operation(summary = "查询All", description = "查询所有支付流水方法")
    public ResultData<List<Pay>> getAll() {

        List<Pay> payList = payService.getAll();

        return ResultData.success(payList);
    }

}

(3)全局异常接入返回的标准格式

新建全局异常类 GlobalExceptionHandler:

package com.atguigu.cloud.exp;

import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.resp.ReturnCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultData<String> exception(Exception e) {

        System.out.println("come in GlobalExceptionHandler");

        log.error("全局异常信息:{}", e.getMessage());

        return ResultData.fail(ReturnCodeEnum.RC500.getCode(), e.getMessage());
    }

}

3.7、当前目录结构

在这里插入图片描述

4、微服务调用者订单80模块

在这里插入图片描述

4.1、创建微服务工程

1️⃣ 建 model

在这里插入图片描述

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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.atguigu.cloud</groupId>
        <artifactId>cloud2024</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

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

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--web + actuator-->
        <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>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--hutool-all-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!--fastjson2-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3️⃣ 编写 application.yml 文件

server:
  port: 80

4️⃣ 编写主启动类 Main80.java

package com.atguigu.cloud;

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

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

5️⃣ 写业务

一般而言,调用者不应该获悉服务提供者的entity资源并知道表结构关系,所以服务提供方给出的接口文档都都应成为DTO。

在这里插入图片描述

4.2、RestTemplate

服务消费者去调用服务提供者提供的服务时,使用了一个极其方便的对象叫RestTemplate,我们通常使用RestTemplate中最简单的一个功能getForObject 发起了一个get请求去调用服务端的数据,同时,我们还通过配置@LoadBalanced注解开启客户端负载均衡

RestTemplate是基于Rest的请求方式,通常是有四种

  • GET 请求 --查询数据

  • POST 请求 –添加数据

  • PUT 请求 – 修改数据

  • DELETE 请求 –删除数据

(1)RestTemplate 的 GET 请求

GET 请求有两种方式:

1️⃣ getForEntity

该方法返回一个 ResponseEntity对象,ResponseEntity是 Spring 对 HTTP 请求响应的封装,包括了几个重要的元素,比如响应码、contentType、 contentLength、响应消息体等。

在这里插入图片描述

(url, requestMap, ResponseBean.class)这三个参数分别代表 REST请求地址、请求参数、HTTP响应转换被转换成的对象类型。

//调用SpringCloud服务提供者提供的服务
ResponseEntity<String> responseEntity = restTemplate.getForEntity(serviceName + "/service/hello", String.class);

int statusCodeValue = responseEntity.getStatusCodeValue();
HttpStatus httpStatus = responseEntity.getStatusCode();
HttpHeaders httpHeaders = responseEntity.getHeaders();
String body = responseEntity.getBody();

System.out.println(statusCodeValue);
System.out.println(httpStatus);
System.out.println(httpHeaders);
System.out.println(body);

return responseEntity.getBody();
两个重载方法: 
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException 
如: 
restTemplate.getForEntity("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/hello?id= {1}&name={2}", String.class, "{1, '张三'}").getBody(); 
 
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException 
如: 
Map<String, Object> paramMap =new ConcurrentHashMap<>(); 
paramMap.put("id", 1); 
paramMap.put("name", "张三"); 
restTemplate.getForEntity("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/hello?id= {id}&name={name}", String.class, paramMap).getBody(); 

2️⃣ getForObject()

getForObject()与 getForEntity 使用类似,getForObject是在getForEntity基础上进行了再次封装,可以将http的响应体body信息转化成指定的对象,方便开发。

在这里插入图片描述

(2)RestTemplate 的 POST 请求

在这里插入图片描述

(3)RestTemplate 的 PUT 请求

在这里插入图片描述

(4)RestTemplate 的 DELETE 请求

在这里插入图片描述

(5)业务实现

编写配置类 RestTemplateConfig:

package com.atguigu.cloud.config;

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

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

在 Controller 中使用 RestTempalte

package com.atguigu.cloud.controller;

import com.atguigu.cloud.entities.PayDTO;
import com.atguigu.cloud.resp.ResultData;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

@RestController
public class OrderController {

    public static final String PaymentSrv_URL = "http://localhost:8001";// 先写死,硬编码

    @Resource
    private RestTemplate restTemplate;

    /**
     * 一般情况下,通过浏览器的地址栏输入url,发送的只能是get请求
     * 我们底层调用的是post方法,模拟消费者发送get请求,客户端消费者
     * 参数可以不添加@RequestBody
     *
     * @param payDTO
     * @return
     */
    @GetMapping(value = "/consumer/pay/add")
    public ResultData addOrder(PayDTO payDTO) {
        return restTemplate.postForObject(PaymentSrv_URL + "/pay/add", payDTO, ResultData.class);
    }

    @GetMapping(value = "/consumer/pay/get/{id}")
    public ResultData getPayInfo(@PathVariable("id") Integer id) {
        return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/" + id, ResultData.class, id);
    }

    @GetMapping(value = "/consumer/pay/update")
    public void update(PayDTO payDTO) {
        restTemplate.put(PaymentSrv_URL + "/pay/update", payDTO);
    }

    @GetMapping(value = "/consumer/pay/del/{id}")
    public void deletePay(@PathVariable("id") Integer id) {
        restTemplate.delete(PaymentSrv_URL + "/pay/del/" + id);
    }

}

每一个微服务都加入SpringBoot启动中

在这里插入图片描述

4.3、优化工程 commons

新建一个通用模块 cloud-api-commons 存放整个系统中共用的组件/api/接口/工具类等

在这里插入图片描述

修改POM文件

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

    <artifactId>cloud-api-commons</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--SpringBoot通用依赖模块-->
        <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.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
    </dependencies>

</project>

将通用组件放入 commons 模块中

在这里插入图片描述

通过 maven 打成 jar 包

在这里插入图片描述

订单80模块和支付8001模块引入该 Maven 坐标

<!--引入自定义的api通用包-->
<dependency>
    <groupId>com.atguigu.cloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

当前项目结构如下:

在这里插入图片描述

4.4、遗留硬编码问题,开始引入微服务

在80模块中有如下代码:

在这里插入图片描述

微服务所在的IP地址和端口号硬编码到订单微服务中,会存在非常多的问题

(1)如果订单微服务和支付微服务的IP地址或者端口号发生了变化,则支付微服务将变得不可用,需要同步修改订单微服务中调用支付微服务的IP地址和端口号。

(2)如果系统中提供了多个订单微服务和支付微服务,则无法实现微服务的负载均衡功能。

(3)如果系统需要支持更高的并发,需要部署更多的订单微服务和支付微服务,硬编码订单微服务则后续的维护会变得异常复杂。

所以,在微服务开发的过程中,需要引入服务治理功能,实现微服务之间的动态注册与发现,从此刻开始正式进入SpringCloud实战。

二、SpringCloud 微服务

1、Consul 服务注册与发现

官网地址:https://consul.io/

1.1、Consul 概念

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

提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。它具有很多优点。包括: 基于 raft 协议,比较简洁;支持健康检查, 同时支持 HTTP 和 DNS 协议;支持跨数据中心的 WAN 集群;提供图形界面;跨平台,支持 Linux、Mac、Windows。

在这里插入图片描述

Spring Cloud Consul 功能:

  • 服务发现:实例可以向 Consul 代理注册,客户端可以使用 Spring 管理的 bean 发现实例
  • 支持 Spring Cloud LoadBalancer - Spring Cloud 项目提供的客户端负载均衡器
  • 通过 Spring Cloud Gateway 支持 API 网关、动态路由器和过滤器
  • 分布式配置:使用 Consul Key/Value 存储
  • 控制总线:使用 Consul Events 的分布式控制事件

1.2、下载安装运行

下载地址:https://developer.hashicorp.com/consul/install

在这里插入图片描述

在这里插入图片描述

使用开发模式启动 Consul:consul agent -dev

在这里插入图片描述

启动成功后可以访问 Consul 的首页:http://localhost:8500

在这里插入图片描述

1.3、服务提供者8001和服务消费者80入驻 Consul

引入POM坐标

<!--SpringCloud consul discovery-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    <!--处理警告-->
    <exclusions>
        <exclusion>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

YML

在这里插入图片描述

在这里插入图片描述

在主启动类上添加注解

@EnableDiscoveryClient  //该注解用于向使用consul为注册中心时注册服务

服务注册成功

在这里插入图片描述

解决微服务80硬编码问题

//public static final String PaymentSrv_URL = "http://localhost:8001";//先写死,硬编码

public static final String PaymentSrv_URL = "http://cloud-payment-service";//服务注册中心上的微服务名称

测试的时候会出现bug,原因是 Consul 默认微服务具有负载均衡

在这里插入图片描述

给 RestTemplateConfig 加上 @LoadBalanced 注解即可

在这里插入图片描述

1.4、三个注册中心异同点(涉及CAP)

在这里插入图片描述

CAP理论是指计算机分布式系统的三个核心特性:一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)。

在这里插入图片描述

AP(Eureka):

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

当数据出现不一致时,虽然A, B上的注册信息不完全相同,但每个Eureka节点依然能够正常对外提供服务,这会出现查询服务信息时如果请求A查不到,但请求B就能查到。如此保证了可用性但牺牲了一致性结论:违背了一致性C的要求,只满足可用性和分区容错,即AP。

在这里插入图片描述

CP(Zookeeper/Consul):

当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性,Consul 遵循CAP原理中的CP原则,保证了强一致性和分区容错性,且使用的是Raft算法,比zookeeper使用的Paxos算法更加简单。虽然保证了强一致性,但是可用性就相应下降了,例如服务注册的时间会稍长一些,因为 Consul 的 raft 协议要求必须过半数的节点都写入成功才认为注册成功 ;在leader挂掉了之后,重新选举出leader之前会导致Consul 服务不可用。结论:违背了可用性A的要求,只满足一致性和分区容错,即CP。

在这里插入图片描述

1.5、服务配置与刷新

(1)分布式配置

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。比如某些配置文件中的内容大部分都是相同的,只有个别的配置项不同。就拿数据库配置来说吧,如果每个微服务使用的技术栈都是相同的,则每个微服务中关于数据库的配置几乎都是相同的,有时候主机迁移了,我希望一次修改,处处生效。

  • POM坐标

    <!--SpringCloud consul config-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-consul-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    
  • YML配置规则

    在这里插入图片描述

    必须在 consul 中的相应键中设置 YAML data。使用上述键的默认值,其形式如下:

    在这里插入图片描述

  • 新增配置文件 bootstrap.yml

    applicaiton.yml是用户级的资源配置项
    bootstrap.yml是系统级的,优先级更加高
    
    Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的`Application Context`的父上下文。初始化的时候,`Bootstrap Context`负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的`Environment`。
    
    `Bootstrap`属性有高优先级,默认情况下,它们不会被本地配置覆盖。 `Bootstrap context`和`Application Context`有着不同的约定,所以新增了一个`bootstrap.yml`文件,保证`Bootstrap Context`和`Application Context`配置的分离。
    
     application.yml文件改为bootstrap.yml,这是很关键的或者两者共存。
    因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml
    

    bootstrap.yml:

    spring:
      application:
        name: cloud-payment-service
        ####Spring Cloud Consul for Service Discovery
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            service-name: ${spring.application.name}
          config:
            profile-separator: '-' # default value is ",",we update '-'
            format: YAML
    
    # config/cloud-payment-service/data
    #       /cloud-payment-service-dev/data
    #       /cloud-payment-service-prod/data
    

    applicaiton.yml

    server:
      port: 8001
    
    # ==========applicationName + druid-mysql8 driver===================
    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
        username: root
        password: 123456
      profiles:
        active: dev # 多环境配置加载内容dev/prod,不写就是默认default配置
    
    # ========================mybatis===================
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: com.atguigu.cloud.entities
      configuration:
        map-underscore-to-camel-case: true
    
  • Consul服务器 key/value 配置填写

    参考规则:
    config/cloud-payment-service/data
          /cloud-payment-service-dev/data
          /cloud-payment-service-prod/data
    

    在Consul客户端创建config文件夹,以 / 结尾

    在这里插入图片描述

    在config文件夹下创建其他3个文件夹,以 / 结尾

    在这里插入图片描述

    在上述3个文件夹下分别创建data内容,data不再是文件夹

    在这里插入图片描述

    在Controller中测试,读取Consul中存放的信息

    @Value("${server.port}")
    private String port;
    
    @GetMapping(value = "/pay/get/info")
    private String getInfoByConsul(@Value("${atguigu.info}") String atguiguInfo)
    {
        return "atguiguInfo: " + atguiguInfo + "\t" + "port: " + port;
    }
    

    在这里插入图片描述

    在这里插入图片描述


(2)及时动态刷新

在这里插入图片描述

其实可以发现,如果在Consul的dev配置分支修改了内容马上访问,有时会读取不到信息,所以需要配置动态刷新。可以在主启动类上加入注解 @RefreshScope 配置动态刷新,默认刷新时间为55秒。

在这里插入图片描述

需要更快速的刷新可以在配置中修改

在这里插入图片描述

(3)Consul 配置持久化

服务配置和动态刷新已全部通过,但是当重启Consul后,之前的配置会全部消失,所以需要进行Consul的配置持久化。

Consul数据持久化配置并注册为Windows服务:

  • 在consul文件目录下新建空文件夹mydata和文件consul_start.bat

  • consul_start.bat内容信息:

    @echo.服务启动......  
    @echo off  
    @sc create Consul binpath= "{consul路径}\consul.exe agent -server -ui -bind=127.0.0.1 -client=0.0.0.0 -bootstrap-expect  1  -data-dir {consul路径}\mydata   "
    @net start Consul
    @sc config Consul start= AUTO  
    @echo.Consul start is OK......success
    @pause
    
  • 右键管理员权限打开

    在这里插入图片描述

  • 启动结果

    在这里插入图片描述

  • win后台

    在这里插入图片描述

  • 后续consul的配置数据会保存进mydata文件夹,实现了consul数据配置持久化。

2、LoadBalancer 负载均衡服务调用

官网:https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/loadbalancer.html

2.1、LoadBalancer 概念

LB负载均衡(Load Balance)是什么

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

What is a load Balancer and its Types? | Cloud4U

spring-cloud-starter-loadbalancer组件是什么

Spring Cloud LoadBalancer是由SpringCloud官方提供的一个开源的、简单易用的客户端负载均衡器,它包含在SpringCloud-commons中用它来替换了以前的Ribbon组件。相比较于Ribbon,SpringCloud LoadBalancer不仅能够支持RestTemplate,还支持WebClient(WeClient是Spring Web Flux中提供的功能,可以实现响应式异步请求)

2.2、客户端负载VS服务器负载

loadbalancer本地负载均衡客户端 VS Nginx服务端负载均衡区别

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

loadbalancer本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

2.3、80轮询访问8001,8002

在这里插入图片描述

LoadBalancer 在工作时分成两步:

第一步,先选择ConsulServer从服务端查询并拉取服务列表,知道了它有多个服务(上图3个服务),这3个实现是完全一样的,

默认轮询调用谁都可以正常执行。类似生活中求医挂号,某个科室今日出诊的全部医生,客户端你自己选一个。

第二步,按照指定的负载均衡策略从server取到的服务注册列表中由客户端自己选择一个地址,所以LoadBalancer是一个客户端的负载均衡器。

复制一个8001创建8002服务,都注册进Consul中

在这里插入图片描述

订单80新增LoadBalancer组件

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

修改Controller

@GetMapping(value = "/consumer/pay/get/info")
private String getInfoByConsul() {
	return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/info", String.class);
}

访问 http://localhost/consumer/pay/get/info 测试,交替访问到了8001和8002

在这里插入图片描述

2.4、loadbalancer 负载均衡解析

编码使用DiscoveryClient动态获取所有上线的服务列表

在这里插入图片描述

修改80微服务Controller

@Resource
private DiscoveryClient discoveryClient;

@GetMapping("/consumer/discovery")
public String discovery()
{
    List<String> services = discoveryClient.getServices();
    for (String element : services) {
        System.out.println(element);
    }

    System.out.println("===================================");

    List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");
    for (ServiceInstance element : instances) {
        System.out.println(element.getServiceId()+"\t"+element.getHost()+"\t"+element.getPort()+"\t"+element.getUri());
    }

    return instances.get(0).getServiceId()+":"+instances.get(0).getPort();
}

在这里插入图片描述

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

List instances = discoveryClient.getInstances(“cloud-payment-service”);

如: List [0] instances = 127.0.0.1:8002

List [1] instances = 127.0.0.1:8001

8001+ 8002 组合成为集群,它们共计2台机器,集群总数为2, 按照轮询算法原理:

当总请求数为1时: 1 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001

当总请求数位2时: 2 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002

当总请求数位3时: 3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001

当总请求数位4时: 4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002

如此类推…

2.5、负载算法切换

在这里插入图片描述

算法切换,轮询改为随机

@Configuration
@LoadBalancerClient(
        //下面的value值大小写一定要和consul里面的名字一样,必须一样
        value = "cloud-payment-service",configuration = RestTemplateConfig.class)
public class RestTemplateConfig
{
    @Bean
    @LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);

        return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

3、OpenFeign 服务接口调用

官网:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign

GitHub:https://github.com/spring-cloud/spring-cloud-openfeign

3.1、OpenFeign 概念

在这里插入图片描述

OpenFeign能干什么

前面在使用SpringCloud LoadBalancer+RestTemplate时,利用RestTemplate对http请求的封装处理形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,OpenFeign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在OpenFeign的实现下,我们只需创建一个接口并使用注解的方式来配置它(在一个微服务接口上面标注一个***@FeignClient***注解即可),即可完成对服务提供方的接口绑定,统一对外暴露可以被调用的接口方法,大大简化和降低了调用客户端的开发量,也即由服务提供者给出调用接口清单,消费者直接通过OpenFeign调用即可。

OpenFeign同时还集成SpringCloud LoadBalancer

可以在使用OpenFeign时提供Http客户端的负载均衡,也可以集成阿里巴巴Sentinel来提供熔断、降级等功能。而与SpringCloud LoadBalancer不同的是,通过OpenFeign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

在这里插入图片描述

3.2、使用方法

在这里插入图片描述

新建一个消费者 Module:cloud-consumer-feign-order80 用来体验OpenFeign

  • POM引入依赖
<!--openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • YML
server:
  port: 80

spring:
  application:
    name: cloud-consumer-openfeign-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
  • 主启动类
package com.atguigu.cloud;

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

@SpringBootApplication
@EnableDiscoveryClient  //该注解用于向使用consul为注册中心时注册服务
@EnableFeignClients     //启用feign客户端,定义服务+绑定接口,以声明式的方法优雅而简单的实现服务调用
public class MainOpenFeign80 {
    public static void main(String[] args) {
        SpringApplication.run(MainOpenFeign80.class, args);
    }
}
  • 修改 cloud-api-commons 通用模块(FeignApi接口)

订单模块要去调用支付模块,订单和支付两个微服务,需要通过Api接口解耦,一般不要在订单模块写非订单相关的业务,自己的业务自己做+其它模块走 FeignApi接口 调用。

在这里插入图片描述

package com.atguigu.cloud.apis;

import com.atguigu.cloud.entities.PayDTO;
import com.atguigu.cloud.resp.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {

    @PostMapping(value = "/pay/add")
    ResultData addPay(@RequestBody PayDTO payDTO);

    @DeleteMapping(value = "/pay/del/{id}")
    ResultData<Integer> deletePay(@PathVariable("id") Integer id);

    @PutMapping(value = "/pay/update")
    ResultData<String> updatePay(@RequestBody PayDTO payDTO);

    @GetMapping(value = "/pay/get/{id}")
    ResultData getPayInfo(@PathVariable("id") Integer id);

    @GetMapping(value = "/pay/get/info")
    String mylb();

}

在Controller直接通过Feign接口调用订单微服务

package com.atguigu.cloud.controller;

import com.atguigu.cloud.apis.PayFeignApi;
import com.atguigu.cloud.entities.PayDTO;
import com.atguigu.cloud.resp.ResultData;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;

@RestController
public class OrderController {

    @Resource
    private PayFeignApi payFeignApi;

    @PostMapping("/feign/pay/add")
    public ResultData addOrder(@RequestBody PayDTO payDTO) {
        System.out.println("第一步:模拟本地addOrder新增订单成功(省略sql操作),第二步:再开启addPay支付微服务远程调用");
        ResultData resultData = payFeignApi.addPay(payDTO);
        return resultData;
    }

    @GetMapping("/feign/pay/get/{id}")
    public ResultData getPayInfo(@PathVariable("id") Integer id) {
        System.out.println("-------支付微服务远程调用,按照id查询订单支付流水信息");
        ResultData resultData = payFeignApi.getPayInfo(id);
        return resultData;
    }

    /**
     * openfeign天然支持负载均衡演示
     *
     * @return
     */
    @GetMapping(value = "/feign/pay/mylb")
    public String mylb() {
        return payFeignApi.mylb();
    }

}

在这里插入图片描述

测试成功实现负载均衡,OpenFeign 默认集成了 LoadBalancer

在这里插入图片描述

3.3、OpenFeign 高级特性

在这里插入图片描述

(1)超时控制

在这里插入图片描述

默认OpenFeign客户端等待60秒钟,但是服务端处理超过规定时间会导致Feign客户端返回报错。

为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制,默认60秒太长或者业务时间太短都不好。

在这里插入图片描述

测试超时情况

在这里插入图片描述

在这里插入图片描述

yml文件中开启全局配置:

connectTimeout 连接超时时间

readTimeout 请求处理超时时间

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            #连接超时时间
                      connectTimeout: 3000
            #读取超时时间
                     readTimeout: 3000

设置指定配置:

在这里插入图片描述

(2)重试机制

在这里插入图片描述

新增配置类 FeignConfig,并修改 Retryer 配置

package com.atguigu.cloud.config;

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

@Configuration
public class FeignConfig {

    @Bean
    public Retryer myRetryer() {

        // return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的

        //最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
        return new Retryer.Default(100,1,3);
    }

}

(3)性能优化 HttpClient5

OpenFeign中http client

如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求,由于默认HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最牛B的,所以加到最大。

在这里插入图片描述

导入POM依赖

<!-- httpclient5-->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hc5</artifactId>
    <version>13.1</version>
</dependency>

YML

#  Apache HttpClient5 配置开启
spring:
  cloud:
    openfeign:
      httpclient:
        hc5:
          enabled: true

替换为 HttpClient5 后

在这里插入图片描述

(4)请求/响应压缩

在这里插入图片描述

对请求和响应进行GZIP压缩

Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。

通过下面的两个参数设置,就能开启请求与相应的压缩功能:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.response.enabled=true

细粒度化设置

对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限,

只有超过这个大小的请求才会进行压缩:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发压缩数据类型

spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小

YML

server:
  port: 80

spring:
  application:
    name: cloud-consumer-openfeign-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      client:
        config:
          default:
          #cloud-payment-service:
            #连接超时时间
                        connectTimeout: 4000
            #读取超时时间
                        readTimeout: 4000
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          min-request-size: 2048 #最小触发压缩的大小
          mime-types: text/xml,application/xml,application/json #触发压缩数据类型
        response:
          enabled: true
(5)日志打印

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

日志级别

NONE:默认的,不显示任何日志;

BASIC:仅记录请求方法、URL、响应状态码及执行时间;

HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;

FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。

在这里插入图片描述

在FeignConfig手动开启日志

package com.atguigu.cloud.config;

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

@Configuration
public class FeignConfig {

    @Bean
    public Retryer myRetryer() {

        // return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的

        //最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
        return new Retryer.Default(100,1,3);
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

}

YML需要开启日志的Feign客户端

在这里插入图片描述

在这里插入图片描述

带着压缩调用信息

在这里插入图片描述

3次重试触发效果

在这里插入图片描述

4、CircuitBreaker 断路器

4.1、服务雪崩

分布式系统面临的问题

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

在这里插入图片描述

服务雪崩

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

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

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

处理问题:禁止服务雪崩故障

解决:有问题的节点,快速熔断(快速返回失败处理或者返回默认兜底数据【服务降级】)。

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

为了避免整个系统大面积故障,需要搞定:

  • 服务熔断:类比保险丝,闭合状态(CLOSE)可以正常使用,当达到最大服务访问后,直接拒绝访问跳闸限电(OPEN),此刻调用方会接受服务降级的处理并返回友好兜底提示

  • 服务降级:不让客户端等待并立刻返回一个友好提示

在这里插入图片描述

  • 服务限流

  • 服务限时

  • 服务预热

  • 接近实时的监控

  • 兜底的处理动作

4.2、CircuitBreaker 概念

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

CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。

当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。

在这里插入图片描述

而 Circuit Breaker 只是一套规范和接口,落地实现者是 Resilience4J

4.3、Resilience4J

中文手册:https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/index.md

在这里插入图片描述

在这里插入图片描述

4.4、熔断(CircuitBreaker)

中文手册:https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/CircuitBreaker.md

(1)断路器三大状态之间的转换

在这里插入图片描述

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

在这里插入图片描述

(2)配置参数

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

精简版

failure-rate-threshold以百分比配置失败率峰值
sliding-window-type断路器的滑动窗口期类型 可以基于“次数”(COUNT_BASED)或者“时间”(TIME_BASED)进行熔断,默认是COUNT_BASED。
sliding-window-size若COUNT_BASED,则10次调用中有50%失败(即5次)打开熔断断路器;若为TIME_BASED则,此时还有额外的两个设置属性,含义为:在N秒内(sliding-window-size)100%(slow-call-rate-threshold)的请求超过N秒(slow-call-duration-threshold)打开断路器。
slowCallRateThreshold以百分比的方式配置,断路器把调用时间大于slowCallDurationThreshold的调用视为慢调用,当慢调用比例大于等于峰值时,断路器开启,并进入服务降级。
slowCallDurationThreshold配置调用时间的峰值,高于该峰值的视为慢调用。
permitted-number-of-calls-in-half-open-state运行断路器在HALF_OPEN状态下时进行N次调用,如果故障或慢速调用仍然高于阈值,断路器再次进入打开状态。
minimum-number-of-calls在每个滑动窗口期样本数,配置断路器计算错误率或者慢调用率的最小调用数。比如设置为5意味着,在计算故障率之前,必须至少调用5次。如果只记录了4次,即使4次都失败了,断路器也不会进入到打开状态。
wait-duration-in-open-state从OPEN到HALF_OPEN状态需要等待的时间
(3)实战演示
  • 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
  • 等待5秒后,CircuitBreaker将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
  • 如还是异常CircuitBreaker将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。

在这里插入图片描述

【1】COUNT_BASED(计数的滑动窗口)

修改cloud-provider-payment8001,新建PayCircuitController

package com.atguigu.cloud.controller;

import cn.hutool.core.util.IdUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
public class PayCircuitController {

    //=========Resilience4j CircuitBreaker 的例子
    @GetMapping(value = "/pay/circuit/{id}")
    public String myCircuit(@PathVariable("id") Integer id)
    {
        if(id == -4) throw new RuntimeException("----circuit id 不能负数");
        if(id == 9999){
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        return "Hello, circuit! inputId:  "+id+" \t " + IdUtil.simpleUUID();
    }

}

修改PayFeignApi接口

/**
* Resilience4j CircuitBreaker 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/circuit/{id}")
public String myCircuit(@PathVariable("id") Integer id);

修改cloud-consumer-feign-order80

  • POM

    <!--resilience4j-circuitbreaker-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
    </dependency>
    <!-- 由于断路保护等需要AOP实现,所以必须导入AOP包 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  • YML

    server:
      port: 80
    
    spring:
      application:
        name: cloud-consumer-openfeign-order
      ####Spring Cloud Consul for Service Discovery
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            prefer-ip-address: true #优先使用服务ip进行注册
            service-name: ${spring.application.name}
    
        openfeign:
          client:
            config:
              default:
                #连接超时时间
                connectTimeout: 3000
                #读取超时时间
                readTimeout: 3000
          httpclient:
            hc5:
              enabled: true
          compression:
            request:
              enabled: true
              min-request-size: 2048 #最小触发压缩的大小
              mime-types: text/xml,application/xml,application/json #触发压缩数据类型
            response:
              enabled: true
          # 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled
          circuitbreaker:
            enabled: true
            group:
              enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
    
    # feign日志以什么级别监控哪个接口
    logging:
      level:
        com:
          atguigu:
            cloud:
              apis:
                PayFeignApi: debug
    
    # Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
    #  6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
    #  等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
    #  如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
    resilience4j:
      circuitbreaker:
        configs:
          default:
            failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
            slidingWindowType: COUNT_BASED # 滑动窗口的类型
            slidingWindowSize: 6 #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
            minimumNumberOfCalls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
            automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
            waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
            permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。
            recordExceptions:
              - java.lang.Exception
        instances:
          cloud-payment-service:
            baseConfig: default
    
  • 新建OrderCircuitController

    package com.atguigu.cloud.controller;
    
    import com.atguigu.cloud.apis.PayFeignApi;
    import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
    import jakarta.annotation.Resource;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class OrderCircuitController {
    
        @Resource
        private PayFeignApi payFeignApi;
    
        @GetMapping(value = "/feign/pay/circuit/{id}")
        @CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
        public String myCircuitBreaker(@PathVariable("id") Integer id) {
            return payFeignApi.myCircuit(id);
        }
    
        // myCircuitFallback就是服务降级后的兜底处理方法
        public String myCircuitFallback(Integer id, Throwable t) {
            // 这里是容错处理逻辑,返回备用结果
            return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
        }
    
    }
    
    

测试:

服务正常使用

在这里插入图片描述

当服务出现异常,实现了服务降级提示

在这里插入图片描述

交替访问两个URL,6次中造成了3次即50%的调用失败,CircuitBreaker变为OPEN状态,当正常使用时服务也已经熔断了

在这里插入图片描述

5s后 CircuitBreaker 变为 HALF_OPEN 状态,又能允许2个请求,如果全部正常则回到CLOSE状态,只要有一个异常,重新进入OPEN状态

在这里插入图片描述

【2】TIME_BASED(时间的滑动窗口)

在这里插入图片描述

改YML

server:
  port: 80

spring:
  application:
    name: cloud-consumer-openfeign-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}

    openfeign:
      client:
        config:
          default:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          min-request-size: 2048 #最小触发压缩的大小
          mime-types: text/xml,application/xml,application/json #触发压缩数据类型
        response:
          enabled: true
      # 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled
      circuitbreaker:
        enabled: true
        group:
          enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后

# Resilience4j CircuitBreaker 按照时间:TIME_BASED 的例子
resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 10s #神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
        slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例。
        slowCallRateThreshold: 30 #慢调用百分比峰值,断路器把调用时间⼤于slowCallDurationThreshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级
        slidingWindowType: TIME_BASED # 滑动窗口的类型
        slidingWindowSize: 2 #滑动窗口的大小配置,配置TIME_BASED表示2秒
        minimumNumberOfCalls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
        permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。
        waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
        recordExceptions:
          - java.lang.Exception
    instances:
      cloud-payment-service:
      	baseConfig: default

为了避免影响实验效果,关闭FeignConfig重试3次

在这里插入图片描述

测试:

在这里插入图片描述

4.5、隔离(BulkHead)

中文手册:https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/bulkhead.md

在这里插入图片描述

在这里插入图片描述

(1)实现SemaphoreBulkhead(信号量舱壁)

在这里插入图片描述

信号量舱壁(SemaphoreBulkhead)原理

当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。

当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,

如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。

若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。

源码 io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead

在这里插入图片描述

修改8001服务的 PayCircuitController

    //=========Resilience4j bulkhead 的例子
    @GetMapping(value = "/pay/bulkhead/{id}")
    public String myBulkhead(@PathVariable("id") Integer id) {
        if (id == -4) throw new RuntimeException("----bulkhead id 不能-4");

        if (id == 9999) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return "Hello, bulkhead! inputId:  " + id + " \t " + IdUtil.simpleUUID();
    }

PayFeignApi 接口新增舱壁api方法

/**
 * Resilience4j Bulkhead 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id);

修改cloud-consumer-feign-order-80

  • POM

    <!--resilience4j-bulkhead-->
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-bulkhead</artifactId>
    </dependency>
    
  • YML

    在这里插入图片描述

    server:
      port: 80
    
    spring:
      application:
        name: cloud-consumer-openfeign-order
      ####Spring Cloud Consul for Service Discovery
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            prefer-ip-address: true #优先使用服务ip进行注册
            service-name: ${spring.application.name}
    
        openfeign:
          client:
            config:
              default:
                #连接超时时间
                connectTimeout: 3000
                #读取超时时间
                readTimeout: 3000
          httpclient:
            hc5:
              enabled: true
          compression:
            request:
              enabled: true
              min-request-size: 2048 #最小触发压缩的大小
              mime-types: text/xml,application/xml,application/json #触发压缩数据类型
            response:
              enabled: true
          # 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled
          circuitbreaker:
            enabled: true
            group:
              enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
            
    ####resilience4j bulkhead 的例子
    resilience4j:
      bulkhead:
        configs:
          default:
            maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
            maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
        instances:
          cloud-payment-service:
            baseConfig: default
      timelimiter:
        configs:
          default:
            timeout-duration: 20s
    
  • OrderCircuitController

        /**
         * (船的)舱壁,隔离
         *
         * @param id
         * @return
         */
        @GetMapping(value = "/feign/pay/bulkhead/{id}")
        @Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadFallback", type = Bulkhead.Type.SEMAPHORE)
        public String myBulkhead(@PathVariable("id") Integer id) {
            return payFeignApi.myBulkhead(id);
        }
    
        public String myBulkheadFallback(Throwable t) {
            return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
        }
    

测试:

在这里插入图片描述

(2)实现FixedThreadPoolBulkhead(固定线程池舱壁)

在这里插入图片描述

固定线程池舱壁(FixedThreadPoolBulkhead)

FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。

当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。

当线程池中无空闲时时,接下来的请求将进入等待队列,若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。

另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法。

源码 io.github.resilience4j.bulkhead.internal.FixedThreadPoolBulkhead

在这里插入图片描述

在这里插入图片描述

修改cloud-consumer-feign-order80

  • POM

    <!--resilience4j-bulkhead-->
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-bulkhead</artifactId>
    </dependency>
    
  • YML

    在这里插入图片描述

    server:
      port: 80
    
    spring:
      application:
        name: cloud-consumer-openfeign-order
      ####Spring Cloud Consul for Service Discovery
      cloud:
        consul:
          host: localhost
          port: 8500
          discovery:
            prefer-ip-address: true #优先使用服务ip进行注册
            service-name: ${spring.application.name}
    
        openfeign:
          client:
            config:
              default:
                #连接超时时间
                connectTimeout: 3000
                #读取超时时间
                readTimeout: 3000
          httpclient:
            hc5:
              enabled: true
          compression:
            request:
              enabled: true
              min-request-size: 2048 #最小触发压缩的大小
              mime-types: text/xml,application/xml,application/json #触发压缩数据类型
            response:
              enabled: true
          # 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled
          circuitbreaker:
            enabled: true
    #        group:
    #          enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
    #        演示Bulkhead.Type.THREADPOOL时spring.cloud.openfeign.circuitbreaker.group.enabled设为false新启线程和原来主线程脱离了。
    
    
    ####resilience4j bulkhead -THREADPOOL的例子
    resilience4j:
      timelimiter:
        configs:
          default:
            timeout-duration: 10s #timelimiter默认限制远程1s,超过报错不好演示效果所以加上10秒
      thread-pool-bulkhead:
        configs:
          default:
            core-thread-pool-size: 1
            max-thread-pool-size: 1
            queue-capacity: 1
        instances:
          cloud-payment-service:
            baseConfig: default
    # spring.cloud.openfeign.circuitbreaker.group.enabled 请设置为false 新启线程和原来主线程脱离
    

    在这里插入图片描述

    在这里插入图片描述

  • OrderCircuitController

    package com.atguigu.cloud.controller;
    
    import com.atguigu.cloud.apis.PayFeignApi;
    import io.github.resilience4j.bulkhead.annotation.Bulkhead;
    import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
    import jakarta.annotation.Resource;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.TimeUnit;
    
    @RestController
    public class OrderCircuitController {
    
        @Resource
        private PayFeignApi payFeignApi;
    
        @GetMapping(value = "/feign/pay/circuit/{id}")
        @CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
        public String myCircuitBreaker(@PathVariable("id") Integer id) {
            return payFeignApi.myCircuit(id);
        }
    
        // myCircuitFallback就是服务降级后的兜底处理方法
        public String myCircuitFallback(Integer id, Throwable t) {
            // 这里是容错处理逻辑,返回备用结果
            return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
        }
    
    
        /**
         * (船的)舱壁,隔离
         *
         * @param id
         * @return
         */
        // @GetMapping(value = "/feign/pay/bulkhead/{id}")
        // @Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadFallback", type = Bulkhead.Type.SEMAPHORE)
        // public String myBulkhead(@PathVariable("id") Integer id) {
        //     return payFeignApi.myBulkhead(id);
        // }
        //
        // public String myBulkheadFallback(Throwable t) {
        //     return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
        // }
    
        /**
         * (船的)舱壁,隔离,THREADPOOL
         *
         * @param id
         * @return
         */
        @GetMapping(value = "/feign/pay/bulkhead/{id}")
        @Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadPoolFallback", type = Bulkhead.Type.THREADPOOL)
        public CompletableFuture<String> myBulkheadTHREADPOOL(@PathVariable("id") Integer id) {
            System.out.println(Thread.currentThread().getName() + "\t" + "enter the method!!!");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "exist the method!!!");
    
            return CompletableFuture.supplyAsync(() -> payFeignApi.myBulkhead(id) + "\t" + " Bulkhead.Type.THREADPOOL");
        }
    
        public CompletableFuture<String> myBulkheadPoolFallback(Integer id, Throwable t) {
            return CompletableFuture.supplyAsync(() -> "Bulkhead.Type.THREADPOOL,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
        }
    
    }
    
    

测试:

在这里插入图片描述

4.6、限流(RateLimiter)

中文手册:https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/ratelimiter.md

在这里插入图片描述

(1)漏斗算法(Leaky Bucket)

在这里插入图片描述

在这里插入图片描述

(2)令牌桶算法(Token Bucket)

SpringCloud默认使用该算法

在这里插入图片描述

(3)滚动时间窗(tumbling time window)

在这里插入图片描述

在这里插入图片描述

(4)滑动时间窗口(sliding time window)

在这里插入图片描述

(5)实战演示

8001 服务修改 PayCircuitController 新增方法

package com.atguigu.cloud.controller;

import cn.hutool.core.util.IdUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
public class PayCircuitController {

    //=========Resilience4j CircuitBreaker 的例子
    @GetMapping(value = "/pay/circuit/{id}")
    public String myCircuit(@PathVariable("id") Integer id) {
        if (id == -4) throw new RuntimeException("----circuit id 不能负数");
        if (id == 9999) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return "Hello, circuit! inputId:  " + id + " \t " + IdUtil.simpleUUID();
    }

    //=========Resilience4j bulkhead 的例子
    @GetMapping(value = "/pay/bulkhead/{id}")
    public String myBulkhead(@PathVariable("id") Integer id) {
        if (id == -4) throw new RuntimeException("----bulkhead id 不能-4");

        if (id == 9999) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return "Hello, bulkhead! inputId:  " + id + " \t " + IdUtil.simpleUUID();
    }

    //=========Resilience4j ratelimit 的例子
    @GetMapping(value = "/pay/ratelimit/{id}")
    public String myRatelimit(@PathVariable("id") Integer id) {
        return "Hello, myRatelimit欢迎到来 inputId:  " + id + " \t " + IdUtil.simpleUUID();
    }

}

PayFeignApi 接口新增限流api方法

/**
 * Resilience4j Ratelimit 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id);

修改 cloud-consumer-feign-order80

  • POM

    <!--resilience4j-ratelimiter-->
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-ratelimiter</artifactId>
    </dependency>
    
  • YML

    在这里插入图片描述

    ####resilience4j ratelimiter 限流的例子
    resilience4j:
      ratelimiter:
        configs:
          default:
            limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
            limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
            timeout-duration: 1 # 线程等待权限的默认等待时间
        instances:
          cloud-payment-service:
            baseConfig: default
    
  • OrderCircuitController

    package com.atguigu.cloud.controller;
    
    import com.atguigu.cloud.apis.PayFeignApi;
    import io.github.resilience4j.bulkhead.annotation.Bulkhead;
    import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
    import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
    import jakarta.annotation.Resource;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.TimeUnit;
    
    @RestController
    public class OrderCircuitController {
    
        @Resource
        private PayFeignApi payFeignApi;
    
        @GetMapping(value = "/feign/pay/circuit/{id}")
        @CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
        public String myCircuitBreaker(@PathVariable("id") Integer id) {
            return payFeignApi.myCircuit(id);
        }
    
        // myCircuitFallback就是服务降级后的兜底处理方法
        public String myCircuitFallback(Integer id, Throwable t) {
            // 这里是容错处理逻辑,返回备用结果
            return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
        }
    
    
        /**
         * (船的)舱壁,隔离
         *
         * @param id
         * @return
         */
        // @GetMapping(value = "/feign/pay/bulkhead/{id}")
        // @Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadFallback", type = Bulkhead.Type.SEMAPHORE)
        // public String myBulkhead(@PathVariable("id") Integer id) {
        //     return payFeignApi.myBulkhead(id);
        // }
        //
        // public String myBulkheadFallback(Throwable t) {
        //     return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
        // }
    
        /**
         * (船的)舱壁,隔离,THREADPOOL
         *
         * @param id
         * @return
         */
        @GetMapping(value = "/feign/pay/bulkhead/{id}")
        @Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadPoolFallback", type = Bulkhead.Type.THREADPOOL)
        public CompletableFuture<String> myBulkheadTHREADPOOL(@PathVariable("id") Integer id) {
            System.out.println(Thread.currentThread().getName() + "\t" + "enter the method!!!");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "exist the method!!!");
    
            return CompletableFuture.supplyAsync(() -> payFeignApi.myBulkhead(id) + "\t" + " Bulkhead.Type.THREADPOOL");
        }
    
        public CompletableFuture<String> myBulkheadPoolFallback(Integer id, Throwable t) {
            return CompletableFuture.supplyAsync(() -> "Bulkhead.Type.THREADPOOL,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
        }
    
        @GetMapping(value = "/feign/pay/ratelimit/{id}")
        @RateLimiter(name = "cloud-payment-service", fallbackMethod = "myRatelimitFallback")
        public String myBulkhead(@PathVariable("id") Integer id) {
            return payFeignApi.myRatelimit(id);
        }
    
        public String myRatelimitFallback(Integer id, Throwable t) {
            return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
        }
    
    }
    
    

测试:

在这里插入图片描述

在这里插入图片描述

5、Sleuth (Micrometer) + ZipKin 分布式链路追踪

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

GitHub:https://github.com/spring-cloud/spring-cloud-sleuth

Sleuth 改头换面,未来的替代方案使用 Micrometer Tracing

在这里插入图片描述

在这里插入图片描述

5.1、分布式链路追踪概述

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

在这里插入图片描述

在分布式与微服务场景下,我们需要解决如下问题:

  • 在大规模分布式与微服务集群下,如何实时观测系统的整体调用链路情况。

  • 在大规模分布式与微服务集群下,如何快速发现并定位到问题。

  • 在大规模分布式与微服务集群下,如何尽可能精确的判断故障对系统的影响范围与影响程度。

  • 在大规模分布式与微服务集群下,如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。

  • 在大规模分布式与微服务集群下,如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。

  • 在大规模分布式与微服务集群下,如何尽可能精确的分析系统的存储瓶颈与容量规划。

上述问题就是我们的落地议题答案:

分布式链路追踪技术要解决的问题,分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

5.2、新一代 Sleuth:Micrometer

文档:https://docs.micrometer.io/micrometer/reference/

Sleuth 不支持 Spring3.x 版本,已迁移至 Micrometer 上去

Micrometer 提供了一套完整的分布式链路追踪(Distributed Tracing)解决方案并且兼容支持了 zipkin 展现

在这里插入图片描述

能将一次分布式请求还原成调用链路,进行日志记录和性能监控,并将一次分布式请求的调用情况集中web展示

行业内其他比较成熟的分布式链路追踪技术解决方案

在这里插入图片描述

5.3、分布式链路追踪原理

在这里插入图片描述

那么一条链路追踪会在每个服务调用的时候加上Trace ID 和 Span ID

链路通过TraceId唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来(Span:表示调用链路来源,通俗的理解span就是一次请求信息)

在这里插入图片描述

一图搞懂链路追踪

在这里插入图片描述

5.4、ZipKin 下载运行

官网:https://zipkin.io/

ZipKin概述

Zipkin是一种分布式链路跟踪系统图形化的工具,Zipkin 是 Twitter 开源的分布式跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调用链路信息展示到Web图形化界面上供开发人员分析,开发人员能够从ZipKin中分析出调用链路中的性能瓶颈,识别出存在问题的应用程序,进而定位问题和解决问题。

单有Sleuth(Micrometer)进行链路追踪

在这里插入图片描述

下载安装运行

https://zipkin.io/pages/quickstart.html

下载方式有三种:

在这里插入图片描述

下载地址:

在这里插入图片描述

启动:

在这里插入图片描述

访问成功 http://localhost:9411

在这里插入图片描述

5.5、Micrometer+ZipKin搭建链路监控案例

在这里插入图片描述

总体父工程 POM

<properties>
    <micrometer-tracing.version>1.2.0</micrometer-tracing.version>
    <micrometer-observation.version>1.12.0</micrometer-observation.version>
    <feign-micrometer.version>12.5</feign-micrometer.version>
    <zipkin-reporter-brave.version>2.17.0</zipkin-reporter-brave.version>
</properties>

<!--micrometer-tracing-bom导入链路追踪版本中心  1-->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bom</artifactId>
    <version>${micrometer-tracing.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<!--micrometer-tracing指标追踪  2-->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing</artifactId>
    <version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 3-->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
    <version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-observation 4-->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-observation</artifactId>
    <version>${micrometer-observation.version}</version>
</dependency>
<!--feign-micrometer 5-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-micrometer</artifactId>
    <version>${feign-micrometer.version}</version>
</dependency>
<!--zipkin-reporter-brave 6-->
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
    <version>${zipkin-reporter-brave.version}</version>
</dependency>

jar 包说明

在这里插入图片描述

服务提供者 8001

  • POM

            <!--micrometer-tracing指标追踪  1-->
            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-tracing</artifactId>
            </dependency>
            <!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-tracing-bridge-brave</artifactId>
            </dependency>
            <!--micrometer-observation 3-->
            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-observation</artifactId>
            </dependency>
            <!--feign-micrometer 4-->
            <dependency>
                <groupId>io.github.openfeign</groupId>
                <artifactId>feign-micrometer</artifactId>
            </dependency>
            <!--zipkin-reporter-brave 5-->
            <dependency>
                <groupId>io.zipkin.reporter2</groupId>
                <artifactId>zipkin-reporter-brave</artifactId>
            </dependency>
    
  • YML(application.yml)

    # ========================zipkin===================
    management:
      zipkin:
        tracing:
          endpoint: http://localhost:9411/api/v2/spans
      tracing:
        sampling:
          probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
    
  • 新建业务类 PayMicrometerController

    package com.atguigu.cloud.controller;
    
    import cn.hutool.core.util.IdUtil;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class PayMicrometerController {
        /**
         * Micrometer(Sleuth)进行链路监控的例子
         *
         * @param id
         * @return
         */
        @GetMapping(value = "/pay/micrometer/{id}")
        public String myMicrometer(@PathVariable("id") Integer id) {
            return "Hello, 欢迎到来myMicrometer inputId:  " + id + " \t    服务返回:" + IdUtil.simpleUUID();
        }
    }
    

Api 接口 PayFeignApi

package com.atguigu.cloud.apis;

import com.atguigu.cloud.entities.PayDTO;
import com.atguigu.cloud.resp.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {

    @PostMapping(value = "/pay/add")
    ResultData addPay(@RequestBody PayDTO payDTO);

    @DeleteMapping(value = "/pay/del/{id}")
    ResultData<Integer> deletePay(@PathVariable("id") Integer id);

    @PutMapping(value = "/pay/update")
    ResultData<String> updatePay(@RequestBody PayDTO payDTO);

    @GetMapping(value = "/pay/get/{id}")
    ResultData getPayInfo(@PathVariable("id") Integer id);

    @GetMapping(value = "/pay/get/info")
    String mylb();

    /**
     * Resilience4j CircuitBreaker 的例子
     * @param id
     * @return
     */
    @GetMapping(value = "/pay/circuit/{id}")
    String myCircuit(@PathVariable("id") Integer id);

    /**
     * Resilience4j Bulkhead 的例子
     * @param id
     * @return
     */
    @GetMapping(value = "/pay/bulkhead/{id}")
    String myBulkhead(@PathVariable("id") Integer id);

    /**
     * Resilience4j Ratelimit 的例子
     * @param id
     * @return
     */
    @GetMapping(value = "/pay/ratelimit/{id}")
    String myRatelimit(@PathVariable("id") Integer id);

    /**
     * Micrometer(Sleuth)进行链路监控的例子
     * @param id
     * @return
     */
    @GetMapping(value = "/pay/micrometer/{id}")
    String myMicrometer(@PathVariable("id") Integer id);

}

cloud-consumer-feign-order80

  • POM

        <!--micrometer-tracing指标追踪  1-->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-tracing</artifactId>
        </dependency>
        <!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-tracing-bridge-brave</artifactId>
        </dependency>
        <!--micrometer-observation 3-->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-observation</artifactId>
        </dependency>
        <!--feign-micrometer 4-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-micrometer</artifactId>
        </dependency>
        <!--zipkin-reporter-brave 5-->
        <dependency>
            <groupId>io.zipkin.reporter2</groupId>
            <artifactId>zipkin-reporter-brave</artifactId>
        </dependency>
    
  • YML

    # zipkin图形展现地址和采样率设置
    management:
      zipkin:
        tracing:
          endpoint: http://localhost:9411/api/v2/spans
      tracing:
        sampling:
          probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
    
  • 新建 OrderMicrometerController

    package com.atguigu.cloud.controller;
    
    import com.atguigu.cloud.apis.PayFeignApi;
    import jakarta.annotation.Resource;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @Slf4j
    public class OrderMicrometerController {
        @Resource
        private PayFeignApi payFeignApi;
    
        @GetMapping(value = "/feign/micrometer/{id}")
        public String myMicrometer(@PathVariable("id") Integer id) {
            return payFeignApi.myMicrometer(id);
        }
    }
    

测试:

启动8001/80两个微服务并注册进 Consul

访问 http://localhost/feign/micrometer/1

访问 ZipKin 客户端 http://localhost:9411

在这里插入图片描述

点击 Show 查看详情

在这里插入图片描述

查看依赖关系

在这里插入图片描述

6、GateWay 新一代网关

6.1、GateWay 概述

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

Gateway是在Spring生态系统之上构建的API网关服务,基于Spring6,Spring Boot 3和Project Reactor等技术。它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,并为它们提供跨领域的关注点,例如:安全性、监控/度量和恢复能力。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关SpringCloud Gateway替代Zuul,

那就是SpringCloud Gateway一句话:gateway是原zuul1.x版的替代。

在这里插入图片描述

微服务架构上的网关

在这里插入图片描述

Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心。

在这里插入图片描述

6.2、三大核心(路由、断言、过滤)

在这里插入图片描述

在这里插入图片描述

web 前端请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。

predicate 就是我们的匹配条件;

filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了

在这里插入图片描述

6.3、GateWay 工作流程

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

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

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

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

在这里插入图片描述

6.4、入门配置

在这里插入图片描述

  • 新建 Model 为 cloud-gateway9527

在这里插入图片描述

  • POM

     <dependencies>
            <!--gateway-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-consul-discovery</artifactId>
            </dependency>
            <!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
  • YML

    server:
      port: 9527
    
    spring:
      application:
        name: cloud-gateway #以微服务注册进consul或nacos服务列表内
      cloud:
        consul: #配置consul地址
          host: localhost
          port: 8500
          discovery:
            prefer-ip-address: true
            service-name: ${spring.application.name}
    
  • 主启动

    package com.atguigu.cloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient //服务注册和发现
    public class Main9527 {
        public static void main(String[] args) {
            SpringApplication.run(Main9527.class, args);
        }
    }
    
  • 不写任何业务代码,网关和业务无关

启动8500服务中心Consul,再启动9527网关入驻

在这里插入图片描述

6.5、路由映射(实现GateWay部署)

由于不想要直接暴露8001端口,需要在8001支付微服务外面套一层9527网关

在这里插入图片描述

8001中新建 PayGateWayController

package com.atguigu.cloud.controller;

import cn.hutool.core.util.IdUtil;
import com.atguigu.cloud.entities.Pay;
import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.service.PayService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PayGateWayController {
    @Resource
    PayService payService;

    @GetMapping(value = "/pay/gateway/get/{id}")
    public ResultData<Pay> getById(@PathVariable("id") Integer id) {
        Pay pay = payService.getById(id);
        return ResultData.success(pay);
    }

    @GetMapping(value = "/pay/gateway/info")
    public ResultData<String> getGatewayInfo() {
        return ResultData.success("gateway info test:" + IdUtil.simpleUUID());
    }
}

9527网关YML配置

server:
  port: 9527

spring:
  application:
    name: cloud-gateway #以微服务注册进consul或nacos服务列表内
  cloud:
    consul: #配置consul地址
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}
    gateway:
      routes:
        - id: pay_routh1 #pay_routh1                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001                #匹配后提供服务的路由地址
          predicates:
            - Path=/pay/gateway/get/**              # 断言,路径相匹配的进行路由


        - id: pay_routh2 #pay_routh2                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8001                #匹配后提供服务的路由地址
          predicates:
            - Path=/pay/gateway/info/**              # 断言,路径相匹配的进行路由

路由映射说明

在这里插入图片描述

修改 cloud-api-commons 的 PayFeignApi 接口

    /**
     * GateWay进行网关测试案例01
     * @param id
     * @return
     */
    @GetMapping(value = "/pay/gateway/get/{id}")
    public ResultData getById(@PathVariable("id") Integer id);

    /**
     * GateWay进行网关测试案例02
     * @return
     */
    @GetMapping(value = "/pay/gateway/info")
    public ResultData<String> getGatewayInfo();

服务 cloud-consumer-feign-order80 新建 OrderGateWayController

package com.atguigu.cloud.controller;

import com.atguigu.cloud.apis.PayFeignApi;
import com.atguigu.cloud.resp.ResultData;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderGateWayController {
    @Resource
    private PayFeignApi payFeignApi;

    @GetMapping(value = "/feign/pay/gateway/get/{id}")
    public ResultData getById(@PathVariable("id") Integer id) {
        return payFeignApi.getById(id);
    }

    @GetMapping(value = "/feign/pay/gateway/info")
    public ResultData<String> getGatewayInfo() {
        return payFeignApi.getGatewayInfo();
    }
}

Feign -> GateWay -> 8001 中间加了一层网关

在这里插入图片描述

测试:

有网关正常 success

在这里插入图片描述

无网关异常

在这里插入图片描述

6.6、高级特性

(1)解决 uri 地址写死

在这里插入图片描述

9527修改后YML

server:
  port: 9527

spring:
  application:
    name: cloud-gateway #以微服务注册进consul或nacos服务列表内
  cloud:
    consul: #配置consul地址
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}
    gateway:
      routes:
        - id: pay_routh1 #pay_routh1                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001                #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service          #匹配后提供服务的路由地址
          predicates:
            - Path=/pay/gateway/get/**              # 断言,路径相匹配的进行路由

        - id: pay_routh2 #pay_routh2                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
          #uri: http://localhost:8001                #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service                #匹配后提供服务的路由地址
          predicates:
            - Path=/pay/gateway/info/**              # 断言,路径相匹配的进行路由
(2)Predicate 断言

在这里插入图片描述

启动gateway9527服务时,后台的输出

在这里插入图片描述

整体架构

在这里插入图片描述

(3)两种配置方式(Shortcut 和 Fully Expanded)

在这里插入图片描述

Shortcut

在这里插入图片描述

Fully Expanded

在这里插入图片描述

(4)常用断言 Api
【1】After Route Predicate

网关限制在某个时间点之后才能访问服务

在这里插入图片描述

可以通过 ZonedDateTime 获取对应的时间格式

package com.atguigu.cloud;

import java.time.ZonedDateTime;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
        System.out.println(zbj);
    }
}

在这里插入图片描述

配置9527的YML

在这里插入图片描述

在该时间前无法访问服务

在这里插入图片描述

到了时间后才访问成功

在这里插入图片描述

【2】Before Route Predicate

网关限制在某个时间点之前才能访问服务

在这里插入图片描述

在这里插入图片描述

在指定时间之前访问成功

在这里插入图片描述

在指定时间之后无法访问服务

在这里插入图片描述

【3】Between Route Predicate

网关限制在两个指定的时间点之间才能访问服务

在这里插入图片描述

在这里插入图片描述

在两个时间之间时访问成功

在这里插入图片描述

在两个时间之外时无法访问服务

在这里插入图片描述

【4】Cookie Route Predicate

访问服务时要带上指定的 Cookie 才能访问

在这里插入图片描述

在这里插入图片描述

原生命令方式访问

在这里插入图片描述

postman方式访问

在这里插入图片描述

Chrome浏览器访问

在这里插入图片描述

【5】Header Route Predicate

通过指定的请求头中的属性名和正则表达式匹配执行

在这里插入图片描述

在这里插入图片描述

原生方式访问

在这里插入图片描述

postman方式访问

在这里插入图片描述

【6】Host Route Predicate

通过匹配域名来控制访问

在这里插入图片描述

在这里插入图片描述

原生命令

在这里插入图片描述

postman

在这里插入图片描述

【7】Path Route Predicate

通过路径控制访问

在这里插入图片描述

在这里插入图片描述

【8】Query Route Predicate

匹配Params参数来控制访问

在这里插入图片描述

在这里插入图片描述

postman

在这里插入图片描述

【9】RemoteAddr Route Predicate

限制IP访问

在这里插入图片描述

在这里插入图片描述

CIDR表示法

在这里插入图片描述

测试时不能用localhost来访问了要将localhost改成自己的ip地址

【10】Method Route Predicate

限制某个请求路径的请求方法来限制访问

在这里插入图片描述

在这里插入图片描述

(5)自定义 Predicate

当原有的断言配置不满足业务需求时,可以自定义断言,例如设置用于会员等级userType,按照钻/金/银与yml中的配置的匹配来限制访问。

参考 AfterRoutePredicateFactory 源码来自定义一个自定义断言

在这里插入图片描述

自定义路由断言规则步骤套路:

  • 创建一个名为 ___RoutePredicateFactory 的类,例如MyRoutePredicateFactory

  • 继承 AbstractRoutePredicateFactory 类

  • 重写 apply 方法

    @Override
    public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config) {
    	return null;
    }
    
  • 新建 apply 方法需要的静态内部类 MyRoutePredicateFactory.Config,这个 Config 类就是我们的路由断言规则

    public static class Config {
        @Setter
        @Getter
        @NotNull
        private String userType;    // 钻/金/银用户等级
    }
    
  • 空参调用方法

    public MyRoutePredicateFactory() {
    	super(MyRoutePredicateFactory.Config.class);
    }
    
  • 改进 apply 方法

        @Override
        public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config) {
            return new Predicate<ServerWebExchange>() {
                @Override
                public boolean test(ServerWebExchange serverWebExchange) {
                    //检查request的参数里面,userType是否为指定的值,符合配置就通过
                    String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");
    
                    if (userType == null) {
                        return false;
                    }
    
                    //如果说参数存在,就和config的数据进行比较
                    if (userType.equalsIgnoreCase(config.getUserType())) {
                        return true;
                    }
    
                    return false;
                }
            };
        }
    
  • 为了让 Shortcut Configuration 生效,还要重写 shortcutFieldOrder 方法

    @Override
    public List<String> shortcutFieldOrder() {
    	return Collections.singletonList("userType");
    }
    

完整代码如下:

package com.atguigu.cloud.mygateway;

import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;

// 自定义断言:配置会员等级 userType,按照钻/金/银和yml配置的会员等级,以适配是否可以访问
@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config> {

    public MyRoutePredicateFactory() {
        super(MyRoutePredicateFactory.Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("userType");
    }

    @Override
    public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config) {
        return new Predicate<ServerWebExchange>() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                //检查request的参数里面,userType是否为指定的值,符合配置就通过
                String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");

                if (userType == null) {
                    return false;
                }

                //如果说参数存在,就和config的数据进行比较
                if (userType.equalsIgnoreCase(config.getUserType())) {
                    return true;
                }

                return false;
            }
        };
    }

    public static class Config {
        @Setter
        @Getter
        @NotNull
        private String userType;    // 钻/金/银用户等级
    }

}

YML 配置如下:

在这里插入图片描述

普通访问无效

在这里插入图片描述

用户类型不匹配也无法访问

在这里插入图片描述

用户类型匹配才能访问成功

在这里插入图片描述

(6)Filter 过滤

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

【1】概述

在这里插入图片描述

在这里插入图片描述

工作原理

在这里插入图片描述

  1. 全局默认过滤器 Global Filters

    在这里插入图片描述

  2. 单一内置过滤器 GateWayFilter

    在这里插入图片描述

  3. 自定义过滤器

【2】内置过滤器

(单一内置过滤器GateWayFilter)

8001微服务 PayGateWayController 新增方法

    @GetMapping(value = "/pay/gateway/filter")
    public ResultData<String> getGatewayFilter(HttpServletRequest request) {
        String result = "";
        Enumeration<String> headers = request.getHeaderNames();
        while (headers.hasMoreElements()) {
            String headName = headers.nextElement();
            String headValue = request.getHeader(headName);
            System.out.println("请求头名: " + headName + "\t\t\t" + "请求头值: " + headValue);
            if (headName.equalsIgnoreCase("X-Request-atguigu1")
                    || headName.equalsIgnoreCase("X-Request-atguigu2")) {
                result = result + headName + "\t " + headValue + " ";
            }
        }
        return ResultData.success("getGatewayFilter 过滤器 test: " + result + " \t " + DateUtil.now());
    }
1.请求头相关组
  • AddRequestHeader 添加请求头ByName

在这里插入图片描述

访问地址,可以看到添加成功

在这里插入图片描述

  • RemoveRequestHeader 删除请求头ByName

删除之前

在这里插入图片描述

删除之后

在这里插入图片描述

  • SetRequestHeader 修改请求头ByName

修改前

在这里插入图片描述

修改后

在这里插入图片描述

2.请求参数相关组
  • AddRequestParameter
  • RemoveRequestParameter

两个一起测试

修改 PayGateWayController

    @GetMapping(value = "/pay/gateway/filter")
    public ResultData<String> getGatewayFilter(HttpServletRequest request) {
        String result = "";
        Enumeration<String> headers = request.getHeaderNames();
        while (headers.hasMoreElements()) {
            String headName = headers.nextElement();
            String headValue = request.getHeader(headName);
            System.out.println("请求头名: " + headName + "\t\t\t" + "请求头值: " + headValue);
            if (headName.equalsIgnoreCase("X-Request-atguigu1")
                    || headName.equalsIgnoreCase("X-Request-atguigu2")) {
                result = result + headName + "\t " + headValue + " ";
            }
        }

        System.out.println("=============================================");
        String customerId = request.getParameter("customerId");
        System.out.println("request Parameter customerId: "+customerId);

        String customerName = request.getParameter("customerName");
        System.out.println("request Parameter customerName: "+customerName);
        System.out.println("=============================================");
        
        return ResultData.success("getGatewayFilter 过滤器 test: " + result + " \t " + DateUtil.now());
    }

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.回应头相关组
  • AddResponseHeader
  • SetResponseHeader
  • RemoveResponseHeader

三个一起测试

在 Chrome 中查看 response

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.前缀和路径相关组

PrefixPath 自动添加路径前缀

在这里插入图片描述

在这里插入图片描述

SetPath 访问路径修改

在这里插入图片描述

image-20240820192249666

RedirectTo 重定向到某个页面

在这里插入图片描述

5.Default Filters

配置在此相当于全局通用,自定义变Global

在这里插入图片描述

【3】自定义过滤器
1.自定义全局 Filter

案例:通过自定义全局过滤器实现统计接口调用耗时情况

网关9527新建类 MyGlobalFilter 实现 GlobalFilter,Ordered 两个接口

package com.atguigu.cloud.mygateway;

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

import java.util.Map;

@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered {
    public static final String BEGIN_VISIT_TIME = "begin_visit_time";   //开始调用方法的时间

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 先记录下访问接口的开始时间
        exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
        // 返回统计的各个结果给后台
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
            if (beginVisitTime != null) {
                log.info("访问接口主机: " + exchange.getRequest().getURI().getHost());
                log.info("访问接口端口: " + exchange.getRequest().getURI().getPort());
                log.info("访问接口URL: " + exchange.getRequest().getURI().getPath());
                log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery());
                log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms");
                log.info("我是分割线: ###################################################");
                System.out.println();
            }
        }));
    }

    // 数字越小,优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
}

对YML中的路由全局应用

img

测试

img

img

2.自定义条件 Filter
  • 新建类名 ___GatewayFilterFactory 并继承 AbstractGatewayFilterFactory
  • 新建 Config 内部类
  • 重写 apply 方法
  • 空参构造方法,内部调用 super
package com.atguigu.cloud.mygateway;

import lombok.Getter;
import lombok.Setter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config> {
    public MyGatewayFilterFactory() {
        super(MyGatewayFilterFactory.Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("status");
    }

    @Override
    public GatewayFilter apply(MyGatewayFilterFactory.Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ServerHttpRequest request = exchange.getRequest();
                System.out.println("进入自定义网关过滤器MyGatewayFilterFactory,status====" + config.getStatus());
                if (request.getQueryParams().containsKey("atguigu")) {
                    return chain.filter(exchange);
                } else {
                    exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
                    return exchange.getResponse().setComplete();
                }
            }
        };
    }

    public static class Config {
        @Setter
        @Getter
        private String status;
    }
}

YML 配置

img

img

测试

img

三、SpringCloud Alibaba

GitHub:https://github.com/alibaba/spring-cloud-alibaba

中文文档:https://github.com/alibaba/spring-cloud-alibaba/blob/2023.x/README-zh.md

开发参考文档:https://sca.aliyun.com/

img

img

img

img

1、Nacos 服务注册和配置中心

1.1、Nacos 概述

官网:https://nacos.io/zh-cn/index.html

Nacos: Dynamic Naming and Configuration Service,是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台,Nacos就是注册中心+配置中心的组合。

在这里插入图片描述

各种注册中心的比较

img

1.2、Nacos 下载安装运行

地址:https://github.com/alibaba/nacos/releases

img

img

运行 Nacos 命令:startup.cmd -m standalone

img

结果页面

img

1.3、Nacos Discovery 服务注册中心

(1)服务提供者 9001
  • 新建 Model:cloudalibaba-provider-payment9001

  • POM

    <!--nacos-discovery-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
  • YML

    server:
      port: 9001
    
    spring:
      application:
        name: nacos-payment-provider
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #配置Nacos地址
    
  • 主启动

    package com.atguigu.cloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class Main9001 {
        public static void main(String[] args) {
            SpringApplication.run(Main9001.class, args);
        }
    }
    
  • 业务类

    package com.atguigu.cloud.controller;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class PayAlibabaController {
        @Value("${server.port}")
        private String serverPort;
    
        @GetMapping(value = "/pay/nacos/{id}")
        public String getPayInfo(@PathVariable("id") Integer id) {
            return "nacos registry, serverPort" + serverPort + "\t id" + id;
        }
    
    }
    
    

启动服务后成功注册进 Nacos

img

(2)服务消费者 83
  • 新建 Model:cloudalibaba-consumer-nacos-order83

  • POM

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>com.atguigu.cloud</groupId>
            <artifactId>cloud2024</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
    
        <artifactId>cloudalibaba-consumer-nacos-order83</artifactId>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <!--nacos-discovery-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!--loadbalancer-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            <!--web + actuator-->
            <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>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    
  • YML

    server:
      port: 83
    
    spring:
      application:
        name: nacos-order-consumer
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
    #消费者将要去访问的微服务名称(nacos微服务提供者叫什么你写什么)
    service-url:
      nacos-user-service: http://nacos-payment-provider
    
  • 主启动

    package com.atguigu.cloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class Main83 {
        public static void main(String[] args) {
            SpringApplication.run(Main83.class, args);
        }
    }
    
  • 业务类

    • 配置 config (restTemplate)

      package com.atguigu.cloud.config;
      
      import org.springframework.cloud.client.loadbalancer.LoadBalanced;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.client.RestTemplate;
      
      @Configuration
      public class RestTemplateConfig {
          @Bean
          @LoadBalanced   //赋予RestTemplate负载均衡的能力
          public RestTemplate restTemplate() {
              return new RestTemplate();
          }
      }
      
      
    • controller

      package com.atguigu.cloud.controller;
      
      import jakarta.annotation.Resource;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.PathVariable;
      import org.springframework.web.bind.annotation.RestController;
      import org.springframework.web.client.RestTemplate;
      
      @RestController
      public class OrderNacosController {
          @Resource
          private RestTemplate restTemplate;
      
          @Value("${service-url.nacos-user-service}")
          private String serverURL;
      
          @GetMapping("/consumer/pay/nacos/{id}")
          public String paymentInfo(@PathVariable("id") Integer id) {
              String result = restTemplate.getForObject(serverURL + "/pay/nacos/" + id, String.class);
              return result + "\t" + "    我是OrderNacosController83调用者。。。。。。";
          }
      }
      

启动查看 Nacos 客户端,服务注册成功

img

(3)负载均衡

拷贝虚拟端口映射出服务9002,来测试负载均衡

img

添加VM选项,设置为-DServer.port=9002

img

启动9002,发现成功注册进服务提供者中,实例变为2

img

测试:访问 http://localhost:83/consumer/pay/nacos/2

9001/9002 交替出现,达到了负载均衡

img

1.4、Nacos Config 服务配置中心

img

  • 新建 Model:cloudalibaba-config-nacos-client3377

  • POM

    <!--bootstrap-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    <!--nacos-config-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    
  • YML

    • bootstrap.yml

      # nacos配置
      spring:
        application:
          name: nacos-config-client
        cloud:
          nacos:
            discovery:
              server-addr: localhost:8848 #Nacos服务注册中心地址
            config:
              server-addr: localhost:8848 #Nacos作为配置中心地址
              file-extension: yaml #指定yaml格式的配置
      
      # nacos端配置文件DataId的命名规则是:
      # ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
      # 本案例的DataID是:nacos-config-client-dev.yaml
      
    • application.yml

      server:
        port: 3377
      
      spring:
        profiles:
          active: dev # 表示开发环境
          #active: prod # 表示生产环境
          #active: test # 表示测试环境
      
  • 主启动

    package com.atguigu.cloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class Main3377 {
        public static void main(String[] args) {
            SpringApplication.run(Main3377.class, args);
        }
    }
    
  • 业务类

    package com.atguigu.cloud.controller;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.context.config.annotation.RefreshScope;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RefreshScope // 在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
    public class NacosConfigClientController {
        @Value("${config.info}")
        private String configInfo;
    
        @GetMapping("/config/info")
        public String getConfigInfo() {
            return configInfo;
        }
    }
    

Nacos 的匹配规则(DataId)

img

DataId 的组成

本案例的 DataId 是:nacos-config-client-dev.yaml

img

创建配置

img

img

运行测试

img

历史配置-回滚

Nacos会记录配置文件的历史版本默认保留30天,此外还有一键回滚功能,回滚操作将会触发配置更新

img

1.5、Namespace-Group-DataId

使用 Nacos 数据模型 Key 的三元组 Namespace-Group-DataId 对微服务配置进行分组和命名空间管理

img

对微服务配置的管理只需要在客户端指定配置所在的 Namespace、Group、DataId 并且在代码中修改对应的配置文件属性即可访问到该环境的配置信息

  • DataId方案:指定 spring.profile.active 和配置文件映射的 DataId 来使不同环境下读取不同的配置,再 Nacos 客户端中新建指定对应 DataId 配置即可读取。

  • Group方案:在新建配置中指定想放入的 Group 即可,并需要在配置文件中指定 spring.cloud.nacos.config.group 属性为该 Group。

  • Namespace方案:可以在客户端中 新建一个 Namespace 空间,实现环境的区分,新建配置使指定其 Namespace 便可放入该空间中,并需要指定配置文件的 spring.cloud.nacos.config.namespace 属性为该 Namespace 的命名空间ID。

img

2、Sentinel 实现熔断与限流

2.1、Sentinel 概述

官网:https://sentinelguard.io/zh-cn/

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

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
  • 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

在这里插入图片描述

img

img

2.2、Sentinel 下载安装运行

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

Sentinel 分为两个部分(后台8719默认,前台8080开启):

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

运行命令:java -jar sentinel-dashboard-1.8.6.jar

img

访问 Sentinel 管理界面:http://localhost:8080(账号密码都为 sentinel)

img

2.3、微服务8401整合Sentinel

  • 新建 Model:cloudalibaba-sentinel-service8401

  • POM

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>com.atguigu.cloud</groupId>
            <artifactId>cloud2024</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
    
        <artifactId>cloudalibaba-sentinel-service8401</artifactId>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <!--SpringCloud alibaba sentinel -->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            </dependency>
            <!--nacos-discovery-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!-- 引入自己定义的api通用包 -->
            <dependency>
                <groupId>com.atguigu.cloud</groupId>
                <artifactId>cloud-api-commons</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--SpringBoot通用依赖模块-->
            <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>
            <!--hutool-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.28</version>
                <scope>provided</scope>
            </dependency>
            <!--test-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    
  • YML

    server:
      port: 8401
    
    spring:
      application:
        name: cloudalibaba-sentinel-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848         #Nacos服务注册中心地址
        sentinel:
          transport:
            dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
            port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
    
  • 主启动

    package com.atguigu.cloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class Main8401 {
        public static void main(String[] args) {
            SpringApplication.run(Main8401.class, args);
        }
    }
    
  • 业务类

    package com.atguigu.cloud.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class FlowLimitController {
        @GetMapping("/testA")
        public String testA() {
            return "------testA";
        }
    
        @GetMapping("/testB")
        public String testB() {
            return "------testB";
        }
    }
     
    

要使用 Sentinel 对接口进行限流和降级操作,一定要先访问下接口,使 Sentinel 检测出相应的接口

在这里插入图片描述

在这里插入图片描述

2.4、流控规则

在这里插入图片描述

Sentinel能够对流量进行控制,主要是监控应用的QPS流量或者并发线程数等指标,如果达到指定的阈值时,就会被流量进行控制,以避免服务被瞬时的高并发流量击垮,保证服务的高可靠性。参数见最下方:

在这里插入图片描述

(1)流控模式
  • 直接

    默认的流控模式,当接口达到限流条件时,直接开启限流功能

    在这里插入图片描述

    连续访问两次testA直接限流

    在这里插入图片描述

  • 关联

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

    在这里插入图片描述

    (使用 Jmeter 模拟并发密集访问 testB)

    安装 Jmeter,地址:https://jmeter.apache.org/download_jmeter.cgi

    在这里插入图片描述

    参数设置,运行

    在这里插入图片描述

    运行后大批量线程高并发访问B,导致A失效了

    在这里插入图片描述

  • 链路

    对于来自不同链路的请求对同一个目标访问时,实施针对性的不同限流措施。如对C请求访问资源限流,D请求访问就正常。

    案例:修改8401在Controller中的不同链路testC和testD中访问Service中的同一资源

    配置 YML

    在这里插入图片描述

    Service

    package com.atguigu.cloud.service;
    
    import com.alibaba.csp.sentinel.annotation.SentinelResource;
    import org.springframework.stereotype.Service;
    
    @Service
    public class FlowLimitService {
        @SentinelResource(value = "common")
        public void common() {
            System.out.println("------FlowLimitService come in");
        }
    
    }
    
    

    Controller新增testC和testD

    在这里插入图片描述

    Sentinel配置

    在这里插入图片描述

    对于testC的访问超过一秒钟一次后,就发生限流,而D仍然正常访问

    在这里插入图片描述

(2)流控效果
  • 快速失败

    默认的流控处理,直接失败,抛出异常 在这里插入图片描述

  • 预热 WarmUp

    当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。

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

    在这里插入图片描述

    WarmUp 配置

    在这里插入图片描述

    多次刷新快速多次访问testB,刚开始时由于阈值限制会无法访问,后面阈值恢复逐渐可以访问

    在这里插入图片描述

  • 排队等待

    在这里插入图片描述

    修改Controller,增加testE

    @GetMapping("/testE")
    public String testE()
    {
        System.out.println(System.currentTimeMillis()+"      testE,排队等待");
        return "------testE";
    }
    

    Sentinel配置

    在这里插入图片描述

(3)阈值类型-并发线程数

Sentinel配置

在这里插入图片描述

Jmeter模拟多个线程并发+循环请求

在这里插入图片描述

访问testB,由于Jmeter打满了,基本无法访问,偶尔在Jmeter线程切换系统判断没访问时才有机会成功

2.5、熔断规则

Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

在这里插入图片描述

Sentinel主要提供了三个熔断策略

在这里插入图片描述

(1)慢调用比例

在这里插入图片描述

进入熔断状态判断依据:在统计时长内,实际请求数目>设定的最小请求数 且 实际慢调用比例>比例阈值 ,进入熔断状态。

在这里插入图片描述

测试:

Controller

    /**
     * 新增熔断规则-慢调用比例
     *
     * @return
     */
    @GetMapping("/testF")
    public String testF() {
        // 暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("----测试:新增熔断规则-慢调用比例 ");
        return "------testF 新增熔断规则-慢调用比例";
    }

10个线程,在一秒的时间内发送完。又因为服务器响应时长设置:暂停1秒,所以响应一个请求的时长都大于1秒综上符合熔断条件,所以当线程开启1秒后,进入熔断状态

Sentinel 配置

在这里插入图片描述

Jmeter 压力测试

在这里插入图片描述

测试结果

在这里插入图片描述

(2)异常比例

在这里插入图片描述

测试:

Controller

    /**
     * 新增熔断规则-异常比例
     *
     * @return
     */
    @GetMapping("/testG")
    public String testG() {
        System.out.println("----测试:新增熔断规则-异常比例 ");
        int age = 10 / 0;
        return "------testG,新增熔断规则-异常比例 ";
    }

Sentinel 配置

(提醒:记得关闭全局异常处理类才能看到效果)

在这里插入图片描述

测试结果

在这里插入图片描述

(3)异常数

在这里插入图片描述

测试:

Controller

    /**
     * 新增熔断规则-异常数
     *
     * @return
     */
    @GetMapping("/testH")
    public String testH() {
        System.out.println("----测试:新增熔断规则-异常数 ");
        int age = 10 / 0;
        return "------testH,新增熔断规则-异常数 ";
    }

Sentinel 配置

在这里插入图片描述

Jmeter

在这里插入图片描述

测试结果

在这里插入图片描述

2.6、@SentinelResource 注解

SentinelResource是一个流量防卫防护组件,用于指定防护资源,对配置的资源进行流量控制、熔断降级等功能。

源码:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {

    //资源名称
    String value() default "";

    //entry类型,标记流量的方向,取值IN/OUT,默认是OUT
    EntryType entryType() default EntryType.OUT;
    //资源分类
    int resourceType() default 0;

    //处理BlockException的函数名称,函数要求:
    //1. 必须是 public
    //2.返回类型 参数与原方法一致
    //3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置blockHandlerClass ,并指定blockHandlerClass里面的方法。
    String blockHandler() default "";

    //存放blockHandler的类,对应的处理函数必须static修饰。
    Class<?>[] blockHandlerClass() default {};

    //用于在抛出异常的时候提供fallback处理逻辑。 fallback函数可以针对所
    //有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求:
    //1. 返回类型与原方法一致
    //2. 参数类型需要和原方法相匹配
    //3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定fallbackClass里面的方法。
    String fallback() default "";

    //存放fallback的类。对应的处理函数必须static修饰。
    String defaultFallback() default "";

    //用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进
    //行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:
    //1. 返回类型与原方法一致
    //2. 方法参数列表为空,或者有一个 Throwable 类型的参数。
    //3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定 fallbackClass 里面的方法。
    Class<?>[] fallbackClass() default {};
 

    //需要trace的异常
    Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};

    //指定排除忽略掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。
    Class<? extends Throwable>[] exceptionsToIgnore() default {};
}
  1. 按照 rest 地址限流 + 默认限流返回

在这里插入图片描述

Sentinel 配置

在这里插入图片描述

前面使用的都是该方案(默认)

在这里插入图片描述

  1. 按 SentinelResource 资源名称限流 + 自定义限流返回

        @GetMapping("/rateLimit/byResource")
        @SentinelResource(value = "byResourceSentinelResource", blockHandler = "handleException")
        public String byResource() {
            return "按资源名称SentinelResource限流测试OK";
        }
        public String handleException(BlockException exception) {
            return "服务不可用@SentinelResource启动" + "\t" + "o(╥﹏╥)o";
        }
    

    Sentinel 配置按资源名称限流,blockHandler映射到自定义的限流返回

    在这里插入图片描述

    限流后返回自定义提示

    在这里插入图片描述

  2. 按 SentinelResource 资源名称限流 + 自定义限流返回 + 服务降级处理

        @GetMapping("/rateLimit/doAction/{p1}")
        @SentinelResource(value = "doActionSentinelResource",
                blockHandler = "doActionBlockHandler", fallback = "doActionFallback")
        public String doAction(@PathVariable("p1") Integer p1) {
            if (p1 == 0) {
                throw new RuntimeException("p1等于零直接异常");
            }
            return "doAction";
        }
    
        public String doActionBlockHandler(@PathVariable("p1") Integer p1, BlockException e) {
            log.error("sentinel配置自定义限流了:{}", e);
            return "sentinel配置自定义限流了";
        }
    
        public String doActionFallback(@PathVariable("p1") Integer p1, Throwable e) {
            log.error("程序逻辑异常了:{}", e);
            return "程序逻辑异常了" + "\t" + e.getMessage();
        }
    

    配置和代码关系

    在这里插入图片描述

    测试结果

    在这里插入图片描述

2.7、热点规则

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

在这里插入图片描述

Controller

    @GetMapping("/testHotKey")
    @SentinelResource(value = "testHotKey", blockHandler = "dealHandler_testHotKey")
    public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
                             @RequestParam(value = "p2", required = false) String p2) {
        return "------testHotKey";
    }
    public String dealHandler_testHotKey(String p1, String p2, BlockException exception) {
        return "-----dealHandler_testHotKey";
    }

Sentinel 配置

在这里插入图片描述

在这里插入图片描述

参数例外项

可以设定某个参数为某个特殊值时有不同的限流规则

当 p1 为 5 时,限流阈值改为 200

在这里插入图片描述

在这里插入图片描述

2.8、授权规则

在某些场景下,需要根据调用接口的来源判断是否允许执行本次请求。此时就可以使用Sentinel提供的授权规则来实现,Sentinel的授权规则能够根据请求的来源判断是否允许本次请求通过。

在Sentinel的授权规则中,提供了 白名单与黑名单 两种授权类型,白放行、黑禁止。

Controller

    @GetMapping(value = "/empower")
    public String requestSentinel4(){
        log.info("测试Sentinel授权规则empower");
        return "Sentinel授权规则";
    }

需要创建一个类实现 RequestOriginParser 接口

在这里插入图片描述

Sentinel 配置

在这里插入图片描述

在这里插入图片描述

2.9、规则持久化

由于一旦我们重启微服务应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。

将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效。

引入POM坐标

<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

YML

在这里插入图片描述

RuleType说明

在这里插入图片描述

Nacos 配置

在这里插入图片描述

在这里插入图片描述

2.10、整合 OpenFeign 集成实现 fallback 服务降级

需求说明

在这里插入图片描述

程序解耦

在这里插入图片描述

(1)修改服务提供方9001
  • POM

    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--alibaba-sentinel-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    
  • YML

    server:
      port: 9001
    
    spring:
      application:
        name: nacos-payment-provider
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #配置Nacos地址
        sentinel:
          transport:
            dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
            port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
    
  • Controller

        // openfeign + sentinel 进行服务降级和流量监控的整合处理 case
        @GetMapping("/pay/nacos/get/{orderNo}")
        @SentinelResource(value = "getPayByOrderNo", blockHandler = "handlerBlockHandler")
        public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo) {
            // 模拟从数据库查询出数据并赋值给DTO
            PayDTO payDTO = new PayDTO();
    
            payDTO.setId(1024);
            payDTO.setOrderNo(orderNo);
            payDTO.setAmount(BigDecimal.valueOf(9.9));
            payDTO.setPayNo("pay:" + IdUtil.fastUUID());
            payDTO.setUserId(1);
    
            return ResultData.success("查询返回值:" + payDTO);
        }
    
        public ResultData handlerBlockHandler(@PathVariable("orderNo") String orderNo, BlockException exception) {
            return ResultData.fail(ReturnCodeEnum.RC500.getCode(), "getPayByOrderNo服务不可用," +
                    "触发sentinel流控配置规则" + "\t" + "o(╥﹏╥)o");
        }
        /*
        fallback服务降级方法纳入到Feign接口统一处理,全局一个
        public ResultData myFallBack(@PathVariable("orderNo") String orderNo,Throwable throwable)
        {
            return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"异常情况:"+throwable.getMessage());
        }
        */
    
(2)修改 cloud-api-commons
  • POM

    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--alibaba-sentinel-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    
  • 新增接口 PayFeignSentinelApi 接口

    package com.atguigu.cloud.apis;
    
    import com.atguigu.cloud.resp.ResultData;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    @FeignClient(value = "nacos-payment-provider", fallback = PayFeignSentinelApiFallBack.class)
    public interface PayFeignSentinelApi {
        @GetMapping("/pay/nacos/get/{orderNo}")
        public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo);
    }
    
    
  • 新建全局统一服务降级类

    package com.atguigu.cloud.apis;
    
    import com.atguigu.cloud.resp.ResultData;
    import com.atguigu.cloud.resp.ReturnCodeEnum;
    import org.springframework.stereotype.Component;
    
    @Component
    public class PayFeignSentinelApiFallBack implements PayFeignSentinelApi {
        @Override
        public ResultData getPayByOrderNo(String orderNo) {
            return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"对方服务宕机或不可用,FallBack服务降级o(╥﹏╥)o");
        }
    }
    
    

在这里插入图片描述

(3)修改消费者83
  • POM

    <!-- 引入自己定义的api通用包 -->
    <dependency>
        <groupId>com.atguigu.cloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--alibaba-sentinel-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    
  • YML

    # 激活Sentinel对Feign的支持
    feign:
      sentinel:
        enabled: true
    
  • 主启动类上加上 @EnableFeignClients 注解,启用Feign的功能

  • Controller

        @Resource
        private PayFeignSentinelApi payFeignSentinelApi;
    
        @GetMapping(value = "/consumer/pay/nacos/get/{orderNo}")
        public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo) {
            return payFeignSentinelApi.getPayByOrderNo(orderNo);
        }
    

测试:

启动服务83,会出现报错(由于Boot和Cloud版本太高和Sentinel不兼容导致)

在这里插入图片描述

需要将父工程的alibaba版本改为2023RC1,即可适配

在这里插入图片描述

启动9001和83进行测试

在这里插入图片描述

配置流控规则

在这里插入图片描述

触发了自定义流控返回

在这里插入图片描述

关闭9001服务,83自动降级

在这里插入图片描述

2.11、整合 GateWay 集成实现服务限流

案例:使用 cloudalibaba-sentinel-gateway9528 保护 cloudalibaba-provider-payment9001

  • 新建 Module:cloudalibaba-sentinel-gateway9528

  • POM

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>com.atguigu.cloud</groupId>
            <artifactId>cloud2024</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
    
        <artifactId>cloudalibaba-sentinel-gateway9528</artifactId>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <!--nacos-discovery-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-transport-simple-http</artifactId>
                <version>1.8.6</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
                <version>1.8.6</version>
            </dependency>
            <dependency>
                <groupId>javax.annotation</groupId>
                <artifactId>javax.annotation-api</artifactId>
                <version>1.3.2</version>
                <scope>compile</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    
  • YML

    server:
      port: 9528
    
    spring:
      application:
        name: cloudalibaba-sentinel-gateway     # sentinel+gateway整合Case
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        gateway:
          routes:
            - id: pay_routh1 #pay_routh1                #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
              uri: http://localhost:9001                #匹配后提供服务的路由地址
              predicates:
                - Path=/pay/**                      # 断言,路径相匹配的进行路由
    
  • 主启动

    package com.atguigu.cloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class Main9528 {
        public static void main(String[] args) {
            SpringApplication.run(Main9528.class, args);
        }
    }
    
  • 业务类

    参照官网案例配置 https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81#spring-cloud-gateway

    在这里插入图片描述

    package com.atguigu.cloud.config;
    
    import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
    import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
    import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
    import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
    import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
    import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
    import org.springframework.beans.factory.ObjectProvider;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.Ordered;
    import org.springframework.core.annotation.Order;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.codec.ServerCodecConfigurer;
    import org.springframework.web.reactive.function.BodyInserters;
    import org.springframework.web.reactive.function.server.ServerResponse;
    import org.springframework.web.reactive.result.view.ViewResolver;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    import javax.annotation.PostConstruct;
    import java.util.*;
    
    @Configuration
    public class GatewayConfiguration {
        private final List<ViewResolver> viewResolvers;
        private final ServerCodecConfigurer serverCodecConfigurer;
    
        public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
            this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
            this.serverCodecConfigurer = serverCodecConfigurer;
        }
    
        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
            // Register the block exception handler for Spring Cloud Gateway.
            return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
        }
    
        @Bean
        @Order(-1)
        public GlobalFilter sentinelGatewayFilter() {
            return new SentinelGatewayFilter();
        }
    
        @PostConstruct // javax.annotation.PostConstruct
        public void doInit() {
            initBlockHandler();
        }
    
        // 处理/自定义返回的例外信息
        private void initBlockHandler() {
            // 配置路由规则
            Set<GatewayFlowRule> rules = new HashSet<>();
            rules.add(new GatewayFlowRule("pay_routh1").setCount(2).setIntervalSec(1));
    
            GatewayRuleManager.loadRules(rules);
            BlockRequestHandler handler = new BlockRequestHandler() {
                @Override
                public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
                    Map<String, String> map = new HashMap<>();
    
                    map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
                    map.put("errorMessage", "请求太过频繁,系统忙不过来,触发限流(sentinel+gataway整合Case)");
    
                    return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                            .contentType(MediaType.APPLICATION_JSON)
                            .body(BodyInserters.fromValue(map));
                }
            };
    
            GatewayCallbackManager.setBlockHandler(handler);
        }
    
    }
    

通过网关访问成功

在这里插入图片描述

Sentinel + Gateway 点击频率加快,出现了限流

在这里插入图片描述

3、Seata 处理分布式事务

3.1、Seata 概述

官网:https://seata.io/zh-cn/index.html

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

在这里插入图片描述

在这里插入图片描述

Seata 术语

TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

3.2、Seata 工作流程简介

纵观整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知

在这里插入图片描述

XID 是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中,通过全局事务注解 @GlobalTransactional 传入。

在这里插入图片描述

在这里插入图片描述

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;

  2. XID 在微服务调用链路的上下文中传播;

  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;

  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议;

  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

3.3、Seata 下载安装运行

网址:https://seata.apache.org/zh-cn/unversioned/download/seata-server/

在这里插入图片描述

建库建表

CREATE DATABASE seata;
USE seata;
------------------- The script used when storeMode is 'db' -------------------------

-- the table to store GlobalSession data

CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;


-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;


-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;


CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;


INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

在这里插入图片描述

修改 seata-server-2.0.0\conf\application.yml文件(先备份)

#  Copyright 1999-2019 Seata.io Group.

#

#  Licensed under the Apache License, Version 2.0 (the "License");

#  you may not use this file except in compliance with the License.

#  You may obtain a copy of the License at

#

#  http://www.apache.org/licenses/LICENSE-2.0

#

#  Unless required by applicable law or agreed to in writing, software

#  distributed under the License is distributed on an "AS IS" BASIS,

#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

#  See the License for the specific language governing permissions and

#  limitations under the License.

 

server:
  port: 7091

spring:
  application:
    name: seata-server
 
logging:
  config: classpath:logback-spring.xml
  file:
    path: ${log.home:${user.home}/logs/seata}
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata

seata:
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace:
      group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
      username: nacos
      password: nacos
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
      namespace:
      cluster: default
      username: nacos
      password: nacos    
  store:
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
      user: root
      password: 123456
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 1000
      max-wait: 5000

  #  server:
  #    service-port: 8091 #If not configured, the default is '${server.port} + 1000'

  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**

运行 seata-server.bat

在这里插入图片描述

访问 http://localhost:7091 (用户名密码都为 seata)

在这里插入图片描述

在这里插入图片描述

3.4、快速启动(AT 模式)

建表建库

建seata_order库+建t_order表+undo_log表

#order

CREATE DATABASE seata_order;

USE seata_order;

 

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=1 DEFAULT CHARSET=utf8;

 

SELECT * FROM t_order;



-- for AT mode you must to init this sql for you business database. the seata server not need it.

CREATE TABLE IF NOT EXISTS `undo_log`

(

    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',

    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',

    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',

    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',

    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',

    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',

    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',

    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)

) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

建seata_storage库+建t_storage 表+undo_log表

#storage

CREATE DATABASE seata_storage;

 

USE seata_storage;

 

CREATE 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=1 DEFAULT CHARSET=utf8;

 

INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');

 

SELECT * FROM t_storage;



 -- for AT mode you must to init this sql for you business database. the seata server not need it.

CREATE TABLE IF NOT EXISTS `undo_log`

(

    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',

    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',

    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',

    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',

    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',

    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',

    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',

    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)

) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

建seata_account库+建t_account 表+undo_log表

#account

create database seata_account;

 

use seata_account;

 

CREATE 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`)VALUES('1','1','1000','0','1000');

 

SELECT * FROM t_account;

 -- for AT mode you must to init this sql for you business database. the seata server not need it.

CREATE TABLE IF NOT EXISTS `undo_log`

(

    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',

    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',

    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',

    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',

    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',

    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',

    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',

    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)

) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

在这里插入图片描述

Mybaits一键生成,依次生成2001,2002,2003服务的基础代码,见第一章3.3.2

  • config.properties

    #t_pay表包名
    package.name=com.atguigu.cloud
    
    # mysql8.0
    jdbc.driverClass = com.mysql.cj.jdbc.Driver
    jdbc.url= jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    jdbc.user = root
    jdbc.password =123456
    
    # seata_order2001
    #jdbc.driverClass = com.mysql.cj.jdbc.Driver
    #jdbc.url = jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    #jdbc.user = root
    #jdbc.password =123456
    
    # seata_storage2002
    #jdbc.driverClass = com.mysql.cj.jdbc.Driver
    #jdbc.url = jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    #jdbc.user = root
    #jdbc.password =123456
    
    # seata_account2003
    #jdbc.driverClass = com.mysql.cj.jdbc.Driver
    #jdbc.url = jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    #jdbc.user = root
    #jdbc.password =123456
    
  • generatorConfig.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE generatorConfiguration
            PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
            "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
    
    <generatorConfiguration>
        <properties resource="config.properties"/>
    
        <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
            <property name="beginningDelimiter" value="`"/>
            <property name="endingDelimiter" value="`"/>
    
            <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
                <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
                <property name="caseSensitive" value="true"/>
            </plugin>
    
            <jdbcConnection driverClass="${jdbc.driverClass}"
                            connectionURL="${jdbc.url}"
                            userId="${jdbc.user}"
                            password="${jdbc.password}">
            </jdbcConnection>
    
            <javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>
    
            <sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>
    
            <javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
    
            <table tableName="t_pay" domainObjectName="Pay">
                <generatedKey column="id" sqlStatement="JDBC"/>
            </table>
    
            <!--  seata_order -->
            <!--<table tableName="t_order" domainObjectName="Order">
                <generatedKey column="id" sqlStatement="JDBC"/>
            </table>-->
    
            <!--seata_storage-->
            <!--<table tableName="t_storage" domainObjectName="Storage">
                <generatedKey column="id" sqlStatement="JDBC"/>
            </table>-->
    
            <!--seata_account-->
            <!--<table tableName="t_account" domainObjectName="Account">
                <generatedKey column="id" sqlStatement="JDBC"/>
            </table>-->
    
        </context>
    </generatorConfiguration>
    
    

省略三个微服务的创建流程,https://github.com/Fetters04/cloud2024.git 下载 cloud-api-commons,seata-order-service2001、seata-order-service2002、seata-order-service2003 服务跑通即可

在这里插入图片描述

Seata后台可以看到全局事务ID和全局锁

在这里插入图片描述

在这里插入图片描述

3.5、AT 模式

AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

在一阶段,Seata 会拦截“业务 SQL”,

1 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,

2 执行“业务 SQL”更新业务数据,在业务数据更新之后,

3 其保存成“after image”,最后生成行锁。

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

在这里插入图片描述

二阶段,有两种情况

1 二阶段如是顺利提交的话,因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

在这里插入图片描述

2 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值