SpringCloud

版本适配

可通过查阅官方文件,可得springCloud适配的SpringBoot和JAVA的版本,建议通过GitHub对应的官方文档来查阅

SpringCloud作用

是将一系列微服务操作整合到一起

image-20240329095859551

  1. 服务网关:Vue前台页面不应该直接知道后台服务器的真实地址,所以需要通过服务网关来实现
  2. 负载均衡:后台服务器不可能只有一个,需要对多个服务器进行调用,通过负载均衡来实现
  3. 分布式事务:后台数据库也不可能只有一个,为了能够实现各个数据库之间的数据一致,通过分布式事务来实现
  4. 服务熔断:当后台有服务器失效的时候,需要通过服务熔断来进行服务降级监控

SpringCloud就是将上述功能进行整合

image-20240329100708056

如果有一些老项目还是需要使用一些不流行的框架

image-20240329102114242

Base工程模块构建

  1. 构建父工程Maven

    设置编码:设置 - 编译器 - 文件编码 - 全部UTF-8

    设置注解生效:设置 - 构建执行部署 - 编译器 - 注解处理器 - 勾上启用注解

    设置JDK版本:设置 - 构建执行部署 - 编译器 - JAVA编译器 - 设置目标字节码版本为17

    设置忽略文件类型:设置 - 编辑器 - 文件类型 - 忽略文件和文件夹

    image-20240329103639958

  2. 设置父工程pom文件

    依赖引入报错是因为dependencyManagement只是管理包依赖,并没有真正的下载,可以先将dependencyManagement标签去掉然后刷新maven项目,等待包下载完毕之后,再添加上dependencyManagement标签就不会爆红了

    dependencyManagement标签的作用:让子项目不用在显示的声明依赖项的版本号,都沿用父项目中定义的,避免引入出错,升级方便

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    <?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.2</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. 创建子工程,子工程pom文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    <?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>19</maven.compiler.source>
    <maven.compiler.target>19</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</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>
    </project>
  4. 配置数据库,外加mapper service entity生成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    spring:
    datasource:
    url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: admin
    driver-class-name: com.mysql.cj.jdbc.Driver

    mybatis-plus:
    # 匿名域
    type-aliases-package: com.auguigu.domain.entity
    # mapper文件映射路径
    mapper-locations: classpath:mybatis/mapper/*.xml
    configuration:
    # # 日志文件
    # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
    cache-enabled: true
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
    global-config:
    db-config:
    logic-not-delete-value: 0
    logic-delete-field: deleted
    logic-delete-value: 1
    id-type: auto
  5. 创建微服务

    1. 建module

    2. 改POM

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      <?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>org.example</groupId>
      <artifactId>cloud2024</artifactId>
      <version>1.0-SNAPSHOT</version>
      </parent>

      <groupId>com.atguigu.cloud</groupId>
      <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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      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: admin

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      package com.atguigu.cloud;

      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import tk.mybatis.spring.annotation.MapperScan;

      @SpringBootApplication
      @MapperScan("com.atguigu.cloud.mapper")
      public class Main8001 {
      public static void main(String[] args) {
      SpringApplication.run(Main8001.class);
      }
      }
    5. 业务类

      mapper

      1
      2
      3
      4
      5
      6
      7
      package com.atguigu.cloud.mapper;

      import com.atguigu.cloud.domain.entities.Pay;
      import tk.mybatis.mapper.common.Mapper;

      public interface PayMapper extends Mapper<Pay> {
      }

      service

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      package com.atguigu.cloud.service;

      import com.atguigu.cloud.domain.entities.Pay;

      import java.util.List;

      public interface PayService {
      public int add(Pay pay);
      public int delete(Integer id);
      public int update(Pay pay);
      public Pay getById(Integer id);
      public List<Pay> getAll();
      }

      serviceimpl

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      package com.atguigu.cloud.service.impl;

      import com.atguigu.cloud.domain.entities.Pay;
      import com.atguigu.cloud.mapper.PayMapper;
      import com.atguigu.cloud.service.PayService;
      import jakarta.annotation.Resource;
      import org.springframework.stereotype.Service;

      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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      package com.atguigu.cloud.controller;

      import com.atguigu.cloud.domain.DTO.PayDTO;
      import com.atguigu.cloud.domain.entities.Pay;
      import com.atguigu.cloud.service.PayService;
      import com.atguigu.cloud.utils.BeanCopyUtils;
      import jakarta.annotation.Resource;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.BeanUtils;
      import org.springframework.web.bind.annotation.*;

      @RestController
      @Slf4j
      public class PayController {
      @Resource
      private PayService payService;

      @PostMapping("/pay/add")
      public String addPay(@RequestBody Pay pay) {
      int i = payService.add(pay);
      return "success: " + i;
      }
      @DeleteMapping("/pay/del/{id}")
      public String deletePay(@PathVariable("id") Integer id) {
      int i = payService.delete(id);
      return "success: " + i;
      }

      @PutMapping("/pay/update")
      public String updatePay(@RequestBody PayDTO payDTO) {
      Pay pay = BeanCopyUtils.CopyBean(payDTO, Pay.class);
      int i = payService.update(pay);
      return "success: " + i;
      }
      }

      Utils

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      package com.atguigu.cloud.utils;

      import org.springframework.beans.BeanUtils;

      import java.util.List;
      import java.util.stream.Collectors;

      public class BeanCopyUtils {
      /**
      * 拷贝单个对象
      * @param o
      * @param clazz
      * @return
      * @param <V>
      */
      public static <V> V CopyBean(Object o, Class<V> clazz) {
      V result = null;
      try {
      result = clazz.getDeclaredConstructor().newInstance();
      BeanUtils.copyProperties(o, result);
      } catch (Exception e) {
      throw new RuntimeException(e);
      }
      return result;
      }

      /**
      * 复制多个对象
      * @param list
      * @param clazz
      * @return
      * @param <O>
      * @param <V>
      */
      public static <O, V> List<V> CopyBeanList(List<O> list, Class<V> clazz) {
      return list.stream()
      .map(o -> CopyBean(o, clazz))
      .collect(Collectors.toList());
      }
      }

      实体类 :PayDTO 和 Pay

测试

swagger3测试

引入依赖

1
2
3
4
5
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>

@Tag注解:标识在Controller类上,表明这个Controller的作用是什么

1
2
3
4
@RestController
@Slf4j
@Tag(name="支付模块", description = "支付CURD")
public class PayController {}

@Operation:标识在Controller类中的接口上,标识该接口的租用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@Slf4j
@Tag(name="支付模块", description = "支付CURD")
public class PayController {
@Resource
private PayService payService;

@PostMapping("/pay/add")
@Operation(summary = "新增", description = "新增流水,JSON为参数")
public String addPay(@RequestBody Pay pay) {
int i = payService.add(pay);
return "success: " + i;
}
}

@Schema:标识在实体类和对应的属性上表明该实体类的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 表名:t_pay
* 表注释:支付交易表
*/
@Table(name = "t_pay")
@ToString
@Schema(title = "支付交易Entity")
public class Pay {
@Id
@GeneratedValue(generator = "JDBC")
@Schema(title = "支付交易ID")
private Integer id;

/**
* 支付流水号
*/
@Column(name = "pay_no")
@Schema(title = "支付流水号")
private String payNo;

/**
* 订单流水号
*/
@Column(name = "order_no")
@Schema(title = "订单流水号")
private String orderNo;

/**
* 用户账号ID
*/
@Column(name = "user_id")
@Schema(title = "用户账号ID")
private Integer userId;

/**
* 交易金额
*/
@Schema(title = "交易金额")
private BigDecimal amount;

/**
* 删除标志,默认0不删除,1删除
*/
@Schema(title = "删除标志,默认0不删除,1删除")
private Integer deleted;

/**
* 创建时间
*/
@Column(name = "create_time")
@Schema(title = "创建时间")
private Date createTime;

/**
* 更新时间
*/
@Column(name = "update_time")
@Schema(title = "更新时间")
private Date updateTime;
}

创建Swagger3配置类

config.Swagger3Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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/"));
}
}

image-20240330124829404

时间格式问题

方法一:注解法

在相应的类的属性上使用@JsonFormat注解:

两个写一个即可,建议写上面的

1
2
3
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date billtime;

方法二:配置文件

application.yml

1
2
3
4
spring:
jsckson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8

统一返回值问题

后端返回是数据不建议直接返回String List 等各种各样的类型

所以需要统一返回值

返回值格式:

  1. code状态值:有后端统一定义各种返回值结果

    分类 区间 分类描述
    1** 100-199 信息,服务器收到请求,需要请求者继续执行操作
    2** 200-299 成功,操作被成功接受并处理
    3** 300-399 重定向,需要进一步的操作已完成请求
    4** 400-499 客户端错误,请求包含语法错误或无法完成请求
    5** 500-599 服务器错误,服务器在请求过程中发生了错误
  2. message描述:本次接口调用的结果描述

  3. data数据:本次返回的数据

  4. (扩展)接口调用时间之类:timestamp接口调用时间

设置方式

  1. 新建枚举类AppHttpCodeEnum

    构造枚举三步

    举值(例如下面的SUCCESS()) -

    构造(举值中有几个参数就构造几个变量, 创建构造方法将这几个变量初始化) -

    遍历()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    package com.atguigu.cloud.enums;

    import java.util.Arrays;

    public enum AppHttpCodeEnum {
    // 成功
    SUCCESS(200,"操作成功"),
    /**操作失败**/
    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, "不支持的认证模式"),
    SYSTEM_ERROR(500, "出现错误");

    int code;
    String msg;

    AppHttpCodeEnum(int code, String errorMessage){
    this.code = code;
    this.msg = errorMessage;
    }

    public int getCode() {
    return code;
    }

    public String getMsg() {
    return msg;
    }

    public static AppHttpCodeEnum getReturnCodeEnum(int code) {
    return Arrays.stream(AppHttpCodeEnum.values())
    .filter(e -> e.getCode() == code)
    .findFirst()
    .orElse(null);
    }
    }

  2. 新建统一定义返回对象ResponseResult

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    package com.atguigu.cloud.domain;

    import com.atguigu.cloud.enums.AppHttpCodeEnum;
    import com.fasterxml.jackson.annotation.JsonInclude;

    import java.io.Serializable;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    public class ResponseResult<T> implements Serializable {
    private Integer code;
    private String msg;
    private T data;
    private long timestamp;

    public ResponseResult() {
    this.code = AppHttpCodeEnum.SUCCESS.getCode();
    this.msg = AppHttpCodeEnum.SUCCESS.getMsg();
    this.timestamp = System.currentTimeMillis();
    }

    public ResponseResult(Integer code, T data) {
    this.code = code;
    this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data) {
    this.code = code;
    this.msg = msg;
    this.data = data;
    }

    public ResponseResult(Integer code, String msg) {
    this.code = code;
    this.msg = msg;
    }

    public static ResponseResult errorResult(int code, String msg) {
    ResponseResult result = new ResponseResult();
    return result.error(code, msg);
    }
    public static ResponseResult okResult() {
    ResponseResult result = new ResponseResult();
    return result;
    }
    public static ResponseResult okResult(int code, String msg) {
    ResponseResult result = new ResponseResult();
    return result.ok(code, null, msg);
    }

    public static ResponseResult okResult(Object data) {
    ResponseResult result = setReturnCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg());
    if(data!=null) {
    result.setData(data);
    }
    return result;
    }

    public static ResponseResult errorResult(AppHttpCodeEnum enums){
    return setReturnCodeEnum(enums,enums.getMsg());
    }

    public static ResponseResult errorResult(AppHttpCodeEnum enums, String msg){
    return setReturnCodeEnum(enums,msg);
    }

    public static ResponseResult setReturnCodeEnum(AppHttpCodeEnum enums){
    return okResult(enums.getCode(),enums.getMsg());
    }

    private static ResponseResult setReturnCodeEnum(AppHttpCodeEnum enums, String msg){
    return okResult(enums.getCode(),msg);
    }

    public ResponseResult<?> error(Integer code, String msg) {
    this.code = code;
    this.msg = msg;
    return this;
    }

    public ResponseResult<?> ok(Integer code, T data) {
    this.code = code;
    this.data = data;
    return this;
    }

    public ResponseResult<?> ok(Integer code, T data, String msg) {
    this.code = code;
    this.data = data;
    this.msg = msg;
    return this;
    }

    public ResponseResult<?> ok(T data) {
    this.data = data;
    return this;
    }

    public Integer getCode() {
    return code;
    }

    public void setCode(Integer code) {
    this.code = code;
    }

    public String getMsg() {
    return msg;
    }

    public void setMsg(String msg) {
    this.msg = msg;
    }

    public T getData() {
    return data;
    }

    public void setData(T data) {
    this.data = data;
    }

    }

