Spring Cloud 原生态万字笔记

696 阅读22分钟

后文 Alibaba 增强生态组件
juejin.cn/spost/75034…
如有细节错误或无关代码的问题,很感谢您在评论区提出

一、笔记内容技术选型

技术 版本

技术版本
JavaJDK17+
boot3.2.0
cloud2023.0.0
cloud alibaba2022.0.0.0-RC2
Maven3.9+
MySQL8.0+

二、Spring Cloud介绍

为什么需要Spring Cloud?

传统的单体架构足以满足中小型项目的需求,但是如果对于一个用户量庞大的系统就会出现各种问题。

例如:如果只有一个支付系统,那么系统崩溃了整个系统就运作不了了。

而分布式系统解决了这个问题,它允许系统以集群的形式部署,形成负载均衡,尽量减少系统崩溃带来的问题。

三、单体项目构建

SpringBoot单体服务

父项目构建 | 数据库建表

<!-- 
 </dependencyManagement> 是父工程必带的标签
定义为:为子工程声明依赖但并不为父工程添加依赖,只是起到声明作用
注意!!!:用</dependencyManagement>框起来是maven默认只从当前库中查找有无依赖,没有的会爆红
            只需要现将此标签去除后,将依赖添加完毕后再用标签框起来
-->
  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <lombok.version>1.18.26</lombok.version>
    <hutool.version>5.8.22</hutool.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>-->
    <!--仅为了openfeign + alibaba sentinel 降低版本处理-->
    <spring.boot.version>3.0.9</spring.boot.version>
    <spring.cloud.version>2022.0.2</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>

            <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>
create database cloud2024;
use cloud2024;
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;

MyBatis逆向工程

本次使用的Mapper4进行逆向工程

  1. 在父工程下面创建一个子模块,给子模块导入依赖

说明:这个工程只是为了暂时存储生成的代码,等到业务工程使用的时候,会将对应的类复制过去

<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.11</version>
                    </dependency>
                    <dependency>
                        <groupId>tk.mybatis</groupId>
                        <artifactId>mapper</artifactId>
                        <version>4.2.3</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
  1. 在子模块的resources下新建文件config.properties,将内容改成自己的
#t_pay表包名
package.name=com.peng

# mysql8.0
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/cloud2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456
  1. 在子模块的resources下新建文件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>
  1. 创建 com.peng 文件夹,双击运行Maven中的插件

编写业务逻辑

模块创建

  1. 创建一个业务逻辑模块cloud-provider-payment8001
  2. 给模块导入依赖
<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>
            <version>3.2.0</version>
        </plugin>
    </plugins>
</build>
  1. 编写yaml配置文件
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/cloud2024?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.peng
  configuration:
    map-underscore-to-camel-case: true
  1. 创建启动类
import tk.mybatis.spring.annotation.MapperScan;
// 注意MapperScan导入的包是tk. 这个导入后可以不用写@Mapper
@MapperScan("com.peng.mapper")
@SpringBootApplication
public class Main8001 {
    public static void main(String[] args) {
        SpringApplication.run(Main8001.class, args);
    }
}
  1. 将逆向工程生成的代码拷贝到业务工程中,删除原本逆向工程中生成的代码
  2. 编写Service、Controller层的增删改查方法
  3. 启动项目,测试接口

整合Swager3

<dependency>
   <groupId>org.springdoc</groupId>
   <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
   <version>${swagger3.version}</version>
</dependency>
注解标注位置
@TagController类
@Operation方法上
@Schemamodel层的bean和bean的方法上
@Tag(name = "支付微服务模块", description = "支付交易表相关接口")
public class PayController {
@Operation(summary = "新增支付交易表", description = "新增支付交易表,json串做参数")
public ResultData<String> addPay(@RequestBody Pay pay) {
/**
* 支付流水号
*/
@Column(name = "pay_no")
@Schema(title="支付流水号")
private String payNo;
@Configuration
public class Swagger3Config{
    @Bean
    public GroupedOpenApi PayApi()
    {
        //以/pay开头的请求都是支付模块
        return GroupedOpenApi.builder().group("支付微服务模块").pathsToMatch("/pay/**").build();
    }
    @Bean
    public GroupedOpenApi OtherApi()
    {
        //以/other开头的都是其他模块的请求
        return GroupedOpenApi.builder().group("其它微服务模块").pathsToMatch("/other/**", "/others").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/"));
    }
}
localhost:8001/swagger-ui/index.html

统一返回结果Result

@Getter
public enum ReturnCodeEnum {

    //1.举值
    RC999("999", "操作XXX失败"),
    RC200("200", "success"),
    RC201("201", "服务开启降级保护,请稍后再试!"),
    RC202("202", "热点参数限流,请稍后再试!"),
    RC203("203", "系统规则不满足要求,请稍后再试!"),
    RC204("204", "授权规则不通过,请稍后再试!"),
    RC403("403", "无访问权限,请联系管理员授予权限"),
    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", "不支持的认证模式");


    //2.构造
    private final String code;//自定义状态码,对应前面枚举的第一个参数
    private final String message;//自定义信息,对应前面枚举的第二个参数
    ReturnCodeEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    //3.遍历
    public static ReturnCodeEnum getReturnCodeEnum(String code) {
        //传入一个状态码,如果有,就返回整个枚举信息,如果没有就返回空
        for (ReturnCodeEnum element : ReturnCodeEnum.values()) {
            if (element.getCode().equalsIgnoreCase(code)) {
                return element;
            }
        }
        return null;
    }
}
@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);

        return resultData;
    }
    public static <T> ResultData<T> fail(ReturnCodeEnum returnCodeEnum) {
        ResultData<T> resultData = new ResultData<>();
        resultData.setCode(returnCodeEnum.getCode());
        resultData.setMessage(returnCodeEnum.getMessage());

        return resultData;
    }
}

优化时间格式

  • 方式一:在实体类的时间属性上加@JsonFormat注解
@JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss" ,timezone = "GMT+8")
private Date createTime;
  • 方式二:SpringBoot项目在yml中进行配置
spring:
  jackson:
    date-format: yyyy-MM-dd HH-mm-ss
    time-zone: GMT+8

异常处理

@RestControllerAdvice //aop。拦截标注了@RestController的controller中的所有方法
//@ControllerAdvice //aop。拦截标注了@Controller的controller中的所有方法
public class GlobalExceptionHandler {