统一异常处理

无需再写try…catch…finally

不用关注错误代码,只需要关注正确结果即可

创建系统异常对象

exception.SystemException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.atguigu.cloud.exception;

import com.atguigu.cloud.enums.AppHttpCodeEnum;

public class SystemException extends RuntimeException{
private int code;
private String msg;

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public SystemException(AppHttpCodeEnum returnCodeEnum){
super();
this.code = returnCodeEnum.getCode();
this.msg = returnCodeEnum.getMsg();
}
}

创建异常处理类

handler.exception.GlobalExceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.atguigu.cloud.handler.exception;

import com.atguigu.cloud.domain.ResponseResult;
import com.atguigu.cloud.enums.AppHttpCodeEnum;
import com.atguigu.cloud.exception.SystemException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j // 日志文件
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 系统异常
* @param e
* @return
*/
@ExceptionHandler(SystemException.class)
public ResponseResult systemException(SystemException e) {
log.error("出现了异常 {}", e);
return ResponseResult.errorResult(e.getCode(), e.getMsg());
}

@ExceptionHandler(Exception.class)
public ResponseResult exceptionHandler(Exception e){
log.error("出现了异常", e);
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(), AppHttpCodeEnum.SYSTEM_ERROR.getMsg());
}
}

RestTemplate

RestTemplate提供了多种便捷访问远程Http服务的方法

是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集

一个模块服务调用另一个模块服务

官网:https://docs.spring.io/spring-framework/docs/6.1.4/javadoc-api/org/springframework/web/client/RestTemplate.html

两种方式

restTemplate.getForObject(String url, Class< T > responseType, Object… uriVariables)

restTemplate.getForEntity(String url, Class< T > responseType, Object… uriVariables)

这三个参数分别代表

Rest请求地址,Http相应被转换成的对象类型, 请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id, ResponseResult.class);
}

@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id) {
ResponseResult entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id, ResponseResult.class);
if(eneity.getStatusCOde().is2xxSuccessful()) {
return entity.getBody();
} else {
return new ResponseResult(444, "操作失败");
}
}

创建restTemplate配置类

1
2
3
4
5
6
7
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

工程重构

将多个模块中公共的代码提取出来

image-20240330204843464

新建cloud-api-commons模块将公共部分提取出来

改POM文件 引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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>

将公共部分提取到该文件中

删除源文件中的内容

在公共项目中install一下

在原项目的pom文件中引入公共项目

1
2
3
4
5
6
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>

服务注册与发现 Consul

为什么要引入微服务

上面Controller问题

1
public static final String PaymentSrv_URL = "http://localhost:8001";

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

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

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

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

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

Eureka停用的原因

  1. Eureka停更进维
  2. Eureka对初学者不好
  3. 注册中心独立和微服务功能解耦,Eureka服务和程序员开发的微服务混在一起
  4. 阿里巴巴Nacos的崛起

什么是consul

Consul 是一套开源的分布服务发现和配置管理系统

SpringCloudConsul官网:https://spring.io/projects/spring-cloud-consul

作用:

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

  2. 健康监测:支持多种方式,HTTP、TCP、Docker、Shell脚本定制

  3. KV存储:KeyValue存储方式

  4. 多数据中心:Consul支持多数据中心

  5. 可视化Web界面

consul的使用:https://docs.spring.io/spring-cloud-consul/docs/current/reference/html/

下载

  1. consul下载路径:https://developer.hashicorp.com/consul/install?product_intent=consul

    win系统控制台输入wmic cpu get AddressWidth 显示64就用AMD64

  2. 在解压路径下的文件夹下使用consul --version 如果输出信息则表明下载正确

  3. 在开发模式下使用

    • consul agent -dev
    • 通过http://localhost:8500访问Consul首页

使用

服务注册与发现

服务提供者8001

  • 修改pom文件

    1
    2
    3
    4
    5
    <!-- springCloudConsul依赖 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
  • 修改yml文件:

    1
    2
    3
    4
    5
    6
    7
    spring:  
    cloud:
    consul:
    host: localhost
    port: 8500
    discovery:
    service-name: ${spring.application.name}
  • 修改写死的微服务网址

    1
    2
    // public static final String PaymentSrv_URL = "http://localhost:8001";
    public static final String PaymentSrv_URL = "http://cloud-payment-service";
  • 在RestTemplate配置类上添加@LoadBalanced注解,表明让testTemplate支持负载均衡

    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    public class RestTemplateConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
    return new RestTemplate();
    }
    }

    如果不添加负载均衡会出现:I/O error on GET request for “http://cloud-payment-service/pay/get/2”: cloud-payment-service

    因为SpringCloudConsul默认支持负载均衡,因为我们微服务提供者有两个,在消费者发送请求给生产者的两个微服务的过程中,服务器并不知道我们到底给哪一个生产者去处理,所以报这个异常。

    所以在RestTemplate上需要添加@LoadBalanced注解,让服务器知道采用轮询的负载均衡方式进行请求即可

三个注册中心的异同点:

CPA

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

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

CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,因此,根据 CAP 原理将 NOSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三大类:

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

CP - 满足一致性,分区容忍必的系统,通常性能不是特别高。

AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些

组件名 语言 CAP 服务健康检查 对外暴露接口 SpringCloud集成
Eureka Java AP 可配支持 HTTP 已集成
Consul Go CP 支持 HTTP/DNS 已集成
Zookeeper JAVA CP 支持 客户端 已集成

分布式配置

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

当下我们每一个微服务自己带着一个application.yml,上百个配置文件的管理…/(ToT)/~~

1. 配置对应项目的pom文件

添加SpringCloudConfig依赖项

1
2
3
4
5
6
7
8
9
<!-- 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>

2.配置yml文件

通过配置bootstrap.yml文件提取所有项目中application.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

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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: admin
profiles:
active: dev # 多环境配置加载内容dev/prod,不写就是默认的default配置 dev开发环境,prod生产环境

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

bootstrap.yml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: cloud-payment-service
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
profile-separator: '-' # 默认文件分割服为“,” 我们将其设置为“-”
format: YAML

3.consul中KeyValue配置

参考规则:文件夹路径默认是",“分割,上面配置文件已改成”-"

1
2
3
config/cloud-payment-service/data
config/cloud-payment-service-dev/data
config/cloud-payment-service-prod/data

配置路径严格按照上面的来建立

image-20240401171535937

测试结果

1
2
3
4
5
6
7
@Value("${server.port}")
public String port;

@GetMapping("/pay/get/info")
public String getInfoByConsul(@Value("${nuyoah.info}") String nuyoahInfo) {
return "nuyoahInfo: " + nuyoahInfo + "\t" + "port: "+ port;
}

动态刷新

  1. 主启动类添加**@RefreshScope**

  2. 设置刷新时间(一般不建议设置,默认时间55s)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    spring:
    application:
    name: cloud-payment-service
    cloud:
    consul:
    host: localhost
    port: 8500
    discovery:
    service-name: ${spring.application.name}
    config:
    profile-separator: '-' # 默认文件分割服为“,” 我们将其设置为“-”
    format: YAML
    watch:
    wait-time: 30 # 设置自动刷新时间

KeyValue持久化

在consul.exe的文件夹下新建consul_start.bat文件写入

1
2
3
4
5
6
7
@echo.服务启动......  
@echo off
@sc create Consul binpath= "H:\SpringCloud\Consul\consul.exe agent -server -ui -bind=127.0.0.1 -client=0.0.0.0 -bootstrap-expect 1 -data-dir H:\SpringCloud\Consul\mydata "
@net start Consul
@sc config Consul start= AUTO
@echo.Consul start is OK......success
@pause

在consul.exe的文件夹下新建mydata文件夹

负载均衡 LoadBalance

什么是LoadBalance

官网:LoadBalance)

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

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

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

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

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

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

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

LoadBalance的使用

image-20240402093313321

LoadBalancer 在工作时分成两步:

第一步,先选择ConsulServer从服务端查询并拉取服务列表,知道了它有多个服务(上图3个服务),这3个实现是完全一样的,默认轮询调用谁都可以正常执行。类似生活中求医挂号,某个科室今日出诊的全部医生,客户端你自己选一个

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

创建一个服务多个实例

创建一个和8001配置服务一模一样的服务8002

得到一个微服务中有两个实例:这样consumer微服务实例想要访问payment微服务实例,需要将payment微服务的所有实例轮巡检查

image-20240402101419024

修改订单80模块

添加依赖:POM文件

1
2
3
4
5
<!-- loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

添加访问8001 8002的方法

1
2
3
4
5
6
public static final String PaymentSrv_URL = "http://cloud-payment-service";

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

cloud-payment-service微服务下面有两个实例,所以需要在restTemplate配置类上加上@LoadBalanced,让restTemplate能够支持轮询检查

1
2
3
4
5
6
7
8
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

总结

Consul获取所有服务的方法,使用DiscoveryClient动态获取所有上线的服务列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  @Resource
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/discovery")
public String discovery()
{
// 获取Consul中的所有微服务
List<String> services = discoveryClient.getServices();
for (String element : services) {
System.out.println(element);
}

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

//获取微服务名称为cloud-payment-service的所有实例
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开始

1
List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");

如: List [0] instances = 127.0.0.1:8002

List [1] instances = 127.0.0.1:8001

8001+ 8002 组合成为集群,它们共计2台机器,集群总数为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

如此类推…

切换负载均衡算法

默认有两种算法:轮询,随机

切换方法 在RestTemplateConfig配置类中切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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);
}
}

服务接口调用 OpenFeign

什么是OpenFeign

官网:Spring Cloud OpenFeign

openfeign是一个声明式的Web服务客户端

只需创建一个Rest接口并在该接口上添加注解@FeignClient即可

OpenFeign基本上就是当前微服务之间调用的事实标准

OpenFeign能干什么

OpenFeign是对RestTemplate 和 LoadBalance的封装

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

OpenFeian同时还集成SpringCloud LoadBalancer

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

OpenFeign使用方法

1. 接口加注解

接口:微服务接口

注解:@FeignClient注解

服务消费者,通过公共API模块调用服务,而不是直接调用服务

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

image-20240402153354281

2. 建立模块

改POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<dependencies>
<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>
<!-- springCloudConsul 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>
<!--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>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>

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

写yaml

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}

修改主启动类

1
2
3
4
5
6
7
@SpringBootApplication
@EnableFeignClients
public class MainOpenFeign80 {
public static void main(String[] args) {
SpringApplication.run(MainOpenFeign80.class, args);
}
}

3.修改通用模块

服务请求模块通过API接口来调用对应的模块,可能有多个服务都要通过API来进行模块的调用,所以要将OpenFeign接口放在通用模块中

在公共模块引入OpenFeign模块

1
2
3
4
5
<!-- OpenFeign接口模块 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

新建服务接口PayFeignApi,头上配置@FeignClient注解

在公共模块下新建apis.PayFeignApi接口,接口中提供的方法就是服务cloud-payment-service对外暴露的方法

1
2
3
4
5
6
7
8
9
10
11
12
// 参数为服务名称
@FeignClient("cloud-payment-service")
public interface PayFeignApi {
@PostMapping("/pay/add")
public ResponseResult addPay(@RequestBody Pay pay);

@GetMapping("/pay/get/{id}")
public ResponseResult getPayInfo(@PathVariable("id") Integer id);

@GetMapping("/pay/get/info")
public String getInfoByConsul();
}

4.修改Controller层接口

不再需要RestTemplate进行多模块调用了,直接调用公共的API来进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
@RequestMapping("/consumer")
public class OrderController {
// 注入公共的API
@Resource
private PayFeignApi payFeignApi;

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

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

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

}

image-20240402171159968

超时控制

在Spring Cloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会有多大问
题的,但是如果是业务比较复杂,服务要进行比较繁杂的业务计算,那后台很有可能会出现Read Timeout这个异常,因此定制化配
置超时时间就有必要了。

OpenFeigen默认超时时间是60s,超过60s之后会报错

yml配置超时时间

connectTimeout:连接超时时间

readTimeout:请求处理超时时间

application.yml

全局配置

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
openfeign:
client:
config:
default:
# 全局配置,不指定模块
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic

局部配置

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
openfeign:
client:
config:
# 指定模块名,调用该模块的服务的时候超时时间设置
cloud-payment-service:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic

重试机制

重试机制:如果访问一次没有获取相应的结果,则会重新访问,设置重新访问的次数

OpenFeign默认机制是关闭的:只会调用一次,如果不成功就结束

开启Retryer功能:新增配置类FeignConfig并修改Retryer配置

1
2
3
4
5
6
7
8
9
@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);
}
}

默认HttpClient修改

OpenFeign中http client

如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求

由于默认HtpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最牛B的,所以加到最大。

使用Apache HttpClient5 替换 OpenFeign默认的HttpClient,解放性能

POM修改

1
2
3
4
5
6
7
8
9
10
11
<!-- Apache HttpClient5  -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>

YML修改

1
2
3
4
5
6
spring:
cloud:
openfeign:
httpclient:
hc5:
enable: true

请求/相应压缩

对请求和响应进行GZIP压缩:

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

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

1
2
3
4
5
6
7
8
spring:
cloud:
openfeign:
compression:
request:
enable: true
response:
enable: true

细粒度化设置:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
openfeign:
compression:
request:
enable: true
# 初始数据压缩类型
mime-types: text/xml,application/xml,application/json
# 最小触发压缩大小
min-request-size: 2048
response:
enable: 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 #最小触发压缩的大小

日志打印

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别

从而了解 Feign 中 Http 请求的细节

说白了就是对Feign接口的调用情况进行监控和输出

日志级别

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

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

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

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

配置Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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;
}
}

配置YAML

通过配置YAML文件,来配置需要开启配置的服务名

公式:logging.level + 含有@FeignClient注解的完整的代包名的接口名 + debug

1
2
3
4
5
6
7
logging:
level:
com:
atguigu:
cloud:
apis:
PayFeignApi: debug

服务的熔断和降级

问题

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

image-20240403085616110

服务雪崩

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

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

解决方式

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

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

需要实现:

  1. 服务熔断:设置一个开关,开能用,关失效,并返回友好的处理方式
  2. 服务降级:服务繁忙,请稍后再试,并返回友好的处理方式
  3. 服务限流:限定1s中几个
  4. 服务限时
  5. 服务预热
  6. 接近实时的监控
  7. 兜底的处理动作

Circuit Breaker

Spring Cloud 断路器

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

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

当过一段时间后CircuitBreaker会切换到HALF_OPEN状态,尝试发送几个探路请求,看链路是否通顺,如果通顺则切换到CLOSED状态,如果不行则返回OPEN状态

image-20240403091702613

**CircuitBreaker和Resilience4J的关系:**CircuitBreaker只是以一套规范和接口,落地实现者是Resilience4J

Resilience4J

Resilience Github

Resilience4j 是一个专为函数式编程而设计的轻量级容错库。 Resilience4j 提供了高阶函数(装饰器)来增强任何功能接口, 带有断路器、速率限制器、重试或隔板的 lambda 表达式或方法引用。 您可以在任何函数接口、lambda 表达式或方法引用上堆叠多个装饰器。 优点是您可以选择所需的装饰器,而没有别的。

Resilience4j 2 需要 Java 17。

Resilience4j 提供了几个核心模块:

  • resilience4j-circuitbreaker:熔断
  • resilience4j-ratelimiter:限流
  • resilience4j-bulkhead: 隔离
  • resilience4j-retry:自动重试(同步和异步)
  • resilience4j-timelimiter:超时处理
  • resilience4j-cache:结果缓存

还有用于度量的附加模块、Feign、Kotlin、Spring、Ratpack、Vertx、RxJava2 等。

三个基本状态

CircuitBreaker

CLOSED OPEN HALF_OPEN

image-20240403091702613

转换方式:

  • 断路器有三个普通状态: 关闭 (CLOSED) 、开启(OPEN) 、半开(HALF_OPEN),还有两个特殊状态: 禁用(DISABLED)、强制开启(FORCED OPEN)

  • 当熔断器关闭时,所有的请求都会通过熔断器

    • 如果失败率超过设定的闻值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝,
    • 当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率
    • 如果失败率超过闻值,则变为打开状态,如果失败率低于阔值,则变为关闭状态。
  • 断路器使用滑动窗口来存储和统计调用的结果。你可以选择基于调用数量的滑动窗口或基于时间的滑动窗口

    • 基于访问数量的滑动窗口统计了最近N次调用返回结果。居于时间的滑动窗口统计最近N秒的调用回结果
  • 除此以外,熔断器还会有两种特殊状态: DISABLED (始终允许访问)和FORCED OPEN (始终拒绝访问)

    • 这两个状态不会生成熔断器事件 (除状态装换外),并且不会记录事件的成功或者失败。
    • 退出这两个状态的唯一方法是触发状态转换或者重置熔断器。

创建和配置断路器

Config 属性 描述
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状态需要等待的时间

COUNT_BASED版本

修改服务提供者8001

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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();
}
}

修改公共API接口

将8001新增服务接口暴露出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi
{
/**
* 新增一条支付相关流水记录
* @param payDTO
* @return
*/
@PostMapping("/pay/add")
public ResultData addPay(@RequestBody PayDTO payDTO);

/**
* 按照主键记录查询支付流水信息
* @param id
* @return
*/
@GetMapping("/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id);

/**
* openfeign天然支持负载均衡演示
* @return
*/
@GetMapping(value = "/pay/get/info")
public String mylb();

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

改POM 80端口服务调用者

修改公共API的POM文件,引入以下文件

1
2
3
4
5
6
7
8
9
10
<!--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>

写YAML 80端口服务调用者

在yaml文件中配置滑动窗口的类型,这里设置为COUNT_BASED

并设置请求窗口大小

最小样本数

是否自动过渡到半关闭状态

开到关的时间

半开状态最大请求数

实例熔断配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
spring:
cloud:
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:
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 # 指定其配置为上面写的default

新增80端口Controller

@CircuitBreaker

参数:

name: 指定在调用那个服务的时候需要进行保障

fallbackMethod:指定在调用失败的时候处理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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ㄒ)/~~";
}
}

TIME_BASED版本

基于时间的滑动窗口是通过有N个桶的环形数组实现

如果滑动窗口的大小为10秒,这个环形数组总是有10个桶,每个桶统计了在这一秒发生的所有调用的结果(部分统计结果),数组中的第一个桶存储了当前这一秒内的所有调用的结果,其他的桶存储了之前每秒调用的结果。

滑动窗口不会单独存储所有的调用结果,而是对每个桶内的统计结果和总的统计值进行增量的更新,当新的调用结果被记录时,总的统计值会进行增量更新。

检索快照(总的统计值)的时间复杂度为0(1),因为快照已经预先统计好了,并且和滑动窗口大小无关.

关于此方法实现的空间需求(内存消耗)约等于O)。由于每次调用结果(元组)不会被单独存储,只是对N个桶进行单独统计和一次总分的统计。

每个桶在进行部分统计时存在三个整型,为了计算,失败调用数,慢调用数,总调用数。还有一个ong类型变量,存储所有调用的响应时间。

YAML文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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

总结

当满足一定的峰值和失败率达到一定条件后,断路器将会进入OPEN状态(保险丝跳闸),服务熔断

当OPEN的时候,所有请求都不会调用主业务逻辑方法,而是直接走fallbackmetnod兜底背锅方法,服务降级

段时间之后,这个时候断路器会从OPEN进入到HALF OPEN半开状态,会放几个请求过去探探链路是否通?

  • 如成功,断路器会关闭CLOSE(类似保险丝闭合,恢复可用);
  • 如失败,继续开启。重复上述

BulkHead

官网Bulkhead

含义:bulkhead(船的)舱壁/(飞机的)隔板

隔板来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船。

image-20240403172414976

是什么:限并发

能干嘛:依赖隔离&负载保护:用来限制对于下游服务的最大并发数量的限制

Resilience4j 提供了两种舱壁模式的实现,可用于限制并发执行的数量:

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

SemaphoreBulkhead

概述:

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

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

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

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

测试

在8001中添加微服务接口

1
2
3
4
5
6
7
8
9
10
11
12
13
//=========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();
}