    /**
     * 异常处理的方法(controller方法发生了异常,那么就使用该方法来处理)
     *
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    public ResultData<String> handException(Exception e) {
        e.printStackTrace(); //在控制台打印异常信息
        return ResultData.fail(ReturnCodeEnum.RC500.getCode(),ReturnCodeEnum.RC500.getMessage());
    }

}

编写订单模块【模块构建参考上述步骤】

通过服务调用RestTemplate,来进行不同模块的接口调用
//两种方式
//1.使用的时候直接new
RestTemplate restTemplate = new RestTemplate();

//2.在容器中配置一个
@Configuration
public class OrderConfiguration {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
@RestController
public class OrderController {
    public static final String url="http://localhost:8001";

    @Resource
    private RestTemplate restTemplate;

    @PostMapping("/consumer/pay/add")
    public ResultData addOrder(PayDTO payDTO) {
        //参数一:  调用的URL接口地址
        //参数二:  传递的参数
        //参数三:  返回类型.class
        return restTemplate.postForObject(url+"/pay/add", payDTO, ResultData.class);
    }
    @GetMapping("/consumer/pay/get/{id}")
    public ResultData getPayInfo(@PathVariable("id") Integer id) throws IOException {
        //参数一:  调用的URL接口地址
        //参数二:  返回类型.class
        //参数三:  路径变量
        return restTemplate.getForObject(url+"/pay/get/"+id, ResultData.class, id);
    }
}
//1.get请求【两个方法任选一个】
//两个方法的参数一样:1.url请求地址 2.返回值接收对象类型.class 3.路径参数

//只接受返回对象用这个:restTemplate.getForObject("请求地址",返回值对象类型,路径参数)
Result result = restTemplate.getForObject("http://localhost:8001/pay/get/{id}", Result.class, 1);

//全部响应体用这个:restTemplate.getForEntity("请求地址",返回值对象类型,路径参数)
ResponseEntity<Result> response = restTemplate.getForEntity("http://localhost:8001/pay/get/all", Result.class);

//2.post请求【两个方法任选一个】
//两个方法的参数一样:1.url请求地址 2.请求参数 3.返回值接收对象类型

//只接受返回对象用这个:restTemplate.postForObject("请求地址",参数,返回值对象类型)
Result result = restTemplate.postForObject("http://localhost:8001/pay/add",payDTO,Result.class);

//全部响应体用这个:restTemplate.postForEntity("请求地址",参数,返回值对象类型)
ResponseEntity<Result> response = restTemplate.postForEntity("http://localhost:8001/pay/",payDTO,Result.class);

//3.delete请求
//方法的参数:1.url请求地址 2.路径参数
restTemplate.delete("http://localhost:8001/pay/del/{id}", 1);

//4.put请求
//方法的参数:1.url请求地址 2.请求参数
restTemplate.put("http://localhost:8001/pay/update", payDTO);

重复代码抽取

问题:两个模块中有很多重复的代码。例如实体类、返回结果、异常处理类等。

解决方法:将公共代码抽取到一个模块中,其他模块引用公共模块

1. 创建一个模块cloud-api-common,引入依赖

<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>
  <dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>persistence-api</artifactId>
  </dependency>
</dependencies>

2. 将前面两个模块中的公共代码抽取出来放到这个新的模块中

例如:entities包、utils包、handler包

3. 将模块打成jar包,放到本地仓库中

4. 支付和订单模块在pom文件中引入公共模块的jar包

<dependency>
    <groupId>com.peng</groupId>
    <artifactId>cloud-api-common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

Consul服务注册和发现 1.20.6 8500

1.基本介绍

  • Consul是什么?

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

官方:consul.io/

  • Consul能干什么?

服务发现:提供HTTP和DNS两种发现方式

健康检测

KV存储

多数据中心

可视化WEB界面

  • 为什么不使用Eureka了?

Eureka停更了,不在开发新版本了

Eureka对初学者不友好

我们希望注册中心能够从项目中分离出来,单独运行,而Eureka做不到这一点

2.下载运行

  1. 下载地址:developer.hashicorp.com/consul/inst…
  2. 下载对应的版本【adm64版本的就是x86_64版本的,386就是x86_32版本的】
  3. windows使用下面的命令启动,然后缩放到最小化就行
consul agent -dev

4. 访问8500端口,进入ui界面

localhost:8500

3.服务注册与发现

需求说明:将前面单体服务中的支付模块、订单模块注册到Consul中

  1. 对应模块的pom文件中引入依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
  1. 编写配置文件yaml【健康检查那里不配置consul会爆红,不知道为什么】
spring:
  #当前服务名
  application:
    name: cloud-pay-service
    #配置注册中心的地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #配置当前服务注册到里面使用的名字
        service-name: ${spring.application.name}
        #开启consul的健康检查
        heartbeat:
          enabled: true
          ttl: 10s
  1. 启动类加上@EnableDiscoveryClient注解,开启服务发现功能【有人说可以不加这个注解了】
@EnableDiscoveryClient
  1. 启动boot项目
  2. 去consul的ui页面查看是否注册成功
  3. 将订单接口中支付模块的url地址改为consul中注册的名字
private String url="http://cloud-pay-service";
  1. 因为consul默认支持负载均衡,所以http客户端加上@LoadBalanced注解
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
   return new RestTemplate();
}
  1. 测试接口调用是否成功

4.服务配置

问题说明:

系统拆分之后,会产生大量的微服务。每个微服务都有其对应的配置文件yml。如果其中的某个配置项发生了修改,一个一个微服务修改会很麻烦。因此一套集中式的、动态的配置管理设施是必不可少的。从而实现一次修改,处处生效。

案例:给班里同学通知下节课不上了

麻烦的方法:一个个发送消息

简单的方法:直接在班级群@所有人

思路:既然是全局配置信息,那么可以把信息注册到Consul中,需要什么就去Consul中获取

  1. 给对应的模块添加服务配置的依赖
<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>
  1. 在resources下新建一个bootstrap.yml文件,将公共配置从application.yml中抽取出来

说明:

bootstrap.yml和applicaiton.yml一样都是配置文件。applicaiton.yml是用户级的,bootstrap.yml是系统级的,优先级更加高

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

Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。

application.yml和bootstrap.yml可以共存,公共的配置项写到bootstrap.yml中,项目特有的配置项写到application.yml

  • bootstrap.yml 是在 application.yml之前加载的
  • 里面配置的东西,比如 Consul 地址、Config Server 地址、加密解密密钥,都是启动必须的
  • 先连上 Consul 拿到配置,再去读 application.yml,否则 application.yml根本不知道远程配置信息。
spring:
  #当前服务名
  application:
    name: cloud-pay-service
    #配置注册中心的地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #配置当前服务注册到里面使用的名字
        service-name: ${spring.application.name}
        #开启consul的健康检查
        heartbeat:
          enabled: true
          ttl: 10s
      #服务配置
      config:
        #这个是配置文件名以-连接【consul的k-v存储用到】,例如:cloud-payment-service
        #默认用 “ , ”
        profile-separator: '-'
        #说明consul中kv的文本格式
        format: YAML
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    #读取consul中的配置到这里
    username: ${mysql.username}
    password: ${mysql.password}
  1. application.yml就只剩下没有抽取出去的属于微服务自己的配置了
server:
  port: 8001

spring:
  jackson:
    date-format: yyyy-MM-dd HH-mm-ss
    time-zone: GMT+8

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.peng
  configuration:
    map-underscore-to-camel-case: true
  1. 打开consul的ui界面,找到key-value,点击右上角的create,创建文件夹

  1. 在consul中创建二级文件夹 config/微服务名/data,创建data文件,供项目测试是否能够读取

如果创建的是文件夹那么以/结尾

说明:配置默认存储到config/微服务名-配置文件版本/data中,项目启动的时候使用的哪套application.yaml文件就会来这里找对应的文件

例如:cloud-payment-service微服务如果在application.yml 没有指定启用的配置文件

**默认环境**

spring:

  profiles:

    active:

config/cloud-payment-service/data

例如:cloud-payment-service微服务如果在application.yml指定启用的配置文件是application-dev.yml

**开发环境**

spring:

  profiles:

    active: dev

config/cloud-payment-service-dev/data

例如:cloud-payment-service微服务如果在application.yml指定启用的配置文件是application-prod.yml

生产环境

spring:

  profiles:

    active: prod

config/cloud-payment-service-prod/data
  1. 创建data文件,输入数据库的账号和密码,测试项目是否能读取成功,并连接数据库

#yml文件会自动读取 consul 中的配置
username: ${mysql.username}
password: ${mysql.password}

5.动态刷新

需求说明:希望Consul的配置变动之后,项目读取的内容也能立马改变。

说明:没配置动态刷新时,在consul配置数据源,项目启动之后,改变数据源没作用

  1. 在主启动类加上@RefreshScope注解【如果不生效,就放到controller上】
  2. 然后在bootstrap.yml中设置刷新的间隔【这一步不设置也可以,因为官网默认设置了55s刷新】
spring:
  application:
    name: cloud-payment-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}
      config:
        profile-separator: '-'
        format: YAML
        #设置了这里,1s刷新
        watch:
          wait-time: 1

6.配置数据持久化

场景:如果我们把Consul关了,下次启动的时候,之前配置的yaml数据就会全丢了。

我们现在需要解决这个问题。

构建一个存储信息的文件夹 mydata

构建脚本代码 consul.start.bat,使用管理员运行一次即可,文件保存为ANSI格式防止中文代码

@echo off
echo ===============================
echo 正在重置 Consul 服务...
echo ===============================

:: 尝试停止 Consul 服务(如果存在)
sc stop Consul >nul 2>nul

:: 尝试删除旧的 Consul 服务
sc delete Consul >nul 2>nul

echo 等待清理完成...
timeout /t 2 /nobreak >nul

:: 创建新的 Consul 服务,注意引号处理
sc create Consul binpath= "G:\SpringCloud软件\consul_1.20.5_windows_amd64\consul.exe agent -server -ui -bind=127.0.0.1 -client=0.0.0.0 -bootstrap-expect=1 -data-dir="G:\SpringCloud软件\consul_1.20.5_windows_amd64\mydata""

:: 设置服务为自动启动
sc config Consul start=auto

:: 启动 Consul 服务
net start Consul

:: 检查启动是否成功
if %errorlevel%==0 (
    echo ===============================
    echo Consul服务启动成功!
    echo ===============================
) else (
    echo ===============================
    echo Consul服务启动失败,请检查路径或配置!
    echo ===============================
)

pause

LoadBlancer负载均衡(客户端的负载均衡)

1.基本介绍

LoadBlancer的前身是Ribbon,是一套负责负载均衡的客户端工具。

主要功能:LoadBlancer的主要作用就是提供客户端软件的负载均衡,然后由OpenFeign去调用具体的微服务

  • 负载均衡:通过算法,将请求平均分摊到多个服务上

2.基本使用

场景:订单模块通过负载均衡访问支付模块的8001/8002/8003服务

使用步骤:

  1. 先从注册中心拉取可调用的服务列表,了解他有多少个服务
  2. 按照指定的负载均衡策略,从服务列表中选择一个地址,进行调用
  1. 使用前提:已经使用了注册中心
  2. 启动两个支付模块的项目【为了方便,就不启动三个了】
  3. 因为spring-cloud-starter-consul-discovery 中已经集成了spring-cloud-starter-loadbalancer,所以不需要额外加注解了
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
  1. 在订单模块的RestTemplate客户端上加@LoadBalanced,开启负载均衡
RestTemplate和WebClient支持使用@LoadBalanced注解实现负载均衡,
而HttpClient不支持使用@LoadBalanced注解实现负载均衡
  1. 将调用的url改成在注册中心注册的名称
public static final String PayService_URL = "http://cloud-pay-service";

3.基本原理

  1. 会在项目中创建一个DiscoveryClient对象
  2. 通过DiscoveryClient对象,就能够获取注册中心中所有注册的服务
  3. 然后将获取的服务与调用地址中传入的微服务名称进行对比
  4. 如果一致,就会将微服务集群的相关信息返回
  5. 然后通过负载均衡算法,选择出其中一个服务进行调用

4.负载均衡算法

LoadBlancer默认包含两种负载均衡算法,轮询算法和随机算法,同时还可以自定义负载均衡算法。默认使用轮询算法。

  • 轮询算法【LoadBlancer默认使用这个】
实际调用服务器位置下标=rest接口第几次请求数 % 服务器集群总数量【每次服务重启动后rest接口计数从1开始】

如:
	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

如此类推......
  • 随机算法【LoadBlancer中也包含】

随机给一个数,然后请求下标对应的微服务

  • 支持自定义负载均衡算法

5.负载均衡算法切换

@Configuration
//下面的value值大小写一定要和consul里面的名字一样,必须一样
//表示将要对哪个 value微服务实行一个新的 负载均衡算法
@LoadBalancerClient(value = "cloud-pay-service",configuration = RestTemplateConfig.class)
public class RestTemplateConfig
{
    @Bean
    @LoadBalanced
    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);
    }
}

OpenFeign(推荐用这个,集成了LoadBlancer)

不再使用原先 RestTemplateConfig 以及 LoadBlancer ,使用OpenFeign可以进行集成解耦

1.基本介绍

OpenFeign编写了一套声明式的Web服务客户端,使用LoadBlancer实现负载均衡,从而使WEB服务的调用变得很简单。

OpenFeign已经是当前微服务调用最常用的技术

2.能干什么

前面的LoadBalancer章节,我们在使用LoadBalancer+RestTemplate实现了微服务的负载均衡调用,但是在实际开发中,一个接口往往会被多处调用,这就需要多次定义重复的代码,而OpenFeign简化了这个过程。

OpenFeign自带负载均衡

3.基本使用

  1. 引入OpenFeign和LoadBlancer的依赖

哪个服务需要调用其他服务的接口,就在哪个服务中引用【例如:订单服务调用支付服务的接口,就在订单服务中引入依赖】

引入LoadBlancer的依赖,是因为它使用LoadBlancer实现负载均衡

<!--openFeign-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer做负载均衡-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
  1. 启动类加上@EnableFeignClients注解,启动OpenFeign功能
  2. 在项目中创建一个api包,专门存放OpenFegin接口
  3. 在公共api包中创建OpenFeign的接口,

加上@FeignClient注解,注解的值就是被调用微服务的name

//例如:被调用的模块是支付模块,支付模块在注册中心的名字叫cloud-pay-service
@FeignClient("cloud-pay-service")
public interface PayFeignApi {}
  1. 编写接口中的方法
@FeignClient("cloud-pay-service")
public interface PayFeignApi {
    //方法上的注解就是被调用方法的请求类型和地址
    //这样他就合成了http://cloud-pay-service/pay/getall
    @GetMapping("/pay/getall")
    //这里的返回值需要和被调用接口的返回值一致
    ResultData getOrders();
}
  1. controller中注入feign接口对象,然后在需要的地方调用feign接口的方法
//注入feign对象
@Resource
private PayFeignApi payFeignApi;

@GetMapping("/consumer/pay/get/all")
public ResultData getPayAllInfo(){
    return payFeignApi.getOrders();
}

5.超时控制

问题引入:比较简单的业务使用默认配置是没有问题的,但是如果是复杂业务需要进行很多操作,就可能会出现Read Timeout异常。因此学习定制化超时时间是有必要的

  • OpenFeign客户端的默认等待时间60S,超过这个时间就会报错(这个时间太长了,我们应该设置短一点)

通过两个参数控制超时时间:

  • connectTimeout:连接超时时间【多长时间内必须建立链接】
  • readTimeout:请求处理超时时间【多长时间内必须处理完成】【默认60S】

5.1 全局配置

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
          #指定超时时间最大:3S
            read-timeout: 3000
            #指定连接时间最大:3S
            connect-timeout: 3000

5.2 指定配置

spring:
  cloud:
    openfeign:
      client:
        config:
        #这里将default换成微服务的名称
          cloud-pay-service:
          #指定超时时间最大:3S
            read-timeout: 3000
            #指定连接时间最大:3S
            connect-timeout: 3000

6.重试机制

超时之后不会直接结束请求,而是会重新尝试连接

重试机制默认是关闭的,如何开启呢?只需要编写一个配置类,配置Retryer对象

//1.创建一个配置类
@Configuration
public class RetryerConfig {
    //2.配置Retryer
    @Bean
    public Retryer retryer() {
        //3,设置重试机制
        //return Retryer.NEVER_RETRY;这个是默认的

        //第一个参数是多长时间后开启重试机制:这里设置100ms
        //第二个参数是重试的间隔:这里设置1s一次
        //第三个参数是最大请求次数:3次【这个次数是一共的,也就是最大请求几次,而不是第一次请求失败后再请求几次】
        return new Retryer.Default(100, 1, 3);
    }
}

7.连接池

OpenFeign允许指定连接方式,但是默认方式使用jdk自带的HttpURLConnection,但是HttpURLConnection不支持连接池,因此性能较低。

HttpClient和OkHttp都支持连接池,因此为了提升OpenFeign的性能,可以改成使用HttpClient5

  1. 引入HttpClient5和Feign-hc5依赖
<!-- 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>
  1. 在配置文件中开启hc5
spring:
  cloud:
    openfeign:
      httpclient:
        hc5:
          enabled: true

8.请求/响应压缩

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

spring:
  cloud:
    openfeign:
      compression:
        request:
        #开启请求压缩
          enabled: true
          #达到多大才触发压缩
          min-request-size: 2048
          #触发压缩的类型
          mime-types: types=text/xml,application/xml,application/json
        response:
          #开启响应压缩
          enabled: true

9.日志打印

OpenFeign需要输出日志需要符合两个条件:

  1. FeignClient所在的包日志级别为debug
  2. Feign的日志级别在NONE以上
Feign的日志级别:
	NONE:不记录任何日志信息,这是默认值。
	BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
	HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
	FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
  1. 定义一个类定义Feign的日志级别
@Configuration
public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.FULL;//日志级别
    }
}
  1. 配置文件中设置feign所在包的打印级别
logging:
  level:
    #下面是feign接口的包
    com.peng:
      apis:
        PayFeignApi: debug

CircuitBreaker断路器

1.基本介绍

分布式系统存在的问题:复杂的分布式应用程序,调用关系复杂,往往有数十个调用关系,调用关系在某些时候将不可避免的失败。比如:超时、异常等。因此我们需要一个框架保证在调用出现问的情况下,不会导致整体服务的失败,避免级联故障,从而提高分布式系统的弹性。


解决思路: 对于有问题的节点/服务,不再接受请求(快速返回失败处理,或者返回默认的兜底处理结果)


断路器就是这种开关装置。可以想象成家里的保险丝,假如家里真有某个电器发生了故障,能保证及时跳闸,别把整个家给烧了。

他的功能:

  1. 服务熔断:当达到最大访问后,直接拒绝访问,此时调用方会接收到服务降级的处理并返回有好的兜底提示【就好像电闸直接跳了】
  2. 服务降级:让用户的体验变差【返回简单的提示】,但是不会导致服务的雪崩
  3. 服务限流:限制访问微服务的请求的并发量,避免服务因流量激增出现故障【实现方法:前面加了一个限流器】
  4. 服务限时:只能在指定时间访问,其他时间均不可访问
  5. 服务预热:请求一点点放通,别一口气全进来
  6. 实时监控
  7. 兜底的处理动作

2.CircuitBreaker和Resilience4的关系

  • CircuitBreaker是一套抽象的规范
  • Resilience4J实现了CircuitBreaker的规范

3.CircuitBreaker的实现原理

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

  1. 正常状态处于close状态【闸刀闭合】
  2. 当一个服务或组件出现故障,CircuitBreaker会迅速切换到Open状态(跳闸断电),组织请求发送到该组件或服务,从而避免更多的请求发送到该组件或服务,防止组件或服务的进一步崩溃。
  3. 等待一段时间之后会尝试闭合Half_Open,放几个请求过来探探路,如果可以用了,就会转换到close状态,如果还不行就还是变成open状态。

Resilience4J

1.基本介绍

Resilience4J是一个轻量级的容错库,专门做服务熔断、降级等工作。

实现了CircuitBreaker规范。

Resilience4J 2要求使用Java17。

2.基本功能

核心模块:

  1. resilience4j-Circuitbreaker:断路
  2. resilience4j-ratelimiter:速率限制
  3. resilience4j-bulkhead:舱壁

断路【服务熔断+服务降级】

断路器3大状态

断路器状态转换
断路器有三个普通状态:关闭CLOSE【正常请求】、开启OPEN【断电不可用】、半开HALF_OPEN
	1.当熔断器处于CLOSE关闭状态,所有的请求都会通过熔断器。
	2.如果失败率超过设定的阈值,熔断器就会从关闭状态【CLOSE】转换到打开状态【OPEN】,
    这时所有的请求都会被拒绝
	3.当处于开启状态【OPEN】一段时间后,熔断器就会从开启状态转换到半开状态【HALF_OPEN】,
    这时会有一定数量的请求放入,并重新计算失败率
	4.如果失败率超过阈值,则会转成打开状态,如果低于阈值,则会变成关闭状态
	
还有两个特殊状态:DISABLED【始终允许访问】、FORCED_OPEN【始终拒绝访问】
                【这两个状态在生产中不会使用】
测试断路(推荐模式用COUNT_BASED)
<!--resilience4j-->
<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>
spring:
  cloud:
    #全局配置
    openfeign:
      # 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled
      circuitbreaker:
        enabled: true
        group:
          enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
         
# Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
#  6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
#  等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
#  如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
resilience4j:
  circuitbreaker:
    configs:
      default:
        #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
        failureRateThreshold: 50 
        #滑动窗口的类型
        slidingWindowType: COUNT_BASED 
        #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
        slidingWindowSize: 6 
        #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
        #如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。
        minimumNumberOfCalls: 6 
        #是否启用自动从开启状态过渡到半开状态,默认值为true。
        #如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
        automaticTransitionFromOpenToHalfOpenEnabled: true 
        #从OPEN到HALF_OPEN状态需要等待的时间,[恢复时间]
        waitDurationInOpenState: 5s 
        #半开状态允许的最大请求数,默认值为10。
        #在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,
        #如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。
        permittedNumberOfCallsInHalfOpenState: 2 
        recordExceptions:
          - java.lang.Exception
    instances:
      cloud-pay-service:
        baseConfig: default
@RestController
public class PayCircuitController {
    @GetMapping(value = "/pay/circuit/{id}")
    public String myCircuit(@PathVariable("id") Integer id) {
        if(id == -4) throw new RuntimeException("----circuit id 不能-4");
        if(id == 9999) {
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        return "Hello, circuit! inputId:  "+id+" \t " + IdUtil.simpleUUID();
    }
}
@Service
public interface PayFeignApi {
    @GetMapping(value = "/pay/circuit/{id}")
    String myCircuit(@PathVariable("id") Integer id);
}
@RestController
public class OrderCircuitController {

    @Resource
    private PayFeignApi payFeignApi;

    @GetMapping("/feign/pay/circuit/{id}")
    //name: 服务模块的名字
    //fallbackMethod:消费降级,调用的api爆出异常后,调用哪个方法
    @CircuitBreaker(name="cloud-pay-service",fallbackMethod = "myCircuitFallback")
    public String myCircuitBreaker(@PathVariable("id") int id) {
        return payFeignApi.myCircuit(id);
    }

    //消费降级,失败后调用此方法
    //注意!!! 如何异常在服务模块中爆出的话,全局异常会提前捕捉并返回数据,导致消费降级失败,
    //          所以要注意全局异常的设定
    public String myCircuitFallback(int id, Throwable throwable) {
        return "系统异常请稍后再试";
    }
}
按照时间的滑动窗口

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

隔离(resilience4j-bulkhead)两种方法都没测试出来...

Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量。

  • SemaphoreBulkhead 使用了信号量
  • FixedThreadPoolBulkhead 使用了有界队列和固定大小线程池

SemaphoreBulkhead可以在各种线程和I/O模型上正常工作。与Hystrix不同,它不提供基于shadow的thread选项。由客户端来确保正确的线程池大小与隔离配置一致。

SemaphoreBulkhead(信号量舱壁)

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

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

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

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

Bulkhead配置属性
代码演示
  1. cloud-consumer-feign-order80模块中添加bulk依赖:
<dependency>
  <groupId>io.github.resilience4j</groupId>
  <artifactId>resilience4j-bulkhead</artifactId>
</dependency>=
  1. cloud-consumer-feign-order80模块中配置yml文件
####resilience4j bulkhead 的例子
resilience4j:
  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
        maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
    instances:
      cloud-pay-service:
        baseConfig: default
timelimiter:
  configs:
    default:
      timeout-duration: 20s # 超时时间,超过这个时间,fallback兜底
  1. cloud-provider-payment8001模块中的PayCircuitController类中添加一个方法
@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();
}
  1. PayFeignApi接口中新增舱壁api方法
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id);
  1. OrderCircuitController中添加接口使用@Bulkhead启动舱壁隔离
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-pay-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ㄒ)/~~";
}
FixedThreadPoolBulkhead(固定线程池舱壁)

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

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

当线程池中无空闲时时,接下来的请求将进入等待队列,

若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,

在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。

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

FixedThreadPoolBulkhead配置

既然FixedThreadPoolBulkhead的原理是通过线程池来控制并发,该如何配置线程池呢,——> 通过yml文件可以配置线程池的最大线程数,核心线程数和阻塞队列,【切记最大线程池包含核心线程池】(下面的配置中当线程数达到6时将不再接收请求)\

代码演示
  1. 添加依赖:【跟信号量舱壁的依赖一样】
<!--resilience4j-bulkhead-->
<dependency>
  <groupId>io.github.resilience4j</groupId>
  <artifactId>resilience4j-bulkhead</artifactId>
</dependency>
  1. 修改配置yml配置文件
thread-pool-bulkhead:
  configs:
    default:
      core-thread-pool-size: 1
      max-thread-pool-size: 1
      queue-capacity: 1
  instances:
    cloud-pay-service:
      baseConfig: default
  1. 添加新的接口【跟信号量舱壁相比只是将类型改为固定线程池舱壁】
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-pay-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ㄒ)/~~");
}

限流

限流的基本概述:限流 就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。

(如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。)

所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。

限流算法

  1. 漏桶算法

一个固定容量的漏桶,按照设定常量固定速率流出水滴,类似医院打吊针,不管你源头流量多大,我设定匀速流出。 如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。

存在的缺陷:

这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。

  1. 令牌桶算法(SpringCloud默认使用的限流算法)

概述:你可以设置令牌的数量,每一个请求都需要用令牌才能执行,执行完后,将令牌回收,将回收的令牌派发给正在排队的请求。

  1. 滚动窗口算法

允许固定数量的请求进入(比如1秒取4个数据相加,超过25值就over)超过数量就拒绝或者排队,等下一个时间段进入。

由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。下图统计了3次,

存在的缺陷:

由于计数器算法存在时间临界点缺陷,因此在时间临界点左右的极短时间段内容易遭到攻击。

假如设定1分钟最多可以请求100次某个接口,如12:00:00-12:00:59时间段内没有数据请求但12:00:59-12:01:00时间段内突然并发100次请求,紧接着瞬间跨入下一个计数周期计数器清零;在12:01:00-12:01:01内又有100次请求。那么也就是说在时间临界点左右可能同时有2倍的峰值进行请求,从而造成后台处理请求加倍过载的bug,导致系统运营能力不足,甚至导致系统崩溃,/(ㄒoㄒ)/~~

滚动窗口算法会将时间划分为固定的窗口(例如每秒一个窗口),在每个窗口内计数请求。如果某个窗口内的请求数超过了设定的阈值,就会触发限流。这种方法在面对突发请求时,可能无法及时响应,因为每个窗口的阈值是固定的,无法动态调整。

(简单来说就是这种算法存在着一个时间间隔,当间隙时突然来了100各请求,就把服务器压垮了)

  1. 滑动时间窗口

顾名思义,该时间窗口是滑动的。所以,从概念上讲,这里有两个方面的概念需要理解:

  • 窗口:需要定义窗口的大小

  • 滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小

滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。下图统计了5次:

默认模式(令牌桶算法)代码演示
  1. 修改cloud-consumer-feign-order80,引入resilience4j限流依赖:
<!--resilience4j-ratelimiter-->
<dependency>
  <groupId>io.github.resilience4j</groupId>
  <artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
  1. 修改yml文件配置

####resilience4j ratelimiter 限流的例子
resilience4j:
  ratelimiter:
    configs:
      default:
        limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
        # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
        limitRefreshPeriod: 1s 
        timeout-duration: 1 # 线程等待权限的默认等待时间
    instances:
        cloud-pay-service:
          baseConfig: default
  1. cloud-provider-payment8001模块中PayCircuitController中新增方法进行测试
//=========Resilience4j ratelimit 的例子
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id){
    return "Hello, myRatelimit欢迎到来 inputId:  "+id+" \t " + IdUtil.simpleUUID();
}
  1. PayFeignApi中新增限流方法进行测试
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id);
  1. orderController中新增方法:使用@RateLimiter注解开启限流
@GetMapping(value = "/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-pay-service",fallbackMethod = "myRatelimitFallback")
public String myBulkhead(@PathVariable("id") Integer id) {
    return payFeignApi.myRatelimit(id);
}
public String myRatelimitFallback(Integer id,Throwable t) {
    return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
}
  1. 疯狂刷新页面,显示限流

Micrometer + ZipKin 分布式链路追踪 9411

Micronmeter简介:

Micrometer 为最流行的可观察性系统提供了一个简单的仪表客户端外观,让您可以对基于 JVM 的应用程序代码进行仪表化,而无需供应商锁定。想想 SLF4J,但用于应用程序可观察性!Micrometer 记录的数据旨在用于观察、警告和响应您环境的当前/近期运行状态

为什么会出现这个技术?

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

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

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

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

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

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

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

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

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

分布式链路追踪原理

假定三个微服务调用的链路如下图所示:Service 1 调用 Service 2,Service 2 调用 Service 3 和 Service 4。

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

链路通过TraceId唯一标识,

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

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

1第一个节点:Span ID = A,Parent ID = null,Service 1 接收到请求。
2第二个节点:Span ID = B,Parent ID= A,Service 1 发送请求到 Service 2 返回响应给Service 1 的过程。
3第三个节点:Span ID = C,Parent ID= B,Service 2 的 中间解决过程。
4第四个节点:Span ID = D,Parent ID= C,Service 2 发送请求到 Service 3 返回响应给Service 2 的过程。
5第五个节点:Span ID = E,Parent ID= D,Service 3 的中间解决过程。
6第六个节点:Span ID = F,Parent ID= C,Service 3 发送请求到 Service 4 返回响应给 Service 3 的过程。
7第七个节点:Span ID = G,Parent ID= F,Service 4 的中间解决过程。
8通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。

Zipkin概述

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

添加Zipkin

#docker 拉取Zipkin
docker pull openzipkin/zipkin

#启动
docker run -d \
--name zipkin \
-p 9411:9411 \
openzipkin/zipkin

#开启9411端口防火墙
firewall-cmd --permanent --add-port=9411/tcp
firewall-cmd --reload

#访问官网
192.168.37.130:9411/zipkin

代码演示(使用Zipkin和Micrometer)

  1. 在父工程中添加以下依赖
<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>

<!--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>
  1. cloud-provider-payment8001模块和cloud-consumer-feign-order80模块中引入以下依赖
<!--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>
  1. cloud-provider-payment8001模块和cloud-consumer-feign-order80模块配置yml文件
# ========================zipkin===================
management:
  zipkin:
    tracing:
      endpoint: http://192.168.37.130:9411/api/v2/spans
  tracing:
    sampling:
      probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
  1. 新建控制器PayMicrometerController
@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();
    }
}
  1. cloud-api-commons模块中的PayFeignApi接口中添加以下接口
@GetMapping(value = "/pay/micrometer/{id}")
public String myMicrometer(@PathVariable("id") Integer id)
  1. cloud-consumer-feign-order80模块中新增控制器,新建OrderMicrometerController接口
@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);
    }
}
  1. 调用http://localhost/feign/micrometer/1 接口后,zipkin官网显示

Gateway新一代网关

Gateway简介:

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

  • 前端通过 Nginx 转发请求到后端 (选择服务器)
  • 后端通过 GetWay 转发到对应的微服务模块。 (选择服务器中的微服务)

Gateway的功能:

  1. 反向代理
  2. 鉴权
  3. 流量控制
  4. 熔断
  5. 日志监控

Gateway原理:

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

Gateway三大核心概念:

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

Gateway代码演示

首先说明演示的流程:Gateway也是微服务的一部分(SpringCloud Gateway)所以第一步需要将Gateway注册到注册中心,在 cloud-consumer-order80 模块 调用 cloud-provider-payment8001模块前加上Getway网关。

Gatway微服务配置

  1. 创建Gateway模块,命名为cloud-getway9527
  2. 引入依赖:
<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>
  1. 配置yml文件,注册到consul中
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}
  1. 修改主启动类:
@SpringBootApplication
@EnableDiscoveryClient //服务注册和发现
public class Main9527 {
    public static void main(String[] args) {
        SpringApplication.run(Main9527.class,args);
    }
}

将8001微服务与GateWay9527端口打通

配置完成网关微服务模块后,我们配置提供者colud-provider-payment8001模块,我们不想暴露8001端口,希望在8001真正的支付微服务外面套一层9527网关。进行如下配置:

  1. 在提供者8001模块中创建 PayGateWayController 控制器:
@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());
    }
}
  1. 配置Gateway对提供者8001模块的路由判断与断言:【在Gateway模块中的yml文件中进行配置】
  • 配置一个id,自定义一个标识
  • 配置一个uri,本机ip+提供者模块端口
  • 配置predicates断言,对访问的地址进行断言限制
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/**      # 断言,路径相匹配的进行路由
  1. 测试结果说明:

重启Gateway9527模块后,我们访问:http://localhost:8001/pay/gateway/get/1,这个是肯定没问题的,访问http://localhost:9527/pay/gateway/get/1,通过9527这个端口访问8001模块的的接口也同样通过,自此就打通了网关到提供者的路由配置

将订单模块通过Feign访问到Gateway网关打通

  1. cloud-api-commons 模块中的PayFeignApi接口中添加以下两个方法:
@GetMapping(value = "/pay/gateway/get/{id}")
ResultData getById(@PathVariable("id") Integer id);

@GetMapping(value = "/pay/gateway/info")
ResultData getGatewayInfo();
  1. cloud-consumer-feign-order80模块中创建一个新的控制器OrderGateController
@RestController
public class OrderGateController {
    @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 getGatewayInfo() {
        return payFeignApi.getGatewayInfo();
    }
}
  1. 修改cloud-api-commons的PayFeignApi接口,将@FeignClient注解修改为@FeignClient("cloud-gateway"),意思为:访问网关微服务
@FeignClient("cloud-gateway") //不直接调用cloud-pay-service,通过网关调用微服务
@Service
public interface PayFeignApi {
}
  1. 测试8端口接口成功后,这样整条链路调用成功。

Gateway高级特性

特性一:Route以微服务名 - 动态获取服务URL

存在的问题:

使用动态获取服务URL来解决URL写死的问题:

修改Gateway模块的yml配置文件:【lb为负载均衡,后面是所访问的服务名】

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

特性二:Predicate断言(谓词)

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

配置断言有两种语法:
  1. Shortcut Configuration 使用等于号和逗号进行配置

  1. Fully Expanded Arguments 这种方式类似于yml的语法,使用的key-value进行配置
有关Predicate断言的配置
After/Before/Between Route Predicate Factory(在某个时间点之后才开放)

路由After谓词工厂采用一个参数 a datetime(即 java ZonedDateTime)。此谓词匹配在指定日期时间之后发生的请求。

那相对应的Before和Between就是在某个时间点之前才能访问,在某个时间点之间才能访问。

应用场景:预约活动要在某个时间之后才能访问…等等

格式:- After=2017-01-20T17:42:47.789-07:00[America/Denver]

spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: https://example.org
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]

问题:这个时间点该怎么写? —— > 使用java的时区,随便定义一个普通的java类编写以下代码就能生成时间格式:

public class ZonedDateTimeDemo{
    public static void main(String[] args){
        ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
        System.out.println(zbj);
    }
}
  • Before Route Predicate Factory示例代码:
spring:
  cloud:
    gateway:
      routes:
      - id: before_route
        uri: https://example.org
        predicates:
        - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
  • Between Route Predicate Factory示例代码:
spring:
  cloud:
    gateway:
      routes:
      - id: between_route
        uri: https://example.org
        predicates:
        - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
Cookie Route Predicate Factory

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

其主要功能就是限制前端访问需要携带上我对应配置的cookie才能让网关放行。

设置cookie配置示例如下:

spring:
  cloud:
    gateway:
      routes:
      - id: cookie_route
        uri: https://example.org
        predicates:
        - Cookie=[key], [value或正则表达式]

Header Route Predicate

前端访问必须要有Getway所对应配置的请求头

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

spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: https://example.org
        predicates:
        - Header=X-Request-Id, \d+  # 请求头要有X-Request-Id属性并且值为整数的正则表达式

Host Route Predicate

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

spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: https://example.org
        predicates:
        - Host=**.atguigu.com

Path Route Predicate

必须是所配置的路由

spring:
  cloud:
    gateway:
      routes:
      - id: path_route
        uri: https://example.org
        predicates:
        - Path=/pay/gateway/get/**         # 断言,路径相匹配的进行路由
Query Route Predicate

只能是对应的请求参数才能放行

spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
       - Query=username, \d+  # 要有参数名username并且值还要是整数才能路由

RemoteAddr route predicate

只有指定的主机地址才能放行

spring:
  cloud:
    gateway:
      routes:
      - id: remoteaddr_route
        uri: https://example.org
        predicates:
        - RemoteAddr=192.168.124.1/24 # 外部访问我的IP限制,最大跨度不超过32,目前是1~24它们是 CIDR 表示法。
Method Route Predicate

只有指定的请求方法才能放行

spring:
  cloud:
    gateway:
      routes:
      - id: method_route
        uri: https://example.org
        predicates:
        - Method=GET,POST
自定义断言【重点】

观察AfterRouterPredicateFactory 类的源码,我们可以看到大致的步骤:

前置规则:自定义一个断言的类名一定要以xxxRouterPredicateFactory命名,并且将该类用@component注解标识

代码演示(场景:自定义配置会员等级userTyp,按照钻/金/银等级,判断是否可以访问)

  1. 继承AbstractRouterPredicateFactory类并且该泛型为`xxxRouterPredicateFactory.Config
  2. 编写一个内部类,其命名一定要为Config,该类定义一个userType(因为这里的场景是根据会员等级判断是否放行)
  3. 编写空参的构造方法,并且使用super调用上一步定义的Config类
  4. 实现apply方法,该方法中return new Predicate,并在这里写判断逻辑
  5. 若想yml文件中支持 Shortcut Configuration 这种配置文件需要实现shortcutFieldOrder方法
//自定义配置会员等级userTyp,按照钻/金/银等级,判断是否可以访问
@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config> {

    public MyRoutePredicateFactory(){
        super(MyRoutePredicateFactory.Config.class);
    }
    
    //支持Shortcut Configuration格式,短触模式
    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("userType","userName");
    }

    @Override
    public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config) {
        return new Predicate<ServerWebExchange>() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                //serverWebExchange类似于request,获取请求参数userType对应的值进行会员逻辑判断
                String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");
                if(userType == null){
                    return false;
                }
                //判断前端传过来的userType是否和yml文件定义的相同
                if(userType.equals(config.getUserType())){
                    return true;
                }
                return false;
            }
        };
    }

    @Validated
    public static class Config{
        @Getter@Setter@NotEmpty
        private String userType;
        @Getter@Setter@NotEmpty
        private String userName;
    }
}
spring:
  cloud:
    gateway:
      routes:
      - id: method_route
        uri: https://example.org
        predicates:
        # 根据MyRoutePredicateFactory xxxRoutePredicateFactory
        - My=gold

特性三:Filter(过滤器)

类似于 SpringMVC里面的拦截器 Interceptor,Servlet的过滤器,"pre"和"post"分别在请求被执行前调用和被执行后调用,用来修改请求和响应信息。

Filter的主要作用:
  1. 请求鉴权
  2. 异常处理
  3. 记录接口调用时长统计(大厂面试题)
Filter过滤器的类型:
  1. 全局默认过滤器 Global Filters:gateway出厂默认已有的,直接用即可,主要作用于所有的路由,不需要在配置文件中配置,作用在所有的路由上,实现GlobalFilter接口即可
  2. 单一内置过滤器 Getway Filter:也可以称为网关过滤器,这种过滤器主要是作用于单一路由或者某个路由分组
  3. 自定义过滤器
Gateway内置过滤器

请求头(RequestHeader)相关组

The AddRequestHeader GatewayFilter Factory(添加请求头内容)

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

这段代码获取前端发送请求的请求信息,并且进行对逻辑进行判断。

在9527网关yml配置文件中添加过滤内容:

- id: pay_routh3 #pay_routh3
  uri: lb://cloud-pay-service       #匹配后提供服务的路由地址
  predicates:
    - Path=/pay/gateway/filter/**   # 断言,路径相匹配的进行路由
  filters:                          # 请求头kv键值对,若一头含有多参则重写一行设置
    - AddRequestHeader=X-Request-atguigu1,atguiguValue1  
    - AddRequestHeader=X-Request-atguigu2,atguiguValue2
The RemoveRequestHeader GatewayFilter Factory(删除请求头)
- RemoveRequestHeader=sec-fetch-site      # 删除请求头sec-fetch-site

修改前:

修改后:

The SetRequestHeader GatewayFilter Factory(修改请求头信息)
# 将请求头sec-fetch-mode对应的值修改为Blue-updatebyzzyy
- SetRequestHeader=sec-fetch-mode, Blue-updatebyzzyy 

修改前:

修改后:

请求参数(RequestParameter)相关组
  1. The AddRequestParameter GatewayFilter Factory:*新增请求参数Parameter:*k *,*v
  2. The RemoveRequestParameter GatewayFilter Factory:*新增请求参数Parameter:*k *,*v
- AddRequestParameter=customerId,9527001 # 新增请求参数Parameter:k ,v
- RemoveRequestParameter=customerName   # 删除url请求参数customerName,你传递过来也是null

为了方便测试,修改8001微服务的PayGatewayController中的getGatewayFilter方法:

@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("request headName:" + headName +"---"+"request headValue:" + 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());
}

访问:http://localhost:9527/pay/gateway/filter,得出如下结果:


访问:http://localhost:9527/pay/gateway/filter?customerId=9999&customerName=z3 ,得出如下结果:

回应头(ResponseHeader)相关组
  1. The AddRepsonseHeader GatewayFilter Factory
  2. The SetRepsonseHeader GateFilter Factory
  3. The RemoveResonseHeader GateFilter Factory
# 新增请求参数X-Response-atguigu并设值为BlueResponse
- AddResponseHeader=X-Response-atguigu, BlueResponse 
# 设置回应头Date值为2099-11-11
- SetResponseHeader=Date,2099-11-11 
# 将默认自带Content-Type回应属性删除
- RemoveResponseHeader=Content-Type 

进行测试:访问:http://localhost:9527/pay/gateway/filter,测试结果如下:

原本的回应信息是这样的:

修改后的回应信息:

前缀和路径相关组
1.1. The PrefixPath GatewayFilter Factory(自动添加路径前缀)

场景:为了保护地址,我们可以使用隐藏掉前缀的方法进行真实地址的隐藏,原正确地址: http://localhost:9527/pay/gateway/filter ,这个地址也是我们控制层所映射的真实地址,通过以下yml文件的修改后,该地址访问不同,访问: http://localhost:9527/gateway/filter ,才能通过,实际就是把前缀隐藏了。

predicates:
 #- Path=/pay/gateway/filter/**   # 被分拆为: PrefixPath + Path
 - Path=/gateway/filter/**        # 断言,为配合PrefixPath测试过滤,暂时注释掉/pay
filters:
 - PrefixPath=/pay # http://localhost:9527/pay/gateway/filter      

测试结果:

1.2. The SetPath GatewayFilter Factory(访问路径修改)

对访问路径的修改,目的也是为了隐藏后端真实的地址。

predicates:
	- Path=/XYZ/abc/{segment}  # 断言,为配合SetPath测试,{segment}的内容最后被SetPath取代
filters:
#- PrefixPath=/pay # http://localhost:9527/pay/gateway/filter
	- SetPath=/pay/gateway/{segment}  # {segment}表示占位符,你写abc也行但要上下一致

yml配置文件说明:【对于这个{segment},意思为前端传过来什么我就对应映射什么,比如:前端传来 /XYZ/abc/abc,后端就会映射成/page/gateway/abc,所以只要前端传来的路径前缀包含/XYZ/abc能对应得上就会映射成/pay/gateway/


{segment}就是个占位符,等价于SetPath后面指定的{segment}内容

测试结果:

1.3. The RedirectTo GatewayFilter Factory

重定向到某个页面

predicates:
	- Path=/pay/gateway/filter/** # 真实地址
filters:	# 访问http://localhost:9527/pay/gateway/filter跳转到http://www.baidu.com/
	- RedirectTo=302, http://www.baidu.com/ 
Gateway自定义过滤器
问题:如何统计每个接口调用耗时情况,——> 使用Gateway自定义过滤器。

如何自定义Gateway过滤器?

参考官网自定义过滤器的示例代码:

@Bean
public GlobalFilter customFilter() {
    return new CustomGlobalFilter();
}

public class CustomGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("custom global filter");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -1;
    }
}
步骤:
  1. 新建一个类标注为@Component,并且该类必须实现GlobalFilter和Ordered两个接口并重写其接口方法
  2. 因为这里的场景是要开始的时间和结束的时间,在ServerWebExchange类中用一个getAttributes的方法能够存放Map,所以我们可以定义一个常量key,value就是当前时间System.currentTimeMillis
  3. 参照官网示例,return chain.filter(exchange),但这里我们需要写时间记录逻辑,所以写为return chain.filter(exchange).then(Mono.fromRunnable(() ->{}))
@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;
    }
}
自定义条件GatewayFilter

上面的这种方式是全局的网关Filter过滤器,下面要进行条件网关过滤的配置,步骤如下:

  1. 新建类名XXX需要以GatewayFilterFactory结尾并继承AbstractGatewayFilterFactory类,重写其抽象方法
  2. 新建XXXGatewayFilterFactory.Config内部类,这个类写的是对应业务逻辑的变量
  3. 在重写的apply方法中编写业务逻辑
  4. 重写shortcutFieldOrder,重写这个类是为了能够支持yml配置文件短促式格式
  5. 构造空参构造方法,调用super
@Component
public class MyGateFilterFactory extends AbstractGatewayFilterFactory<MyGateFilterFactory.Config> {
    public MyGateFilterFactory(){
        super(MyGateFilterFactory.Config.class);
    }

    //支持Shortcut Configuration格式,短触模式
    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("status");
    }

    @Override
    public GatewayFilter apply(MyGateFilterFactory.Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                ServerHttpRequest request = exchange.getRequest();
                if(request.getQueryParams().containsKey("Love")){
                    return chain.filter(exchange);
                }else{
                    exchange.getResponse().setStatusCode(HttpStatus.BAD_GATEWAY);
                    return exchange.getResponse().setComplete();
                }
            }
        };
    }
    
    public static class Config{
        @Setter @Getter
        private String status;
    }
}
filters:
 - My=Love

需要携带参数"Love" localhost:9527/pay/gateway/filter?Love=java 才能进行连接