在公共模块APIS中添加对外暴露的接口

1
2
3
4
5
6
7
/**
* Resilience4j Bulkhead 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id);

修改服务请求者的POM文件

1
2
3
4
5
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>

修改服务请求者的YAML文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#resilience4j bulkhead 的例子
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
instances:
cloud-payment-service:
baseConfig: default
timelimiter:
configs:
default:
timeout-duration: 20s

调用方式Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
*(船的)舱壁,隔离
* @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ㄒ)/~~";
}

FixedThreadPoolBulkhead

概述:

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

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

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

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

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

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

测试:

POM文件:

1
2
3
4
5
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>

YAML 文件

配置名称 默认值 含义
maxThreadPoolSize Runtime.getRuntime().avaliableProcessors() 配置最大线程池大小
coreThreadPoolSize Runtime.getRuntime().avaliableProcessors()-1 配置核心线程池大小
queueCapacity 100 配置队列的容量
keepAliveDuration 20ms 当前线程数大于核心的时候,这是多余线程
在终止前等待任务的最长时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
####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 新启线程和原来主线程脱离

调用Controller

将Type设置为:THREADPOOL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* (船的)舱壁,隔离,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ㄒ)/~~");
}

RateLimiter

限流官网:TimeLimiter

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

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

限流算法

  1. 漏斗算法:Leaky Bucket

    一个固定容量的漏桶,按照设定常量固定速率流出水滴,类似医院打吊针,不管你源头流量多大,我设定匀速流出。

    如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。

    image-20240403204241567

    缺点:

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

    image-20240403204330679

  2. 令牌桶算法(Token Bucket)SpringBoot默认算法

    image-20240403204751696

  3. 滚动时间窗算法

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

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

    image-20240403204925688

    缺点:

    间隔临界的一段时间内的请求就会超过系统限制,可能导致系统被压垮,例如在间隔端最后一次性全部涌入,会导致系统压垮

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

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

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

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

    滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,

    不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。下图统计了5次

    image-20240403205025410

测试

修改服务提供者8001

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

在公共模块APIS中添加对外暴露的接口

1
2
3
4
5
6
7
/**
* Resilience4j Ratelimit 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id);

修改服务请求者的POM文件

添加ratelimiter依赖

1
2
3
4
5
<!--resilience4j-ratelimiter-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>

修改服务请求者的YAML文件

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

增加服务请求者的Controller方法

使用@RateLimiter

1
2
3
4
5
6
7
8
9
10
@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ㄒ)/~~";
}

服务链路追踪

分布式链路追踪

分布式所面临的的问题:

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

需要解决的问题:

  • 如何实时观测系统的整体调用链路情况。
  • 如何快速发现并定位到问题。
  • 如何尽可能精确的判断故障对系统的影响范围与影响程度。

  • 如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。

  • 如何尽可能精确的分析整个系统调用链路的性能与瓶颈点

  • 如何尽可能精确的分析系统的存储瓶颈与容量规划

解决方法:

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

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

micrometer负责收集链路信息,zipkin负责展示信息

分布式链路追踪原理

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

链路通过TraceId唯一标识

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

CS: Client Sent: 客户端发送这个请求的时间

CR: Client Reeceived: 客户端接受到数据的时间

SS: Server Sent: 服务端发送响应的时间

SR: Server Received: 服务端接受到这个请求的时间

SR-CS:服务端接收到该请求的时间 - 客户端发送请求的时间 = 网络传输时间

SS - SR:服务端发送相应的时间 - 服务端接收到请求的时间 = 业务处理时间

CR - CS:客户端接收到数据的时间 - 客户端发送请求的时间 = 远程调用耗时

CR - SS:客户端收到数据的时间 - 服务器端发送请求的时间 = 网络传输时间

image-20240405094542833

一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来,通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。

Zipkin

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

下载:Quickstart · OpenZipkin

使用:

1
java -jar zipkin-server-3.1.1-exec.jar

默认网址: http://127.0.0.1:9411/

Micrometer整合Zipkin实现链路监控

Micrometer:链路采样

Zipkin:图形展示

父工程POM

引入依赖的原因

由于Micrometer Tracing是一个门面工具自身并没有实现完整的链路追踪系统,具体的链路追踪另外需要引入的是第三方链路追踪系统的依赖:

序号 名称 描述
1 micrometer-tracing-bom 导入链路追踪版本中心,体系化说明
2 micrometer-tracing 指标追踪
3 micrometer-tracing-bridge-brave 一个Micrometer模块,用于与分布式跟踪工具 Brave 集成,以收集应用程序的分布式跟踪数据。
Brave是一个开源的分布式跟踪工具,它可以帮助用户在分布式系统中跟踪请求的流转,
它使用一种称为"跟踪上下文"的机制,将请求的跟踪信息存储在请求的头部,
然后将请求传递给下一个服务。在整个请求链中,Brave会将每个服务处理请求的时间和其他
信息存储到跟踪数据中,以便用户可以了解整个请求的路径和性能。
4 micrometer-observation 一个基于度量库 Micrometer的观测模块,用于收集应用程序的度量数据。
5 feign-micrometer 一个Feign HTTP客户端的Micrometer模块,用于收集客户端请求的度量数据。
6 zipkin-reporter-brave 一个用于将 Brave 跟踪数据报告到Zipkin 跟踪系统的库。

补充包:spring-boot-starter-actuator SpringBoot框架的一个模块用于监视和管理应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<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>

服务提供者8001

POM文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!--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>

YAML文件:

1
2
3
4
5
6
7
8
# ========================zipkin===================
management:
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。

新建Controller测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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();
}
}

公共APIS添加暴露接口

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

调用者80

POM文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!--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>

YAML文件:

1
2
3
4
5
6
7
8
# zipkin图形展现地址和采样率设置
management:
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时.

测试业务类Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**

* Micrometer 替代 Sleuth
*/
@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);
}
}

服务网关 GateWay

概述

是什么

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

所有的请求都要先经过Gateway,处理之后再发送给后面的微服务框架

image-20240405153055637

微服务网关所处位置

image-20240405153424644

作用

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

总结

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

Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心

image-20240405153543068

三大核心

三大核心

  • 路由 router:网关的基本构建块。 它由 ID、目标URI、一系列断言和过滤器组成。如果断言为 true,则匹配路由。
  • 断言 Predicate:这是一个 Java 8中 java.util.function.Predicate。 可以匹配 HTTP 请求中的任何内容,例如标头或参数。
  • 过滤器 Filter:这些是使用特定工厂构建的GatewayFilter实例。 使用过滤器,可以在发送请求被路由之前或之后修改请求和响应。

对应的三个功能

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

  2. predicate就是我们的匹配条件;

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

工作流程

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

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

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

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

image-20240405155246905

配置

建Module

cloud-gateway9527

改POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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>

写YAML

将该服务入驻Consul

1
2
3
4
5
6
7
8
9
10
11
12
13
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
2
3
4
5
6
7
8
9
10
11
package com.atguigu.cloud;

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

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

路由映射

8001服务准备PayGatewayController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
public class PayGateWayController
{
@Resource
PayService payService;

@GetMapping("/pay/gateway/get/{id}")
public ResponseResult getById(@PathVariable("id") Integer id)
{
Pay pay = payService.getById(id);
return ResponseResult.okResult(pay);
}

@GetMapping(value = "/pay/gateway/info")
public ResponseResult getGatewayInfo()
{
return ResponseResult.okResult("gateway info test:"+ IdUtil.simpleUUID());
}
}

修改Gateway的YAML文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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/** # 断言,路径相匹配的进行路由

测试

没有添加网关前:http://localhost:8001/pay/gateway/get/1

添加网关后:http://localhost:9527/pay/gateway/get/1

通过9527端口来替换8001端口

Gateway网关将后端真实地址8001替换成了9527

通过Feign配合GateWay来访问

修改公共APIS中的服务名称,改为GateWay的服务名称,通过调用GateWay中的服务,进而来调用8001服务提供者的服务

1
2
3
4
5
6
7
8
9
10
// @FeignClient("cloud-payment-service")
@FeignClient("cloud-gateway")
public interface PayFeignApi {
@GetMapping(value = "/pay/gateway/info")
public ResponseResult getGatewayInfo();

@GetMapping("/pay/gateway/get/{id}")
public ResponseResult getById(@PathVariable("id") Integer id);

}

高级特性

Route以微服务名-动态获取服务URL

上述方法存在问题:在9524GateWay的配置文件中后端服务端口地址写死了,需要改成服务名,动态获取URL

问题写法===http://localhost:8001

1
2
3
4
5
6
7
8
9
10
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/** # 断言,路径相匹配的进行路由

优化之后===lb://cloud-payment-service

1
2
3
4
5
6
7
8
9
10
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由

Predicate断言

Route决定前台页面能不能够找到

Predicate决定前台页面能不能访问

是什么

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

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

Spring Cloud Gateway 创建 Route 对象时,使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给Route。 Spring Cloud Gateway 包含许多内置的Route Predicate Factories。所有这些谓词都匹配HTTP请求的不同属性多种谓词工厂可以组合,并通过逻辑and。

image-20240405203944490

配置方法:

Shortcut Configuration:缩写

Fully Expanded Argument:全写,KeyValue

常用APIS

  1. AfterRoutePredicateFactory,在一个时间之后才能访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - Path=/pay/gateway/get/**
    - After=2024-04-05T21:22:15.037109200+08:00[Asia/Shanghai]
  2. BeforeRoutePredicateFactory,在一个时间之前能访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - Path=/pay/gateway/get/**
    - After=2024-04-05T21:22:15.037109200+08:00[Asia/Shanghai]
    - Before=2024-04-05T22:22:15.037109200+08:00[Asia/Shanghai]
  3. BetweenRoutePredicateFactory,在一个时间之前能访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - Path=/pay/gateway/get/**
    - Between=2024-04-05T21:22:15.037109200+08:00[Asia/Shanghai],Before=2024-04-05T22:22:15.037109200+08:00[Asia/Shanghai]
  4. CookieRoutePredicateFactory

    两个参数:

    第一个:Cookie name

    第二个:正则表达式

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - Path=/pay/gateway/get/**
    - Cookie=Cookiename, Re表达式
  5. HeaderRoutePredicateFactory

    两个参数

    第一个头部参数名

    第二个正则表达式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - Path=/pay/gateway/get/**
    - Header=X-Request-Id, Re表达式 #请求头要有X-Request-Id属性并且值为正则表达式
  6. HostRoutePredicateFactory

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - Path=/pay/gateway/get/**
    - Host=**.somehost.org, **.anotherhost.org
  7. PathRoutePredicateFactory

    路径相匹配的在进行访问

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - Path=/pay/gateway/get/**
  8. QueryRoutePredicateFactory

    请求参数必须符合条件才能访问

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - Query=参数名, 参数值的正则表达式
  9. RemoteAddrRoutePredicateFactory

    访问的IP地址必须是特定的才行

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - RemoteAddr= 192.168.1.1/24 # 网络前缀必须是192.168.1才能访问
  10. MethodRoutePredicatet

    特定方法才能访问

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: pay_routh1 #pay_routh1
    uri: lb://cloud-payment-service
    predicates:
    - Method= GTE, POST
  11. 自定义规则

    自带规则无法满足需求

    要么继承AbstractRoutePredicateFactory抽象类

    要么实现RoutePredicateFactory接口

    1. 新建类名XXX需要以RoutePredicate结尾并继承AbstractRoutePredicateFactory类

      1
      2
      public class MyRoutePredicateFactor extends AbstractRoutePredicateFactory<MyRoutePredicateFactor.Config> {
      }
    2. 重写Apply方法

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class MyRoutePredicateFactor extends AbstractRoutePredicateFactory<MyRoutePredicateFactor.Config> {
      @Validated
      public static class Config {
      @Setter
      @Getter
      @NotEmpty
      private String userType;
      }
      }
    4. 空参构造方法,内部调用super

      1
      2
      3
      4
      5
      6
      @Component
      public class MyRoutePredicateFactor extends AbstractRoutePredicateFactory<MyRoutePredicateFactor.Config> {
      public MyRoutePredicateFactor() {
      super(MyRoutePredicateFactor.Config.class);
      }
      }
    5. 重写Apply方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      @Override
      public Predicate<ServerWebExchange> apply(MyRoutePredicateFactor.Config config) {
      return new Predicate<ServerWebExchange>() {
      @Override
      public boolean test(ServerWebExchange serverWebExchange) {
      String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");
      if(userType == null){ // 如果没有带参数直接返回false
      return false;
      }
      // 参数存在,就和Config中的内容进行比较
      if(userType.equalsIgnoreCase(config.getUserType())){
      return true;
      }
      return false;
      }
      };
      }
    6. 测试:YAML文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      spring:
      cloud:
      gateway:
      routes:
      - id: pay_routh2 #pay_routh2
      uri: lb://cloud-payment-service #匹配后提供服务的路由地址
      predicates:
      - Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
      - My=diamond

      上述方式会出错,因为自己设计的断言不支持短格式

      完整格式:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      spring:
      cloud:
      gateway:
      routes:
      - id: pay_routh2 #pay_routh2
      uri: lb://cloud-payment-service #匹配后提供服务的路由地址
      predicates:
      - Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
      - name:My

      在我们自定义Predicate类中添加shortcutFieldOrder,就支持短格式了

      1
      2
      3
      public List<String> shortcutFieldOrder() {
      return Collections.singletonList("userType");
      }

Filter 过滤器

“pre”和“post”分别会在请求被执行前调用和被执行后调用用来修改请求和响应信息

作用:请求鉴权,异常处理

有三种类型

  1. 全局默认过滤器

    Global Filters

    gateway出厂默认已有的,直接用即可,主要作用于所有的路由

    不需要在配置文件中配置,作用在所有的路由上,实现GlobalFilter接口即可

  2. 单一内置过滤器

    GatewayFilter Factories

    也可以称为网关过滤器,这种过滤器主要是作用于单一路由或者某个路由分组

    有多重过滤器,可在官网查验

  3. 自定义过滤器

内置 请求头相关组(RequestHeader)

  1. AddRequestHeader GatewayFilter Factory

    在进行访问https://example.org该网址的时候,将请求头中的内容添加一个X-Request-red=blue参数

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: https://example.org
    filters:
    - AddRequestHeader=X-Request-red, blue
  2. RemoveRequestHeader GatewayFilter Factory

    移除请求头中的一部分

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: https://example.org
    filters:
    - RemoveRequestHeader=X-Request-red, blue
  3. SetRequestHeader GatewayFilter Factory

    更新请求头中的一部分

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: https://example.org
    filters:
    - SetRequestHeader=X-Request-red, blue

内置 请求参数相关组 (RequestParameter)

  1. AddRequestParameter GatewayFilter Factory

    添加请求参数

    如果请求中含有相同名称的参数,则按照请求头中的参数为依据

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: https://example.org
    filters:
    - AddRequestParameter=X-Request-red, blue
  2. RemoveRequestParameter GatewayFilter Factory

    删除请求参数

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: https://example.org
    filters:
    - RemoveRequestParameter=X-Request-red, blue

内置 响应头相关组(ResponseHeader)

  1. AddResponseHeader GatewayFilter Factory

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: https://example.org
    filters:
    - AddResponseHeader=X-Request-red, blue
  2. RemoveResponseHeader GatewayFilter Factory

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: https://example.org
    filters:
    - RemoveResponseHeader=X-Request-red, blue
  3. SetResponseHeader GatewayFilter Factory

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: https://example.org
    filters:
    - SetResponseHeader=X-Request-red, blue

前置和路径相关组

  1. PrefixPath GatewayFilter Factory

    前缀路径添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: lb://cloud-payment-service
    predicates:
    # - Path="/pay/gateway/filter/**" 正确路径
    # 即使访问路径不正确,也能通过,在filters中拼接成为正确的访问路径
    - Path= "/gateway/filter/**"
    filters:
    - PrefixPath=/pay
  2. SetPath GatewayFilter Factory

    将错误地址转换成正确地址,如果想要错误地址中的数据,需要使用占位符

    例如下面,我们可以通过 http://localhost:9527/xyz/abc/filter访问到 http://localhost:9527/pay/gateway/filter

    filter被占位符所占用,放到真实的地址中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: lb://cloud-payment-service
    predicates:
    # - Path="/pay/gateway/filter/**" 正确路径
    # 设置错误的访问路径
    - Path= "xyz/abc/{segment}"
    filters:
    - SetPath=/pay/gateway/{segment}
  3. RedirectTo GatewayFilter Factory

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    cloud:
    gateway:
    routes:
    - id: add_request_header_route
    uri: lb://cloud-payment-service
    predicates:
    - Path="/pay/gateway/filter/**"
    filters:
    - RedirectTo=302, https://www.baidu.com

内置 其他

全局配置,一次配置所有路由生效,不建议

1
2
3
4
5
6
spring:
cloud:
gateway:
default-filters:
- RedirectTo=302, https://www.baidu.com
- SetPath=/pay/gateway/{segment}

自定义全局过滤器

新建类MyGlobalFilter并实现GlobalFilter,Ordered两个接口

实现两个方法filter,getOrder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;

@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)+"毫秒");
}
}));
}

@Override
public int getOrder() {
return 0;
}
}

自定义单一过滤器

查看自带的单一过滤器

都是继承AbstractNameValueGatewayFilterFactory抽象类,所以自定义单一过滤器也需要继承AbstractNameValueGatewayFilterFactory抽象类

新建类名以GatewayFilterFactory结尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config> {
public MyGatewayFilterFactory() {
super(MyGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(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());
// 参数中有nuyoah才能放行
if(request.getQueryParams().containsKey("nuyoah")){
return chain.filter(exchange);
}else{
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
}
};
}

public List<String> shortcutFieldOrder() {
return List.of("status");
}
public static class Config {
@Getter
@Setter
private String status;
}
}

配置YAML文件

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
filters:
- My=nuyoah

SpringCloudAlibaba入门

SpringCloud官网

版本依赖:版本发布说明 | Spring Cloud Alibaba (aliyun.com)

SpringCloudAlibaba-Nacos 服务注册

作用

替代Consul做服务注册中心

替代Consul做服务配置中心和满足动态刷新广播通知

安装

Nacos 快速开始 | Nacos

使用:在下载好的Bin文件夹中敲CMD,然后运行startup.cmd -m standalone即可开始

Nacos Discovery服务注册中心

服务提供者

  1. 新建Module—cloudalibaba-provider-payment9001

  2. POM文件:引入Discovery文件

    1
    2
    3
    4
    5
    <!--nacos-discovery-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    全部POM文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    <dependencies>
    <!--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>
  3. YAML文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server:
    port: 9001

    spring:
    application:
    name: nacos-payment-provider
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 #配置Nacos地址
  4. 创建主启动类

    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class Main9001 {
    public static void main(String[] args) {
    SpringApplication.run(Main9001.class);
    }
    }
  5. 创建Controller对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @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;
    }
    }
  6. 启动9001,然后访问http://localhost:8848/nacos点击服务管理即可找到我们注册的nacos-payment-provider

服务消费者

  1. 创建模块cloudalibaba-consumer-nacos-order83

  2. 改POM文件

    新引入文件依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--nacos-discovery Nacos服务发现 将消费者项目注册进Nacos -->
    <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>

    总体POM:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    <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>
  3. 写YAML文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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
  4. 业务类

    新建config.RestTemplateConfig业务配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class RestTemplateConfig {
    @Bean
    @LoadBalanced //赋予RestTemplate负载均衡的能力
    public RestTemplate restTemplate()
    {
    return new RestTemplate();
    }
    }

    新建业务类Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @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支持负载均衡

Nacos Config服务配置中心

之前案例Consul8500服务配置动态变更功能可以被Nacos取代

通过Nacos和spring-cloud-starter-alibaba-nacos-config实现中心化全局配置的动态变更

配置步骤

  1. 建Module----cloudalibaba-config-nacos-client3377

  2. 改POM

    添加的依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--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>

    完整的POM文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    <dependencies>
    <!--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>
    <!--nacos-discovery-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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>
  3. 写YAML文件

    Nacos同Consul一样,在项目初始化时,要保证先从配置中心进行配置拉取

    拉取配置之后,才能保证项目的正常启动,为了满足动态刷新和全局广播通知

    springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application

    bootstrap.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 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格式的配置

    application.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    server:
    port: 3377

    spring:
    profiles:
    active: dev # 表示开发环境
    #active: prod # 表示生产环境
    #active: test # 表示测试环境
  4. 修改主启动类

    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class Main3377 {
    public static void main(String[] args) {
    SpringApplication.run(Main3377.class, args);
    }
    }
  5. 配置业务类

    @RefreshScope,只要服务器中更改了,本地自动修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    @RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
    public class NacosConfigClientController
    {
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/config/info")
    public String getConfigInfo() {
    return configInfo;
    }
    }

在Nacos中添加配置信息

在 Nacos Spring Cloud 中,dataId 的完整格式如下:

1
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

@RefreshScope注解实现配置的自动更新

添加配置文件:DataID需要是配置文件中配置的内容

image-20240408192227518

当我们修改了配置文件中的内容,那么添加了@RefreshScope注解的配置类,也会自动获取新文件

Nacos配置文件自动保存30天,30天内的文件配置修改都可以回滚

NameSpace-Group-DataId

为什么要设置NameSpace命名空间

问题1:

实际开发中,通常一个系统会准备

dev开发环境

test测试环境

prod生产环境。

如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?

问题2:

一个大型分布式微服务系统会有很多微服务子项目,

每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…

那怎么对这些微服务配置进行分组和命名空间管理呢?

DATAID方案

默认空间public+默认分组DEFAULT GROUP+新建DatalD

image-20240408195353438

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

bootstrap.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# nacos配置 第一种:默认空间+默认分组+新建DataID
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.profile.active}.${spring.cloud.nacos.config.file-extension}
# 本案例的DataID是:nacos-config-client-dev.yaml

application.yml

1
2
3
4
5
6
7
8
server:
port: 3377

spring:
profiles:
#active: dev # 表示开发环境
active: test # 表示测试环境
#active: prod # 表示生产环境

通过不同的测试环境来配置不同的配置变量

GROUP方案

在配置文件中添加一个新的分组

group: PROD_GROUP

bootstrap.yml

1
2
3
4
5
6
7
8
9
10
11
12
# 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格式的配置
group: PROD_GROUP

application.yml

1
2
3
4
5
6
7
8
9
server:
port: 3377

spring:
profiles:
active: prod # 表示生产环境
# active: dev # 表示开发环境
# active: test # 表示测试环境

image-20240408195330274

Namespace方案

新建命名空间

image-20240408200211819

在新的命名空间中新建配置

image-20240408200401715

修改YAML文件,在config文件中添加一条

namespace:Prod_Namespace

1
2
3
4
5
6
7
8
9
10
11
12
13
# 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格式的配置
group: PROD_GROUP
namespace: Prod_Namespace

SpringCloudAlibaba Sentinel 熔断限流

功能

官网 home | Sentinel (sentinelguard.io)

作用:从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性

面试题:

  1. 服务雪崩

    因为微服务之间的相互调用,可能因为一个微服务出错,导致整个微服务系统瘫痪,称为服务雪崩

    多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

    所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。

  2. 服务降级

    当服务出错的时候有个保底的响应方式

    服务降级,说白了就是一种服务托底方案,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。

    例如,在商品详情页一般都会展示商品的介绍信息,一旦商品详情页系统出现故障无法调用时,会直接获取缓存中的商品介绍信息返回给前端页面。

  3. 服务熔断

    当服务访问量过大的时候,会自动断开连接,防止服务器崩溃

    在分布式与微服务系统中,如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会暂时断开与下游服务的调用连接。这种方式就是熔断。类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。

    服务熔断一般情况下会有三种状态:闭合、开启和半熔断;

    闭合状态(保险丝闭合通电OK):服务一切正常,没有故障时,上游服务调用下游服务时,不会有任何限制。

    开启状态(保险丝断开通电Error):上游服务不再调用下游服务的接口,会直接返回上游服务中预定的方法。

    半熔断状态:处于开启状态时,上游服务会根据一定的规则,尝试恢复对下游服务的调用。此时,上游服务会以有限的流量来调用下游服务,同时,会监控调用的成功率。如果成功率达到预期,则进入关闭状态。如果未达到预期,会重新进入开启状态。

  4. 服务限流

    限制服务访问量

    服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。

    限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如令牌桶算法和漏牌桶算法就是时间窗口的限流算法。

  5. 服务隔离

    当服务出错的时候,可使用服务隔离来将出错的服务,进行隔离,从而不影响别的进程

    有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。

    互联网行业常用的服务隔离方式有:线程池隔离和信号量隔离。

  6. 服务超时

    当服务相应之间的时间间隔过大的时候会断开服务之间的链接

    整个系统采用分布式和微服务架构后,系统被拆分成一个个小服务,就会存在服务与服务之间互相调用的现象,从而形成一个个调用链。

    形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。服务超时就是在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源。

安装

下载:Releases · alibaba/Sentinel (github.com)

运行:java -jar sentinel-dashboard-1.8.7.jar

查看界面:http://localhost:8080/#/dashboard 账号密码默认是sentinel

入门案例

  1. 启动Nacos

  2. 启动Sentinel

  3. 建Module====cloudalibaba-sentinel-service8401

  4. 改POM

    新增依赖

    1
    2
    3
    4
    5
    <!--SpringCloud alibaba sentinel -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>

    整体POM文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    <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>
  5. 写YAML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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扫描,直至找到未被占用的端口
  6. 主启动类

    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class Main8401 {
    public static void main(String[] args) {
    SpringApplication.run(Main8401.class);
    }
    }
  7. 业务类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @RestController
    public class FlowLimitController {
    @GetMapping("/testA")
    public String testA()
    {
    return "------testA";
    }

    @GetMapping("/testB")
    public String testB()
    {
    return "------testB";
    }
    }
  8. Sentinel采用懒加载,如果接口不被访问,则Sentinel不会监控

流量控制

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

image-20240409155001747

资源名 资源的唯一名称,默认就是请求的接口路径,可以自行修改,但是要保证唯一。
针对来源 具体针对某个微服务进行限流,默认值为default,表示不区分来源,全部限流。
阈值类型 QPS表示通过QPS进行限流,并发线程数表示通过并发线程数限流。
单机阈值 与阈值类型组合使用。如果阈值类型选择的是QPS,表示当调用接口的QPS达到阈值时,
进行限流操作。如果阈值类型选择的是并发线程数,则表示当调用接口的并发线程数达到阈值时,进行限流操作。
是否集群 选中则表示集群环境,不选中则表示非集群环境。

流控模式-直接

image-20240409155117758

设置了阈值类型为QPS,且阈值为2

所以在1s内访问超过两次之后会限流报错

也可以自定义兜底方案,只需要在Controller上设置fallback兜底方案

流控模式-关联

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

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

配置

image-20240409161357421

流控模式-链路

来自不同链路的请求对同一个目标访问时,实施针对性的不同限流措施,比如C请求来访问就限流,D请求来访问就是OK

针对同一个Service服务资源,两个接口同时调用,使用链路控制的就会限流,不使用链路控制的就不会

image-20240409171136109

测试:

  1. 新建FlowLimitService,设置值为common,再使用链路限流的的时候的资源名

    1
    2
    3
    4
    5
    6
    7
    @Service
    public class FlowLimitService {
    @SentinelResource(value = "common")
    public void common() {
    System.out.println("------FlowLimitService come in");
    }
    }
  2. 修改Controller

    testc 和 testd都访问flowLimitService的common方法,如果阈值到了就对C限流,对D不限流

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @RestController
    public class FlowLimitController {
    @Resource
    private FlowLimitService flowLimitService;
    @GetMapping("/testa")
    public String testA()
    {
    return "------testA";
    }

    @GetMapping("/testb")
    public String testB()
    {
    return "------testB";
    }

    @GetMapping("/testc")
    public String testC()
    {
    flowLimitService.common();
    return "------testC";
    }

    @GetMapping("/testd")
    public String testD()
    {
    flowLimitService.common();
    return "------testD";
    }
    }
  3. 修改YAML文件web-context-unify: false # controller 层的方法对 service层调用不认为是同一个根链路

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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扫描,直至找到未被占用的端口
    web-context-unify: false # controller层的方法对service层调用不认为是同一个根链路

流控效果-直接

直接失败抛出异常

image-20240409202355904

流控效果-预热

我们设置一个阈值,在刚开始的时候并不能直接到达阈值巅峰,而是刚开始的时候阈值比较低,等过一段时间之后在过渡到阈值巅峰

初始阈值为:公式: 阈值除以冷却因子coldFactor(默认值为3),经过预热时长后才会达到阈值

image-20240409203914943

例如上面的,阈值为10,初始阈值为3,经过5s之后阈值才为10

流控效果-排队等待

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下
来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求而不是在第一秒直接拒绝多余的
请求

配置:

超时时间:1000ms

单机阈值:1

就是在1000ms中只能有一个请求,多出来的请求需要往后排队

image-20240409204436243

熔断

熔断降级 · alibaba/Sentinel Wiki · GitHub

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

慢调用比例

选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

image-20240409211254127

当访问请求大于RT的时候,该请求被标记为慢调用,我们在请求数大于最小请求数的时候熔断才会生效,在统计时长内达到最小请求数量,且阈值大于设定的阈值的时候,会被熔断

异常比例

当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

image-20240410094858670

上面的图片表明在1s内请求数大于5,且异常请求大于10% 那么在接下来的5s内请求会被熔断

异常数

当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

image-20240410101951352

统计异常数量,如果超过两个的话则会进行熔断

@SentinelResource注解

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

注解说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@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 {};
}

按照rest地址限流+默认限流返回

按照路径限流

该注解没有引入SentinelResource注解,多次访问会出现Blocked by Sentinel (flow limiting)

测试:没有引入SentinelResource注解

1
2
3
4
5
@GetMapping("/rateLimit/byUrl")
public String byUrl()
{
return "按rest地址限流测试OK";
}

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

主要是SentinelResource接口中的value的值,通过设置value值来判断那个元素应该被限流

使用@SentinelResource注解来标识资源名

使用blockHandler来自定义处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@Slf4j
public class RateLimitController
{
@GetMapping("/rateLimit/byUrl")
public String byUrl()
{
return "按rest地址限流测试OK";
}

@GetMapping("/rateLimit/byResource")
@SentinelResource(value = "byResourceSentinelResource", blockHandler = "handlerBlockHandler")
public String byResource()
{
return "按照资源名称SentinelResource限流测试OK,0(n n)0";
}

public String handlerBlockHandler(BlockException blockException) {
return "服务不可用 @SentinelResource启动";
}

}

image-20240410203334906

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

通过fallback来进行服务降级处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RestController
@Slf4j
public class RateLimitController
{
@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();
}

}

配置规则:

image-20240410205032130

如果多次点击触发限流:会使用blockHandler来处理

如果程序出错会到时:fallback来处理异常

blockHandler,主要针对sentinel配置后出现的违规情况处理

fallback,程序异常了JVM抛出的异常服务降级

两者可以同时共存

热点规则

热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据并对其访问进行限流或者其它操作

代码

1
2
3
4
5
6
7
8
9
10
@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";
}

代码配置图:

限流模式只支持QPS模式,固定写死了。(这才叫热点)

@SentinelResource注解的方法参数索引,0代表第一个参数,1代表第二个参数,以此类推

单机阀值以及统计窗口时长表示在此窗口时间超过阀值就限流。

下面面的抓图就是第一个参数有值的话,1秒的QPS为1,超过就限流,限流后调用dealHandler_testHotKey支持方法。

image-20240411094249694

当我们设置好p1的限流时访问:http://localhost:8401/testHotKey?p1=1,当超过1s1次的时候就会被限流,只要我们访问的时候有p1这个参数,就会被限流,当我们访问的时候没有p1这个参数的时候就不会被限流

例外

设置p1的值为特定的值的时候,我们将其访问设置为不限流

一定要点击添加

image-20240411100111026

授权规则

我们需要根据来源名单判断是否能放行,设置黑白名单,黑名单不能访问,白名单能访问

新建EmpowerController

1
2
3
4
5
6
7
8
9
10
@RestController
@Slf4j
public class EmpowerController {
@GetMapping(value = "/empower")
public String requestSentinel4(){
log.info("测试Sentinel授权规则empower");
return "Sentinel授权规则";
}
}

新建处理器MyRequestOriginParser

handler.MyRequestOriginParser处理器,实现RequestOriginParser接口,实现parseOrigin方法

设置请求参数名:serverName

1
2
3
4
5
6
7
8
@Component
public class MyRequestOriginParser implements RequestOriginParser
{
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
return httpServletRequest.getParameter("serverName");
}
}

配置授权

配置流控应用test,test2设置为黑名单

image-20240411103359325

我们在RequestOriginParser处理器中配置了参数名为serverName

所以我们在请求的时候,如果参数中含有serverName=test 或者 serverName=test1的情况,则直接禁止访问

规则持久化

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

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

步骤

修改cloudalibaba-sentinel-service8401

  1. 修改POM文件

    添加依赖项

    1
    2
    3
    4
    5
    <!--SpringCloud ailibaba sentinel-datasource-nacos -->
    <dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>

    完整的POM文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    <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>
  2. 改YAML文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    spring:
    cloud:
    sentinel:
    datasource:
    ds1: # 自定义key,也可以做限流类型
    nacos:
    server-addr: localhost:8848 # nacos地址
    dataId: ${spring.application.name}
    groupId: DEFAULT_GROUP
    data-type: json
    rule-type: flow # com.alibaba.cloud.sentinel.datasource.RuleType 流控规则

    rule-type参数

    1
    2
    3
    4
    5
    6
    7
    public enum RuleType {
    FLOW("flow", FlowRule.class), // 流量控制规则
    DEGRADE("degrade", DegradeRule.class), // 熔断降级规则
    PARAM_FLOW("param-flow", ParamFlowRule.class), // 热点规则
    SYSTEM("system", SystemRule.class), // 系统保护规则
    AUTHORITY("authority", AuthorityRule.class), // 访问权限控制规则
    }
  3. 添加Nacos业务规则配置

    创建配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [
    {
    "resource": "/rateLimit/byUrl",
    "limitApp": "default",
    "grade": 1,
    "count": 1,
    "strategy": 0,
    "controlBehavior": 0,
    "clusterMode": false
    }
    ]

    resource:资源名称;

    limitApp:来源应用;

    grade:阈值类型,0表示线程数,1表示QPS;

    count:单机阈值;

    strategy:流控模式,0表示直接,1表示关联,2表示链路;

    controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;

    clusterMode:是否集群。

OpenFeign整合Sentinel

需求:访问者要有fallback服务降级的情况,不要持续访问9001加大微服务负担,但是通过feign接口调用的又方法各自不同, 如果每个不同方法都加一个fallback配对方法,会导致代码膨胀不好管理,工程埋雷

解决方式:通过fallback属性进行统一配置,feign接口里面定义的全部方法都走统一的服务降级,一个搞定即可

9001微服务自身还带着sentinel内部配置的流控规则,如果满足也会被触发

  • OpenFeign接口的统一fallback服务降级处理

  • Sentinel访问触发了自定义的限流配置,在注解@SentinelResource里面配置的blockHandler方法。

步骤1 修改服务提供方9001

  1. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--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>
  2. 修改YAML,引入Sentinel配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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扫描,直至找到未被占用的端口
  3. 修改Controller,方便测试,只有blockHandler方法,没有fallback方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @GetMapping("/pay/nacos/get/{orderNo}")
    @SentinelResource(value = "getPayByOrderNo",blockHandler = "handlerBlockHandler")
    public ResponseResult getPayByOrderNo(@PathVariable("orderNo") String orderNo)
    {
    //模拟从数据库查询出数据并赋值给DTO
    PayDTO payDTO = new PayDTO();

    payDTO.setId(1024);
    payDTO.setOrderNo(orderNo);
    payDTO.setAmount(9.9);
    payDTO.setPayNo("pay:"+ IdUtil.fastUUID());
    payDTO.setUserId(1);

    return ResponseResult.okResult("查询返回值:"+payDTO);
    }
    public ResponseResult handlerBlockHandler(@PathVariable("orderNo") String orderNo, BlockException exception)
    {
    return ResponseResult.errorResult(AppHttpCodeEnum.BAD_REQUEST,"getPayByOrderNo服务不可用," +
    "触发sentinel流控配置规则"+"\t"+"o(╥﹏╥)o");
    }

步骤2 修改cloud-api-commons

通过Feign将我们设置的两个接口暴露出去

先在cloud-api-commons添加依赖

1
2
3
4
5
6
7
8
9
10
<!--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接口,兜底方法为PayFeignSentinelApiFallBack类中的方法

1
2
3
4
5
6
@FeignClient(value = "nacos-payment-provider",fallback = PayFeignSentinelApiFallBack.class)
public interface PayFeignSentinelApi
{
@GetMapping("/pay/nacos/get/{orderNo}")
public ResponseResult getPayByOrderNo(@PathVariable("orderNo") String orderNo);
}

新建PayFeignSentinelApiFallBack类,实现PayFeignSentinelApi接口,并重写对应方法,对应方法出错之后调用该兜底方法

1
2
3
4
5
6
7
@Component
public class PayFeignSentinelApiFallBack implements PayFeignSentinelApi{
@Override
public ResponseResult getPayByOrderNo(String orderNo) {
return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR, "服务不可用");
}
}

步骤3 修改服务消费者

修改cloudalibaba-consumer-nacos-order83通过PayFeignSentinelApi去调用9001

  1. 修改POM文件

    新添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!-- 引入自己定义的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>

    整体POM文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    <dependencies>
    <!-- 引入自己定义的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>
    <!--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>
  2. 修改YAML,添加支持

    1
    2
    3
    4
    # 激活Sentinel对Feign的支持
    feign:
    sentinel:
    enabled: true
  3. 修改Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    public class OrderNacosController
    {
    @Resource
    private PayFeignSentinelApi payFeignSentinelApi;

    @GetMapping(value = "/consumer/pay/nacos/get/{orderNo}")
    public ResponseResult getPayByOrderNo(@PathVariable("orderNo") String orderNo)
    {
    return payFeignSentinelApi.getPayByOrderNo(orderNo);
    }
    }
  4. 修改整体的SpringBoot Cloud 版本,为了整合Alibaba

    1
    2
    3
    4
    5
    6
    <!-- 正常版本 -->
    <!-- <spring.boot.version>3.2.0</spring.boot.version> -->
    <!-- <spring.cloud.version>2023.0.0</spring.cloud.version> -->
    <!-- SpringBoot Cloud Alibaba适配版本 -->
    <spring.boot.version>3.0.9</spring.boot.version>
    <spring.cloud.version>2022.0.2</spring.cloud.version>
  5. 修改主启动类,添加@EnableFeignClients注解

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableFeignClients
    public class Main83 {
    public static void main(String[] args) {
    SpringApplication.run(Main83.class);
    }
    }

GateWay整合Sentinel

实例DEMO

在服务提供者9001,之前再加一层网关

cloudalibaba-sentinel-gateway9528 保护 cloudalibaba-provider-payment9001

建Module

cloudalibaba-sentinel-gateway9528

改POM文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependencies>
<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>

写YAML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 9528

spring:
application:
name: cloudalibaba-sentinel-gateway # sentinel+gataway整合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/** # 断言,路径相匹配的进行路由

主启动

1
2
3
4
5
6
@SpringBootApplication
public class Main9528 {
public static void main(String[] args) {
SpringApplication.run(Main9528.class);
}
}

配置类

config.GatewayConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@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
public void doInit() {
// 自己定义特定的处理器
initBlockHandler();
}

// 处理+自定义返回例外信息的内容,类似我们的调用触发了流控规则保护
private void initBlockHandler() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("pay_routh1")
.setCount(1)
.setIntervalSec(1)
);
GatewayRuleManager.loadRules(rules);

BlockRequestHandler handler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map<String, String> map = new HashMap<>();
map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
map.put("errorMessage", "请求太过频繁");

return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(handler);
}

SpringCloudAlibaba Seata分布式

一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题

but

关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。

分布式事务解决的问题:

单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。

此时每个服务自己内部的数据一致性由本地事务来保证但是全局的数据一致性问题没法保证

**需求:**希望提供一中分布式事务框架,解决微服务架构下的分布式事务问题

概述

Simple Extensible Autonomous Transaction Architecture:简单可扩展自治事务框架

Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务

下载地址:Release v2.0.0 · apache/incubator-seata · GitHub

@Transactional控制本地事务

@GlobalTransactional 控制全局事务

TC TM RM分别表示什么意思

image-20240412111338209

工作流程

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

Seata对分布式事务协调和控制就是1+3

一个XID是全局事务的唯一标识,他可以在服务的调用链路中传递,绑定到服务的事务的上下文

三个TC TM RMSeata术语表 | Apache Seata

TC (Transaction Coordinator) - 事务协调者

  • 维护全局和分支事务的状态,驱动全局事务提交或回滚
  • 有且仅有一个
  • 就是Seata,负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚

TM(Transaction Manager) - 事务管理器

  • 定义全局事务的范围: 开始全局事务、提交或回滚全局事务
  • 有且仅有一个
  • 标注全局@GlobalTransactional启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议

RM(Resource Manager) - 资源管理器

  • 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态
  • 可以有多个
  • 就是mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向 TC注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚

执行流程:

image-20240412160401056

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
  2. XID 在微服务调用链路的上下文中传播
  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求

安装

  1. 下载地址:seata下载地址

  2. 配置:参数配置 | Apache Seata

  3. 新人文档:新人文档 | Apache Seata

  4. 建立数据库:

    • CREATE DATABASE seata;
      USE seata;
      <!--code186-->
      
      
  5. 更改配置

    Seata下载文件夹下的conf下的application.yml中的内容,记得先备份

    按照application.example.yml的样式配置

    将模式改成db模式

    修改完成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    #  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:
    # support: file 、 db 、 redis 、 raft
    mode: db
    db:
    datasource: druid
    db-type: mysql
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true
    user: mysql
    password: mysql
    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/**

  6. 启动Nacos,startup.cmd -m standalone 和Seata seata-server.bat

  7. seata初始密码:seata

实战案例-数据库准备

这里我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。

当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题

image-20240413085213365

创建数据库

1
2
3
4
5
CREATE DATABASE seata_order;

CREATE DATABASE seata_storage;

CREATE DATABASE seata_account;

创建对应的回滚表

应为需要三个数据库的事务,要么同时成功,要么全部回滚,这时候每个数据库都需要一个回滚表

官方回滚表

1
2
3
4
5
6
7
8
9
10
11
12
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_order数据库:

1
2
3
4
5
6
7
8
9
10
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;

seata_account数据库

1
2
3
4
5
6
7
8
9
10
11
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;

seata_storage数据库

1
2
3
4
5
6
7
8
9
10
11
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;

实战案例-微服务编码

业务需求:下订单 减库存 扣余额 改订单状态

通过MybatisPlus生成entity和mapper

FeignApi接口

新增cloud-api-commons新增库存和账户两个Feign两个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
//扣减账户余额
@PostMapping("/account/decrease")
ResponseResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}

@FeignClient("seata-storage-service")
public interface StorageFeignApi {
/**
* 扣减库存
*/
@PostMapping("/storage/decrease")
ResponseResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

Order微服务

  1. 建Module seata-order-service2001

  2. 改POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    <dependencies>
    <!-- nacos -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--alibaba-seata-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <!--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>
    <!--cloud-api-commons-->
    <dependency>
    <groupId>com.atguigu.cloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
    </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>
    <!--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>
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3</version>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
  3. 写YAML

    新内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    seata:
    registry:
    type: nacos
    nacos:
    server-addr: 127.0.0.1:8848 # 将seata注册进seata
    namespace: "" # seata三件套
    group: SEATA_GROUP # seata三件套
    application: seata-server # seata三件套
    tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
    service:
    vgroup-mapping: # 点击源码分析
    default_tx_group: default # 事务组与TC服务集群的映射关系
    data-source-proxy-mode: AT

    # seata的日志级别
    logging:
    level:
    io:
    seata: info

    image-20240413103115591

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    server:
    port: 2001

    spring:
    application:
    name: seata-order-service
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 #Nacos服务注册中心地址
    # ==========applicationName + druid-mysql8 driver===================
    datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: admin
    # ========================mybatis-plus===================
    mybatis-plus:
    # 匿名域
    type-aliases-package: com.atguigu.cloud.entity
    # mapper文件映射路径
    mapper-locations: classpath:mybatis/mapper/*.xml
    configuration:
    # # 日志文件
    # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
    cache-enabled: true
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler


    # ========================seata===================
    seata:
    registry:
    type: nacos
    nacos:
    server-addr: 127.0.0.1:8848
    namespace: ""
    group: SEATA_GROUP
    application: seata-server
    tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
    service:
    vgroup-mapping: # 点击源码分析
    default_tx_group: default # 事务组与TC服务集群的映射关系
    data-source-proxy-mode: AT

    logging:
    level:
    io:
    seata: info
  4. 主启动类:

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableFeignClients
    @MapperScan("com.atguigu.cloud.mapper")
    public class SeataOrderMain2001 {
    public static void main(String[] args) {
    SpringApplication.run(SeataOrderMain2001.class, args);
    }
    }
  5. Service层方法

    OrderService

    1
    2
    3
    4
    public interface OrderService extends IService<Order> {

    public void create(Order order);
    }

    OrderServiceImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    @Service("orderService")
    @Slf4j
    public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    @Resource
    private OrderMapper orderMapper;

    @Resource // 订单微服务通过OpenFeign去调用库存微服务
    private StorageFeignApi storageFeignApi;
    @Resource // 订单微服务通过OpenFeign去调用账户微服务
    private AccountFeignApi accountFeignApi;

    @Override
    public void create(Order order) {
    // xid全局事务id检查, 重要!!!
    String xid = RootContext.getXID();
    // 1、新建订单
    log.info("---------------开始新建订单: " + "\t" + "xid: " + xid);
    // 订单初始状态为0
    order.setStatus(0);
    // 保存订单,此时订单实体已经有数据库主键了
    save(order);
    log.info("---------------新建订单成功: " + "\t" + "orderInfo: " + order);

    // 库存操作
    log.info("---------------开始调用Storage库存,扣减Count");
    storageFeignApi.decrease(order.getProductId(), order.getCount());
    log.info("---------------开始调用Storage库存,扣减Count完成");

    // 账户余额操作
    log.info("---------------开始调用Account余额,扣减余额");
    accountFeignApi.decrease(order.getUserId(), order.getMoney());
    log.info("---------------结束调用Account余额,扣减余额");

    // 修改订单状态,将0改为1
    log.info("---------------开始修改订单状态");
    order.setStatus(1);
    updateById(order);
    log.info("---------------结束修改订单状态");


    log.info("---------------结束新建订单: " + "\t" + "xid: " + xid);

    }
    }

  6. Controller层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    public class OrderController {
    @Resource
    private OrderService orderService;

    @PostMapping("/order/create")
    public ResponseResult create(Order order)
    {
    orderService.create(order);
    return ResponseResult.okResult(order);
    }
    }

Storage微服务

  1. 建Module seata-storage-service2022

  2. 改POM文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    <dependencies>
    <!-- nacos -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--alibaba-seata-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <!--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>
    <!--cloud-api-commons-->
    <dependency>
    <groupId>com.atguigu.cloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
    </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>
    <!--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>
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3</version>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
  3. 写YAML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    server:
    port: 2002

    spring:
    application:
    name: seata-storage-service
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 #Nacos服务注册中心地址
    # ==========applicationName + druid-mysql8 driver===================
    datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: admin
    # ========================mybatis-plus===================
    mybatis-plus:
    # 匿名域
    type-aliases-package: com.atguigu.cloud.entity
    # mapper文件映射路径
    mapper-locations: classpath:mapper/*.xml
    configuration:
    # # 日志文件
    # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
    cache-enabled: true
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler


    # ========================seata===================
    seata:
    registry:
    type: nacos
    nacos:
    server-addr: 127.0.0.1:8848
    namespace: ""
    group: SEATA_GROUP
    application: seata-server
    tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
    service:
    vgroup-mapping: # 点击源码分析
    default_tx_group: default # 事务组与TC服务集群的映射关系
    data-source-proxy-mode: AT

    logging:
    level:
    io:
    seata: info
  4. 主启动

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableFeignClients
    @MapperScan("com.atguigu.cloud.mapper")
    public class SeataStorageMain2022 {
    public static void main(String[] args) {
    SpringApplication.run(SeataStorageMain2022.class, args);
    }
    }
  5. 服务类

    StorageService

    1
    2
    3
    public interface StorageService extends IService<Storage> {
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
    }

    StorageServiceImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Service("storageService")
    public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements StorageService {
    /**
    * 扣减库存
    */
    @Override
    public void decrease(@Param("productId") Long productId, @Param("count") Integer count){
    LambdaQueryWrapper<Storage> queryWrapper = new LambdaQueryWrapper<Storage>();
    queryWrapper.eq(Storage::getProductId, productId);
    Storage storage = getOne(queryWrapper);
    storage.setTotal(storage.getTotal()-count);
    updateById(storage);
    }
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RestController
    public class StorageController
    {
    @Resource
    private StorageService storageService;

    /**
    * 扣减库存
    */
    @PostMapping("/storage/decrease")
    public ResponseResult decrease(Long productId, Integer count) {

    storageService.decrease(productId, count);
    return ResponseResult.okResult("扣减库存成功!");
    }
    }

Account微服务

  1. 建Module seata-account-service2003

  2. 改POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    <dependencies>
    <!-- nacos -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--alibaba-seata-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <!--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>
    <!--cloud-api-commons-->
    <dependency>
    <groupId>com.atguigu.cloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
    </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>
    <!--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>
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3</version>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
  3. 写YAML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    server:
    port: 2003

    spring:
    application:
    name: seata-account-service
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 #Nacos服务注册中心地址
    # ==========applicationName + druid-mysql8 driver===================
    datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: admin
    # ========================mybatis-plus===================
    mybatis-plus:
    # 匿名域
    type-aliases-package: com.atguigu.cloud.entity
    # mapper文件映射路径
    mapper-locations: classpath:mapper/*.xml
    configuration:
    # # 日志文件
    # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
    cache-enabled: true
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler


    # ========================seata===================
    seata:
    registry:
    type: nacos
    nacos:
    server-addr: 127.0.0.1:8848
    namespace: ""
    group: SEATA_GROUP
    application: seata-server
    tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
    service:
    vgroup-mapping: # 点击源码分析
    default_tx_group: default # 事务组与TC服务集群的映射关系
    data-source-proxy-mode: AT

    logging:
    level:
    io:
    seata: info
  4. 主启动

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableFeignClients
    @MapperScan("com.atguigu.cloud.mapper")
    public class SeataAccountMain2003 {
    public static void main(String[] args) {
    SpringApplication.run(SeataAccountMain2003.class, args);
    }
    }
  5. Service服务提供

    AccountService

    1
    2
    3
    4
    5
    6
    7
    8
    public interface AccountService extends IService<Account> {
    /**
    * 扣减账户余额
    * @param userId 用户id
    * @param money 本次消费金额
    */
    void decrease(@Param("userId") Long userId, @Param("money") Long money);
    }

    AccountServiceImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Service("accountService")
    public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {

    @Override
    public void decrease(Long userId, Long money) {
    LambdaQueryWrapper<Account> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Account::getUserId, userId);
    Account account = getOne(queryWrapper);
    account.setResidue(account.getResidue() - money);
    updateById(account);
    }
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RestController
    public class AccountController {
    @Resource
    private AccountService accountService;


    /**
    * 扣减账户余额
    */
    @PostMapping("/account/decrease")
    public ResponseResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money){
    accountService.decrease(userId,money);
    return ResponseResult.okResult("扣减账户余额成功!");
    }
    }

测试

没有添加@GlobalTransactional

在没有添加@GlobalTransactional的时候

当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1

但是三个数据库都已经产生效果

但是出错之后没有回退情况,还是会保持原先已经产生的效果

所以在没有添加@GlobalTransactional的时候如果一程序出错,不会发生回退情况

添加@GlobalTransactional

添加全局事务注解:设置名称,和异常处理方法

1
@GlobalTransactional(name="nuyoah-create-order", rollbackFor = Exception.class)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
@GlobalTransactional(name="nuyoah-create-order", rollbackFor = Exception.class)
public void create(Order order) {
// xid全局事务id检查, 重要!!!
String xid = RootContext.getXID();
// 1、新建订单
log.info("---------------开始新建订单: " + "\t" + "xid: " + xid);
// 订单初始状态为0
order.setStatus(0);
// 保存订单,此时订单实体已经有数据库主键了
save(order);
log.info("---------------新建订单成功: " + "\t" + "orderInfo: " + order);

// 库存操作
log.info("---------------开始调用Storage库存,扣减Count");
storageFeignApi.decrease(order.getProductId(), order.getCount());
log.info("---------------开始调用Storage库存,扣减Count完成");

// 账户余额操作
log.info("---------------开始调用Account余额,扣减余额");
accountFeignApi.decrease(order.getUserId(), order.getMoney().longValue());
log.info("---------------结束调用Account余额,扣减余额");

// 修改订单状态,将0改为1
log.info("---------------开始修改订单状态");
order.setStatus(1);
updateById(order);
log.info("---------------结束修改订单状态");


log.info("---------------结束新建订单: " + "\t" + "xid: " + xid);

}

此时订单模块是TM,也是其中一个RM,谁使用@GlobalTransactional注解,谁就是TM

image-20240413200542694

Seata通过全局锁XID将三个事务连接在一起

image-20240413201635557

三个事务通过XID绑定在一起

image-20240413201717972

添加了@GlobalTransactional之后,在数据库发生改变的时候,会将原先的操作存放在undo_log表中,方便事务回滚,当回滚完事务的时候,undo_log表中的数据就会被删除

机制

两阶段提交协议的演变:

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

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

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

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

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

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

image-20240413203445185

二阶段如是顺利提交的话

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

顺利提完完毕之后:undo_log会将回滚记录删除

image-20240413203842413

二阶段回滚

二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据

回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,

如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理

image-20240413204024821