SpringCloud笔记
#Part1 Spring Cloud
一、微服务概述
微服务架构就是把—个大系统按业务功能分解成多个职责单一的小系统,每一个小模块尽量专一的只做一件事,并利用简单的方法使多个小系统相互协作,组合成一个大系统后再统一对外提供整体服务。
专注于单一责任小型功能模块为基础,并利用轻量化机制(通常为HTTP RESTful API)实现通信完成复杂业务搭建的一种架构思想。
SpringCloud官网:https://spring.io/projects/spring-cloud

二、🛠️案例基础工程模块构建
1.业务需求说明
需求:下订单,调用支付。
- 先做一个通用Spring Boot微服务。
- 再逐步依次引入Spring Cloud组件纳入微服务支撑体系。
2. 新建项目
2.1 Maven创建空父工程
(1) 创建空项目
创建Maven空项目 springcloud-learn。
(2) 聚合总父工程的名称
创建后生成的根项目pom.xml。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
(3) 字符编码

(4) 注解生效激活

(5) Java编译版本

(6) 文本过滤

(7) maven中跳过单元测试
2.2 父工程POM文件内容
根项目pom.xml。
[!NOTE]
<dependencyManagement>里只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖。
<?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.example.cloud</groupId>
<artifactId>springcloud-learn</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>2023.0.1.0</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 2023.0.1.0-->
<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 -->
<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. Mapper4一键生成
MyBatis-Mapper仓库:https://github.com/abel533/Mapper
MyBatis-Mapper文档:https://mapper.mybatis.io/
[!TIP]
本节 Mapper4 主要用于自动生成数据库表的CRUD,建议使用 MybatisX逆向工程 更加方便。
如果使用 MybatisX 逆向工程,则该模块没有存在的必要,直接生成在业务模块即可。
3.1 创建数据库
创建数据库 db2024,再创建表 t_pay:
DROP TABLE IF EXISTS `t_pay`;
CREATE TABLE `t_pay`
(
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`pay_no` VARCHAR(50) NOT NULL COMMENT '支付流水号',
`order_no` VARCHAR(50) NOT NULL COMMENT '订单流水号',
`user_id` INT(10) DEFAULT '1' COMMENT '用户账号ID',
`amount` DECIMAL(8, 2) NOT NULL DEFAULT '9.9' COMMENT '交易金额',
`deleted` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除标志,默认0不删除,1删除',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4
COMMENT ='支付交易表';
INSERT INTO t_pay(pay_no, order_no)
VALUES ('pay17203699', '6544bafb424a');
SELECT *
FROM t_pay;
3.2 创建数据库工具模块
在根项目下创建子工具模块 mybatis_generator2024,用于专门生成对数据库的CRUD。
3.3 修改pom
修改子工具模块 mybatis_generator2024 的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!--该模块独一份,只是一个普通Maven工程,与boot和cloud无关-->
<artifactId>mybatis_generator2024</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--Mybatis 通用mapper tk单独使用,自己独有+自带版本号-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- Mybatis Generator 自己独有+自带版本号-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--mysql8驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence持久化工具api-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<!-- 构建配置 -->
<build>
<!-- 资源目录 -->
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
</resources>
<!-- spring-boot插件 -->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- mybatis-generator插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
<!-- 读取的配置文件位置 -->
<configuration>
<configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.2.3</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
3.4 配置文件
在子工具模块 mybatis_generator2024 的 src/main/resources目录 中新建2个配置文件:
-
config.properties:Jdbc的配置文件。#t_pay表包名 package.name=com.example.cloud # mysql8.0 jdbc.driverClass=com.mysql.cj.jdbc.Driver # 1. localhost是ip地址 # 2. db2024是数据库名 jdbc.url=jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true jdbc.user=root jdbc.password=123456 -
generatorConfig.xml:mybatis-generator的配置文件。<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <properties resource="config.properties"/> <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/> <property name="caseSensitive" value="true"/> </plugin> <!-- 读取jdbc驱动的配置 --> <jdbcConnection driverClass="${jdbc.driverClass}" connectionURL="${jdbc.url}" userId="${jdbc.user}" password="${jdbc.password}"> </jdbcConnection> <!-- 生成实体类 --> <javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/> <!-- 生成Mapper接口 --> <sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/> <!-- 生成MapperXML实现 --> <javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/> <!-- t_pay表对应的实体类名Pay --> <table tableName="t_pay" domainObjectName="Pay"> <generatedKey column="id" sqlStatement="JDBC"/> </table> </context> </generatorConfiguration>
3.5 一键生成
自动生成前:
自动生成后:
4. 构建提供者支付模块8001
创建微服务提供者支付Module模块 cloud-provider-payment8001。
4.1 建Module
4.2 改pom
修改微服务提供者支付Module模块 cloud-provider-payment8001 的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-provider-payment8001</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringBoot监控-->
<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 -->
<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>
4.3 写yml
在微服务提供者支付Module模块 cloud-provider-payment8001 的 src/main/resources目录 中创建 application.yml 文件。
# ===================模块端口号===================
server:
port: 8001
spring:
# ===================微服务名===================
application:
name: cloud-payment-service
# ===================Druid连接池===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
# ===================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.cloud.entities
configuration:
map-underscore-to-camel-case: true
4.4 主启动
在微服务提供者支付Module模块 cloud-provider-payment8001 修改(创建)Main8001类。
package com.example.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan("com.example.cloud.mapper") // tk.mybatis.spring.annotation.MapperScan
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class, args);
}
}
4.5 业务类
(1) 剪切生成的代码
将 mybatis_generator2024 工具模块生成的代码剪切到 cloud-provider-payment8001 模块中。
[!TIP]
建议直接使用 MybatisX 逆向工程,避免多此一举。
(2) ☀️创建数据传输对象DTO
在 cloud-provider-payment8001 模块的 entities包 中创建 PayDTO类,用于和前端数据交互,防止使用 Pay 类接受数据而暴露表结构。
数据传输对象(DTO,Data Transfer Object),用于封装和传输应用程序不同层之间的数据。
DTO是轻量级对象,通常只包含必要的字段,不包含任何业务逻辑。DTO作用于应用程序中不同的业务之间的数据传输,例如在前端和后端之间或在分布式系统中不同的微服务之间。
在Spring Boot应用程序中,DTO特别有用,因为需要在控制器层、服务层和持久层之间传输数据。通过使用DTO就可以将内部数据模型与外部表示解耦,从而更好地控制数据传输。
一般而言,调用者不应该获悉服务提供者的entity资源并知道表结构关系,所以服务提供方给出的接口文档都都应成为DTO。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PayDTO implements Serializable {
private Integer id;
//支付流水号
private String payNo;
//订单流水号
private String orderNo;
//用户账号ID
private Integer userId;
//交易金额
private BigDecimal amount;
}
(3) service
① 接口
在 cloud-provider-payment8001 模块中创建 service包 中的 PayService接口。
public interface PayService {
int add(Pay pay);
int delete(Integer id);
int update(Pay pay);
Pay getById(Integer id);
List<Pay> getAll();
}
② 实现类
在 cloud-provider-payment8001 模块中创建 service/impl包 中的 PayServiceImpl实现类。
@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();
}
}
(4) controller【v1】
在 cloud-provider-payment8001 模块中创建 controller包 中的 PayController类。
@RestController
public class PayController {
@Resource
PayService payService;
@PostMapping(value = "/pay/add")
public String addPay(@RequestBody Pay pay) {
System.out.println(pay.toString());
int i = payService.add(pay);
return "成功插入记录,返回值:" + i;
}
@DeleteMapping(value = "/pay/del/{id}")
public Integer deletePay(@PathVariable("id") Integer id) {
return payService.delete(id);
}
@PutMapping(value = "/pay/update")
public String updatePay(@RequestBody PayDTO payDTO) {
Pay pay = new Pay();
BeanUtils.copyProperties(payDTO, pay);
int i = payService.update(pay);
return "成功修改记录,返回值:" + i;
}
@GetMapping(value = "/pay/get/{id}")
public Pay getById(@PathVariable("id") Integer id) {
return payService.getById(id);
}
@GetMapping(value = "/pay/get")
public List<Pay> getAll() {
return payService.getAll();
}
}
4.6 Swagger测试
(1) Swagger常用注解
| 注解 | 标注位置 | 作用 |
|---|---|---|
@Tag |
controller类 | 标识controller作用 |
@Parameter |
参数 | 标识参数作用 |
@Parameters |
参数 | 参数多重说明 |
@Schema |
JavaBean | 描述模型作用及每个属性 |
@Operation |
方法 | 描述方法作用 |
@ApiResponse |
方法 | 描述响应状态码 |
(2) 修改controller添加Swagger注解【v2】
修改 cloud-provider-payment8001 模块下 controller包 中的 PayController类。
@RestController
@Tag(name = "支付微服务模块", description = "支付CRUD")
public class PayController {
@Resource
PayService payService;
@PostMapping(value = "/pay/add")
@Operation(summary = "新增", description = "新增支付流水方法,json串做参数")
public String addPay(@RequestBody Pay pay) {
System.out.println(pay.toString());
int i = payService.add(pay);
return "成功插入记录,返回值:" + i;
}
@DeleteMapping(value = "/pay/del/{id}")
@Operation(summary = "删除", description = "删除支付流水方法")
public Integer deletePay(@PathVariable("id") Integer id) {
return payService.delete(id);
}
@PutMapping(value = "/pay/update")
@Operation(summary = "修改", description = "修改支付流水方法")
public String updatePay(@RequestBody PayDTO payDTO) {
Pay pay = new Pay();
BeanUtils.copyProperties(payDTO, pay);
int i = payService.update(pay);
return "成功修改记录,返回值:" + i;
}
@GetMapping(value = "/pay/get/{id}")
@Operation(summary = "按照ID查流水", description = "查询支付流水方法")
public Pay getById(@PathVariable("id") Integer id) {
return payService.getById(id);
}
@GetMapping(value = "/pay/get")
@Operation(summary = "查全部流水", description = "查询全部支付流水方法")
public List<Pay> getAll() {
return payService.getAll();
}
}
(3) 修改实体类添加Swagger注解
@Table(name = "t_pay")
@Schema(title = "支付交易表实体类")
public class Pay {
@Id
@GeneratedValue(generator = "JDBC")
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删除
*/
private Byte deleted;
/**
* 创建时间
*/
@Column(name = "create_time")
@Schema(title = "创建时间")
private Date createTime;
/**
* 更新时间
*/
@Column(name = "update_time")
@Schema(title = "更新时间")
private Date updateTime;
// setter, getter......
}
(4) 创建含分组迭代的配置类
在 cloud-provider-payment8001 模块中创建 config包 中的 Swagger3Config类。
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;
/**
* @author CorianderSaint
* @date 2024/10/9 15:17
* @description
*/
@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("springcloud-learn")
.description("通用base工程")
.version("v1.0"))
// 联系方式
.externalDocs(new ExternalDocumentation()
.description("www.example.com")
.url("https://yiyan.baidu.com/"));
}
}
(5) 启动测试
启动 Main8001,在浏览器输入 http://localhost:8001/swagger-ui/index.html,进行测试。

4.7 完善代码
(1) ☀️时间格式问题
方案1:添加 @JsonFormat 注解。
// @JsonFormat(pattern = "时间格式", timezone = "时区")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
方案2:在 application.yml 文件中指定。
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
修改 Pay实体类。
@Table(name = "t_pay")
@Schema(title = "支付交易表实体类")
public class Pay {
// ......
/**
* 创建时间
*/
@Column(name = "create_time")
@Schema(title = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 更新时间
*/
@Column(name = "update_time")
@Schema(title = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
// ......
}
(2) ☀️统一返回值
思路:定义返回标准格式,3大标配+1拓展。
code状态值:由后端统一定义各种返回结果的状态码。message描述:对本次接口调用的结果描述,也即对该状态值对应的描述。data数据:本次返回的数据。timestamp时间戳:接口调用的时间,方便调试。
* HTTP请求返回的状态码
| 分类 | 区间 | 分类描述 |
|---|---|---|
| 1** | 100 ~ 199 | 信息,服务器收到请求,需要请求者继续执行操作。 |
| 2** | 200 ~ 299 | 成功,操作被成功接收并处理。 |
| 3** | 300 ~ 399 | 重定向,需要进一步的操作以完成请求。 |
| 4** | 400 ~ 499 | 客户端错误,请求包含语法错误或无法完成请求。 |
| 5** | 500 ~ 599 | 服务器错误,服务器在处理请求的过程中发生了错误。 |
① 创建状态码枚举类
在 cloud-provider-payment8001 模块中创建 resp包 中的 ReturnCodeEnum枚举类。
import lombok.Getter;
import java.util.Arrays;
@Getter
public enum ReturnCodeEnum {
// ============================== 枚举值 ==============================
/**
* XXX操作失败
**/
RC999("999", "操作XXX失败"),
/**
* 操作成功
**/
RC200("200", "success"),
/**
* 服务降级
**/
RC201("201", "服务开启降级保护,请稍后再试!"),
/**
* 热点参数限流
**/
RC202("202", "热点参数限流,请稍后再试!"),
/**
* 系统规则不满足
**/
RC203("203", "系统规则不满足要求,请稍后再试!"),
/**
* 授权规则不通过
**/
RC204("204", "授权规则不通过,请稍后再试!"),
/**
* access_denied
**/
RC403("403", "无访问权限,请联系管理员授予权限"),
/**
* access_denied
**/
RC401("401", "匿名用户访问无权限资源时的异常"),
RC404("404", "404页面找不到的异常"),
/**
* 服务异常
**/
RC500("500", "系统异常,请稍后重试"),
RC375("375", "数学运算异常,请稍后重试"),
INVALID_TOKEN("2001", "访问令牌不合法"),
ACCESS_DENIED("2003", "没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED("1001", "客户端认证失败"),
USERNAME_OR_PASSWORD_ERROR("1002", "用户名或密码错误"),
BUSINESS_ERROR("1004", "业务逻辑异常"),
UNSUPPORTED_GRANT_TYPE("1003", "不支持的认证模式");
// ============================== 构造器 ==============================
// 自定义的状态码
private final String code;
// 自定义的描述信息
private final String message;
// 生成构造器
ReturnCodeEnum(String code, String message) {
this.code = code;
this.message = message;
}
// ============================== 遍历枚举值 ==============================
// Stream流式计算
public static ReturnCodeEnum getReturnCodeEnum(String code) {
return Arrays.stream(ReturnCodeEnum.values())
.filter(x -> x.getCode().equalsIgnoreCase(code))
.findFirst()
.orElse(null);
}
/* 以上代码等价于:
public static ReturnCodeEnum getReturnCodeEnum(String code) {
for (ReturnCodeEnum element : ReturnCodeEnum.values()) {
if (element.getCode().equalsIgnoreCase(code)) {
return element;
}
}
return null;
}
*/
}
② 创建统一定义返回对象
在 cloud-provider-payment8001 模块的 resp包 中创建 ResultData类。
@Data
@Accessors(chain = true) // 允许链式编程
public class ResultData<T> {
private String code;
private String message;
private T data;
private long timestamp;
/**
* 构造器
*/
public ResultData() {
// 每次调用接口时,都记录调用的时刻
this.timestamp = System.currentTimeMillis();
}
/**
* 成功返回
*/
public static <T> ResultData<T> success(T data) {
ResultData<T> resultData = new ResultData<>();
resultData.setCode(ReturnCodeEnum.RC200.getCode());
resultData.setMessage(ReturnCodeEnum.RC200.getMessage());
resultData.setData(data);
return resultData;
}
/**
* 失败返回
*/
public static <T> ResultData<T> fail(String code, String message) {
ResultData<T> resultData = new ResultData<>();
resultData.setCode(code);
resultData.setMessage(message);
return resultData;
}
}
③ 修改controller统一返回对象【v3(last)】
修改 cloud-provider-payment8001 模块下 controller包 中的 PayController类。
@RestController
@Tag(name = "支付微服务模块", description = "支付CRUD")
public class PayController {
@Resource
PayService payService;
@PostMapping(value = "/pay/add")
@Operation(summary = "新增", description = "新增支付流水方法,json串做参数")
public ResultData<String> addPay(@RequestBody Pay pay) {
System.out.println(pay.toString());
int i = payService.add(pay);
return ResultData.success("成功插入记录,返回值:" + i);
}
@DeleteMapping(value = "/pay/del/{id}")
@Operation(summary = "删除", description = "删除支付流水方法")
public ResultData<Integer> deletePay(@PathVariable("id") Integer id) {
int i = payService.delete(id);
return ResultData.success(i);
}
@PutMapping(value = "/pay/update")
@Operation(summary = "修改", description = "修改支付流水方法")
public ResultData<String> updatePay(@RequestBody PayDTO payDTO) {
Pay pay = new Pay();
BeanUtils.copyProperties(payDTO, pay);
int i = payService.update(pay);
return ResultData.success("成功修改记录,返回值:" + i);
}
@GetMapping(value = "/pay/get/{id}")
@Operation(summary = "按照ID查流水", description = "查询支付流水方法")
public ResultData<Pay> getById(@PathVariable("id") Integer id) {
Pay pay = payService.getById(id);
return ResultData.success(pay);
}
@GetMapping(value = "/pay/get")
@Operation(summary = "查全部流水", description = "查询全部支付流水方法")
public ResultData<List<Pay>> getAll() {
return ResultData.success(payService.getAll());
}
}
(3) ☀️全局异常处理
在 cloud-provider-payment8001 模块中创建 exp包 中的 GlobalExceptionHandler类。
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 默认全局异常处理。
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(Exception e) {
System.out.println("----come in GlobalExceptionHandler");
log.error("全局异常信息exception:{}", e.getMessage(), e);
return ResultData.fail(ReturnCodeEnum.RC500.getCode(), e.getMessage());
}
}
4.8 到目前为止的工程目录结构
目前为止 cloud-provider-payment8001 模块的工程目录结构。
5. 构建消费者订单模块80
创建微服务调用者订单Module模块 cloud-consumer-order80。
5.1 建Module
5.2 改pom
修改 cloud-consumer-order80 模块中的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-consumer-order80</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- swagger3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5.3 写yml
在 cloud-consumer-order80 模块的 resources目录 中创建 application.yml配置文件。
server:
port: 80
5.4 主启动
在 cloud-consumer-order80 模块中修改(创建)Main80类。
@SpringBootApplication
public class Main80 {
public static void main(String[] args) {
SpringApplication.run(Main80.class, args);
}
}
5.5 业务类
(1) 创建数据传输对象DTO
在 cloud-consumer-order80 模块中创建 entities包 中的 PayDTO类。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PayDTO implements Serializable {
private Integer id;
//支付流水号
private String payNo;
//订单流水号
private String orderNo;
//用户账号ID
private Integer userId;
//交易金额
private BigDecimal amount;
}
(*) ☀️RestTemplate
① 简介
RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集。
② 常用API
| 返回值类型 | 方法 | 说明 |
|---|---|---|
<T> ResponseEntity<T> |
getForEntity(String url, Class<T> responseType, Map<String,?> uriVariables) |
通过对指定的URL执行GET操作来检索实体。 |
<T> T |
getForObject(String url, Class<T> responseType, Map<String,?> uriVariables) |
通过对指定的URL执行GET操作来检索表示形式。 |
<T> ResponseEntity<T> |
postForEntity(String url, Object request, Class<T> responseType, Map<String,?> uriVariables) |
通过将给定对象post到URI模板来创建新资源,并将响应作为HttpEntity返回。 |
<T> T |
patchForObject(String url, Object request, Class<T> responseType, Map<String,?> uriVariables) |
通过将给定对象修补到URI模板来更新资源,并返回在响应中找到的表示。 |
方法参数说明:
url:REST请求地址。responseType:HTTP响应转换被转换成的对象类型(ResultData.class)。uriVariables:请求参数(DTO)。
③ 使用方法
-
getForObject():返回对象为响应体中数据转化成的对象,基本上可以理解为Json。@GetMapping("/consumer/pay/get/{id}") public ResultData getPayment(@PathVariable("id") Integer id) { return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/" + id, ResultData.class, id); } -
getForEntity():返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。@GetMapping("/consumer/pay/get/{id}") public ResultData getPayment(@PathVariable("id") Integer id) { ResponseEntity<ResultData> entity = restTemplate.getForEntity( PaymentSrv_URL + "/pay/get/" + id, ResultData.class, id); if (entity.getStatusCode().is2xxSuccessful()) { return entity.getBody(); } else { return ResultData.fail("444", "操作失败"); } } -
postForObject()@GetMapping("/consumer/pay/create") public ResultData createPayment(PayDTO payDTO) { return restTemplate.postForObject(PaymentSrv_URL + "/pay/create", payDTO, ResultData.class); } -
postForEntity()@GetMapping("/consumer/pay/create") public ResultData createPayment(PayDTO payDTO) { return restTemplate .postForEntity(PaymentSrv_URL + "/pay/create", payDTO, ResultData.class) .getBody(); }
(2) 配置类
在 cloud-consumer-order80 模块中创建 config包 中的 RestTemplateConfig类。
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
(3) 统一返回值
拷贝 cloud-provider-payment8001 模块中 resp包 的内容到 cloud-consumer-order80 模块中。
(4) 消费者模块controller
在 cloud-consumer-order80 模块中创建 controller包 中的 OrderController类。
@RestController
public class OrderController {
public static final String PaymentSrv_URL = "http://localhost:8001";// 先写死,硬编码
@Resource
private RestTemplate restTemplate;
/**
* 增加订单
* 一般情况下,通过浏览器的地址栏输入url,发送的只能是get请求
* 我们底层调用的是post方法,模拟消费者发送get请求,客户端消费者
* 参数可以不添加@RequestBody
*/
@GetMapping("/consumer/pay/add")
public ResultData addOrder(PayDTO payDTO) {
return restTemplate.postForObject(PaymentSrv_URL + "/pay/add", payDTO, ResultData.class);
}
/**
* 删除订单
*/
@GetMapping("/consumer/pay/del/{id}")
public ResultData deleteOrder(@PathVariable("id") Integer id) {
return restTemplate.postForObject(PaymentSrv_URL + "/pay/del/" + id, null, ResultData.class);
}
/**
* 更新订单
*/
@PutMapping("/consumer/pay/update")
public ResultData updateOrder(PayDTO payDTO) {
return restTemplate.postForObject(PaymentSrv_URL + "/pay/update", payDTO, ResultData.class);
}
/**
* 查询订单
*/
@GetMapping("/consumer/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id) {
return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/" + id, ResultData.class, id);
}
}
(5) 启动测试
在IDEA中,以Services启动微服务。每个启动后的Main类都会进行统一管理。
6. 提取公共代码模块
6.0 代码问题

6.1 建Module
创建公用Module模块 cloud-api-commons。
6.2 改pom
修改 cloud-api-commons 模块中的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-api-commons</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
</project>
6.3 提取公共类
6.4 maven打包
将公用Module模块 cloud-api-commons 打成jar包,供别的模块调用。

6.5 改造原模块
(1) 删除公共内容
(2) 引入公共模块
分别在 cloud-provider-payment8001 和 cloud-consumer-order80 模块的 pom.xml 文件中引入公共模块的jar包。
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.example.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
6.6 到目前为止的工程目录结构
[!WARNING]
截至到目前,尚未引入任何 SpringCloud 相关内容。
不引入微服务存在的问题:如在
cloud-consumer-order80模块的 [controller](#(4) 消费者模块controller) 中,微服务所在的IP地址和端口号硬编码到订单微服务中,会存在非常多的问题。public static final String PaymentSrv_URL = "http://localhost:8001";// 先写死,硬编码
- 如果订单微服务和支付微服务的IP地址或者端口号发生了变化,则支付微服务将变得不可用,需要同步修改订单微服务中调用支付微服务的IP地址和端口号。
- 如果系统中提供了多个订单微服务和支付微服务,则无法实现微服务的负载均衡功能。
- 如果系统需要支持更高的并发,需要部署更多的订单微服务和支付微服务,硬编码订单微服务则后续的维护会变得异常复杂。
所以,在微服务开发的过程中,需要引入服务治理功能,实现微服务之间的动态注册与发现。

三、Consul服务注册与发现
1. Consul简介
1.1 简介
Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发。
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。它具有很多优点。包括: 基于 raft 协议,比较简洁; 支持健康检查, 同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持 Linux、Mac、Windows
官方文档:https://developer.hashicorp.com/consul/docs
1.2 作用
- 服务发现:提供HTTP和DNS两种发现方式。
- 健康监测:支持多种方式,HTTP、TCP、Docker、Shell脚本定制化监控。
- KV存储:Key、 Value的存储方式。
- 多数据中心:Consul支持多数据中心。
- 可视化Web界面
2. 下载安装
2.1 安装
官网下载:https://developer.hashicorp.com/consul/install
验证安装是否成功:
consul --version
2.2 ☀️使用开发者模式启动Consul
命令行中输入:
consul agent -dev
然后在浏览器中访问:http://localhost:8500

3. 🛠️服务注册与发现
官网文档:https://docs.spring.io/spring-cloud-consul/reference/quickstart.html#discovery-client-usage
3.1 服务提供者8001模块
(1) 改pom
修改 cloud-provider-payment8001 模块的 pom.xml 文件,添加Consul服务发现依赖。
<!-- Consul服务发现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<!-- 排除logging干扰(可选) -->
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
(2) 改yml
修改 cloud-provider-payment8001 模块的 application.yml 文件,添加Consul配置。
# ===================consul===================
spring:
# 微服务名
application:
name: cloud-payment-service
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name} # 注册的服务名
(3) 主启动|@EnableDiscoveryClient
修改 cloud-provider-payment8001 模块的 Main8001类,添加 @EnableDiscoveryClient 注解激活consul开启服务发现。
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.example.cloud.mapper") // tk.mybatis.spring.annotation.MapperScan
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class, args);
}
}
(4) 启动测试
启动 cloud-provider-payment8001 模块的 Main8001类,在consul的 web ui 中查看。

3.2 服务消费者80模块
(1) 改pom
修改 cloud-consumer-order80 模块的 pom.xml 文件,添加Consul依赖。
<!-- Consul服务发现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<!-- 排除logging干扰(可选) -->
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
(2) 改yml
修改 cloud-consumer-order80 模块的 application.yml 文件,添加Consul配置。
# ===================consul===================
spring:
# 微服务名
application:
name: cloud-consumer-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name} # 注册的服务名
prefer-ip-address: true # 优先使用服务ip进行注册
(3) 主启动
修改 cloud-consumer-order80 模块的 Main80类,添加 @EnableDiscoveryClient 注解激活consul开启服务发现。
@EnableDiscoveryClient
@SpringBootApplication
public class Main80 {
public static void main(String[] args) {
SpringApplication.run(Main80.class, args);
}
}
(4) 修改controller中的url
修改 cloud-consumer-order80 模块下 controller包 中的 OrderController类。
@RestController
public class OrderController {
// 使用consul服务注册中心上的微服务名称,而不使用硬编码写死url地址
public static final String PaymentSrv_URL = "http://cloud-payment-service";
//public static final String PaymentSrv_URL = "http://localhost:8001";//先写死,硬编码
// ......
}
(5) 修改RestTemplateConfig支持负载均衡
修改 cloud-consumer-order80 模块下 config包 中的 RestTemplateConfig类,添加 @LoadBalanced 注解使其支持负载均衡。
[!CAUTION]
Consul 默认支持负载均衡,在 controller 中通过微服务的名字调用,默认是查找该服务名对应的集群,所以在 RestTemplate 中必须要注明支持负载均衡,否则在调用时会报
java.net.UnknownHostException异常。
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
(6) 启动测试
启动 cloud-consumer-order80 模块的 Main80类,在consul的 web ui 中查看。

3.3 三种注册中心的异同点
(1) CAP
CAP
C:Consistency强一致性。A:Availability可用性。P:Partition tolerance分区容错性。

最多只能同时较好的满足两个。
CAP理论的核心是:一个分布式系统不可能同时很好的满足 一致性、可用性和分区容错性 这三个需求。
因此根据 CAP 原理将 NoSQL 数据库分成了满足 CA原则、满足 CP原则 和满足 AP原则三大类:
CA- 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。CP- 满足一致性,分区容忍性的系统,通常性能不是特别高。AP- 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
(2) 注册中心比较
| 组件 | 编写语言 | CAP | 服务健康检查 | 对外暴露接口 | SpringCloud集成 |
|---|---|---|---|---|---|
| Eureka | Java | AP | 可配支持 | HTTP | 已集成 |
| Consul | Go | CP | 支持 | HTTP/DNS | 已集成 |
| Zookeeper | Java | CP | 支持 | 客户端 | 已集成 |
① AP(Eureka)
AP架构:当网络分区出现后,为了保证可用性,系统B可以返回旧值,以保证系统的可用性。
当数据出现不一致时,虽然A, B上的注册信息不完全相同,但每个Eureka节点依然能够正常对外提供服务,这会出现查询服务信息时如果请求A查不到,但请求B就能查到,如此保证了可用性但牺牲了一致性。
结论:违背了一致性C的要求,只满足可用性和分区容错,即AP。

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

4. 🛠️服务配置与刷新
官方文档:https://docs.spring.io/spring-cloud-consul/reference/quickstart.html#distributed-configuration-usage
4.0 需求说明
将通用的全局配置信息(application.yml)注册进Consul服务器,从Consul获取。
4.1 改pom
修改 cloud-provider-payment8001 模块的 pom.xml 文件,添加Consul服务配置依赖。
<!--Consul服务配置-->
<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>
4.2 写yml
(1) bootstrap.yml
applicaiton.yml是用户级的资源配置项。bootstrap.yml是系统级的,优先级更加高。
Spring Cloud会创建一个
Bootstrap Context,作为Spring应用的Application Context的父上下文。初始化的时候,Bootstrap Context负责从外部源(Consul中)加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment。
Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap context和Application Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap Context和Application Context配置的分离。
application.yml文件改为bootstrap.yml或者两者共存。因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml
在 cloud-provider-payment8001 模块的 resources目录 中创建 bootstrap.yml文件。
spring:
application:
name: cloud-payment-service
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
# 文件分隔符(默认是“,”)
profile-separator: '-'
format: YAML
# config/cloud-payment-service/data
# /cloud-payment-service-dev/data
# /cloud-payment-service-prod/data
(2) application.yml
修改 cloud-provider-payment8001 模块的 resources目录 中 application.yml文件。
# ===================模块端口号===================
server:
port: 8001
spring:
# ===================Druid连接池===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
profiles:
# 多环境配置加载内容dev/prod/test,不写就是默认default配置
active: dev
# ===================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.cloud.entities
configuration:
map-underscore-to-camel-case: true
4.3 consul服务器key/value配置填写
(0) 参考规则
# default默认环境
config/cloud-payment-service/data
# dev开发环境
config/cloud-payment-service-dev/data
# prod生产环境
config/cloud-payment-service-prod/data
# test测试环境
config/cloud-payment-service-test/data
(1) 创建config文件夹
在 consul 服务器中创建 Key/Value 文件夹。

(2) 创建子文件夹
在创建的 config 文件夹中创建子文件夹。




(3) 创建子data
在上述三个子文件夹中分别创建data内容,data不再是文件夹,不要以 / 结尾。



4.4 读取data内容测试
在 cloud-provider-payment8001 模块下 controller包 中的 PayController类 中添加获取consul服务器中data内容的测试代码。
@RestController
@Tag(name = "支付微服务模块", description = "支付CRUD")
public class PayController {
// ......
@Value("${server.port}") // org.springframework.beans.factory.annotation.Value
private String port;
@GetMapping(value = "/pay/get/info")
public String getInfoFromConsul(@Value("${example.info}") String exampleInfo) {
return "DataInfo: " + exampleInfo + "\t" + "port: " + port;
}
}
启动 Main8001,在浏览器输入 http://localhost:8001/pay/get/info 测试。
浏览器中返回结果:
DataInfo: This is dev config, version=1 port: 8001
[!NOTE]
在
application.yml文件中可以切换环境配置。# 多环境配置加载内容dev spring: profiles: active: dev # 多环境配置加载内容prod spring: profiles: active: prod # 多环境配置加载内容默认 spring: profiles: active:
4.5 实现动态即时刷新
(1) @RefreshScope
修改 cloud-provider-payment8001 模块下 controller包 中的 PayController类,添加 @RefreshScope 注解开启动态即时刷新。
@RefreshScope
@RestController
@Tag(name = "支付微服务模块", description = "支付CRUD")
public class PayController {
// ......
}
(2) 设置刷新间隔(可选)
修改 cloud-provider-payment8001 模块的 resources目录 中的 bootstrap.yml文件。
添加 spring.cloud.consul.config.watch.wait-time 属性以修改刷新时间(默认为55秒,建议不用修改)。
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: 1 # 默认为55秒
4.6 ☀️Consul配置数据持久化
(1) 注册为Windows服务
-
新建文件夹
consul_data存储Consul数据。例如:D:\path\to\consul_data -
新建
consul_start.bat脚本:@echo.服务启动...... @echo off @sc create Consul binpath= "D:\path\to\consul.exe agent -server -ui -bind=127.0.0.1 -client=0.0.0.0 -bootstrap-expect 1 -data-dir D:\path\to\consul_data " @net start Consul @sc config Consul start= AUTO @echo.Consul start is OK......success @pause -
右键管理员权限执行脚本。
-
使用开发者模式启动Consul。
consul agent -dev -
然后在浏览器中访问:http://localhost:8500
(2) MacOS配置
-
首先,创建一个目录来存储Consul的数据。
mkdir /path/to/consul-data -
创建Consul的配置文件,比如
consul-config.json,并指定使用本地文件系统作为后端存储。{ "data_dir": "/absolute/path/to/consul-data", "ui": true, "server": true, "bootstrap_expect": 1, "bind_addr": "127.0.0.1", "client_addr": "127.0.0.1", "retry_join": ["provider=virtualbox"] }"data_dir":指定了Consul数据存储的路径。"ui": true:启用Consul的Web UI(可选)。"server": true:将Consul配置为服务器节点。"bootstrap_expect": 1:期望的服务器节点数目。"bind_addr":绑定的IP地址。"client_addr":客户端访问地址。"retry_join": ["provider=virtualbox"]:用于指定Consul集群的节点信息。
-
使用指定的配置文件启动Consul服务。
consul agent -config-file=/absolute/path/to/consul-config.json -
然后在浏览器中访问:http://localhost:8500
四、LoadBalancer负载均衡服务调用
[!CAUTION]
LoadBalancer 已被 OpenFeign 集成,直接使用 OpenFeign 即可。
1. LoadBalancer简介
LB负载均衡(Load Balance)简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用),常见的负载均衡有软件Nginx,LVS,硬件 F5等
Spring Cloud LoadBalancer是由SpringCloud官方提供的一个开源的、简单易用的客户端负载均衡器,它包含在SpringCloud-commons中用它来替换了以前的Ribbon组件。相比较于Ribbon,SpringCloud LoadBalancer不仅能够支持RestTemplate,还支持WebClient(WebClient是Spring Web Flux中提供的功能,可以实现响应式异步请求)
官方文档:https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/loadbalancer.html
LoadBalancer本地负载均衡客户端 和 Nginx服务端负载均衡区别:
- Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的。
- loadbalancer本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
2. LoadBalancer原理

LoadBalancer 在工作时分成两步:
- 先选择ConsulServer从服务端查询并拉取服务列表,知道了它有多个服务(上图3个服务),这3个实现是完全一样的,默认轮询调用谁都可以正常执行。
- 按照指定的负载均衡策略从server取到的服务注册列表中由客户端自己选择一个地址,所以LoadBalancer是一个客户端的负载均衡器。
3. 🛠️新建服务模块实现LoadBalancer负载均衡
[!CAUTION]
LoadBalancer 已被 OpenFeign 集成,直接使用 OpenFeign 即可。
3.1 新建服务提供者8002模块
按照 cloud-provider-payment8001 模块,拷贝新建 cloud-provider-payment8002 模块。
(1) 建Module
(2) 改pom
按照 cloud-provider-payment8001 模块修改 cloud-provider-payment8002 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-provider-payment8002</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>
<!--Consul服务配置-->
<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>
<!-- Consul服务发现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<!-- 排除logging干扰(可选) -->
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.example.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>
<!--SpringBoot监控-->
<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 -->
<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
复制 cloud-provider-payment8001 模块 resources目录 中的内容到 cloud-provider-payment8002 模块的 resources目录 中。
然后修改 cloud-provider-payment8002 模块下 application.yml 文件中的端口号。
# ===================模块端口号===================
server:
port: 8002
# ......
[!NOTE]
cloud-provider-payment8002模块的 bootstrap.yml 文件和cloud-provider-payment8001模块完全相同即可,两者共用同一个微服务名。spring: application: name: cloud-payment-service
(4) 主启动
在 cloud-provider-payment8002 模块中修改(创建)Main8002类。
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.example.cloud.mapper") // tk.mybatis.spring.annotation.MapperScan
public class Main8002 {
public static void main(String[] args) {
SpringApplication.run(Main8002.class, args);
}
}
(5) 业务类
复制 cloud-provider-payment8001 模块 java包 中除 Main类 以外的所有内容到 cloud-provider-payment8002 模块的 java包 中。
3.2 修改服务消费者80模块
(1) 改pom
修改 cloud-consumer-order80 模块的 pom.xml 文件,添加LoadBalancer组件。
<!--LoadBalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
(2) 负载均衡测试
在 cloud-consumer-order80 模块下 controller包 中的 OrderController类 中添加负载均衡内容的测试代码。
[!IMPORTANT]
有两点前置操作:
- Controller 中的 url 是写服务名,而不是硬编码。具体见 [#part1三、3.2(4) 修改controller中的url](#(4) 修改controller中的url)。
- RestTemplateConfig 类中需要添加
@LoadBalanced注解使其支持负载均衡。具体见 [#Part1三、3.2(5) 修改RestTemplateConfig支持负载均衡](#(5) 修改RestTemplateConfig支持负载均衡)。
@RestController
public class OrderController {
public static final String PaymentSrv_URL = "http://cloud-payment-service";
@Resource
private RestTemplate restTemplate;
// ......
@GetMapping(value = "/consumer/pay/get/info")
public String getInfoByConsul() {
return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/info", String.class);
}
}
4. LoadBalancer负载均衡解析
4.1 DiscoveryClient
官网说明:https://docs.spring.io/spring-cloud-consul/reference/discovery.html#using-the-discoveryclient

4.2 代码示例
在 cloud-consumer-order80 模块下 controller包 中的 OrderController类 中添加 DiscoveryClient 的测试代码。
@RestController
public class OrderController {
// ......
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/discovery")
public String discovery() {
// 获取所有的服务名
List<String> services = discoveryClient.getServices();
for (String element : services) {
System.out.println(element);
}
System.out.println("===================================");
// 获取 cloud-payment-service 服务的所有实例
List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");
for (ServiceInstance element : instances) {
System.out.println(
"ServiceId = " + element.getServiceId() + "\t|\t"
+ "Host = " + element.getHost() + "\t|\t"
+ "Port = " + element.getPort() + "\t|\t"
+ "Uri = " + element.getUri()
);
}
// 浏览器中返回第0号实例
return instances.get(0).getServiceId() + ":" + instances.get(0).getPort();
}
}
先启动 Consul 服务器,然后启动 cloud-consumer-order80模块、cloud-provider-payment8001、cloud-provider-payment8002模块。
再在浏览器中访问:http://localhost:80/consumer/discovery
浏览器中显示的内容:
cloud-payment-service:8001
IDEA控制台中显示的内容:
cloud-consumer-order
cloud-payment-service
consul
===================================
ServiceId = cloud-payment-service | Host = 192.168.5.2 | Port = 8001 | Uri = http://192.168.5.2:8001
ServiceId = cloud-payment-service | Host = 192.168.5.2 | Port = 8002 | Uri = http://192.168.5.2:8002
4.3 负载选择原理
负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启动后rest接口计数从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
如此类推...
5. LoadBalancer负载均衡算法
5.1 默认算法
默认有两种负载均衡算法:
- 轮询算法
RoundRobinLoadBalancer - 随机算法
RandomLoadBalancer
5.2 算法切换
修改 cloud-consumer-order80 模块下 config包 中的 RestTemplateConfig类。
@Configuration
@LoadBalancerClient(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服务接口调用
1. OpenFeign简介
1.1 简介
Feign是一个声明性web服务客户端。它使编写web服务客户端变得更容易。使用Feign创建一个接口并对其进行注释。它具有可插入的注释支持,包括Feign注释和JAX-RS注释。Feign还支持可插拔编码器和解码器。Spring Cloud添加了对Spring MVC注释的支持,以及对使用Spring Web中默认使用的 HttpMessageConverter 的支持。Spring Cloud集成了Eureka、Spring Cloud CircuitBreaker以及Spring Cloud LoadBalancer,以便在使用Feign时提供负载平衡的http客户端。
官方文档:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/

1.2 作用
- 可插拔的注解支持,包括Feign注解和JAX-RS注解
- 支持可插拔的HTTP编码器和解码器
- 支持Sentinel和它的Fallback
- 支持SpringCloudLoadBalancer的负载均衡
- 支持HTTP请求和响应的压缩
前面在使用SpringCloud LoadBalancer + RestTemplate时,利用RestTemplate对http请求的封装处理形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,OpenFeign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。
在OpenFeign的实现下,我们只需创建一个接口并使用注解的方式来配置它(在一个微服务接口上面标注一个 @FeignClient 注解即可),即可完成对服务提供方的接口绑定,统一对外暴露可以被调用的接口方法,大大简化和降低了调用客户端的开发量,也即由服务提供者给出调用接口清单,消费者直接通过OpenFeign调用即可。
OpenFeign同时还集成 SpringCloud LoadBalancer,可以在使用OpenFeign时提供Http客户端的负载均衡,也可以集成阿里巴巴Sentinel来提供熔断、降级等功能。而与SpringCloud LoadBalancer不同的是,通过OpenFeign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。
2. 🛠️新建服务模块实现OpenFeign

2.1 新建服务消费者feign80模块
(1) 建Module
(2) 改pom
修改 cloud-consumer-feign-order80 模块的 pom.xml 文件,添加 OpenFeign 的依赖。
<!--OpenFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
完整的 pom.xml 内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-consumer-feign-order80</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--注册进入SpringCloud consul discovery-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.example.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>
<!--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 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3) 写yml
在 cloud-consumer-feign-order80 模块的 src/main/resources目录 中创建 application.yml 文件。
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
(4) 主启动|@EnableFeignClients
在 cloud-consumer-feign-order80 模块中修改(创建)MainOpenFeign80类。
添加 @EnableFeignClients 注解启用feign客户端,定义服务并绑定接口,以声明式的方法优雅而简单的实现服务调用。
@SpringBootApplication
@EnableDiscoveryClient // 该注解用于向使用consul为注册中心时注册服务
@EnableFeignClients // 启用feign客户端,定义服务并绑定接口,以声明式的方法优雅而简单的实现服务调用
public class MainOpenFeign80 {
public static void main(String[] args) {
SpringApplication.run(MainOpenFeign80.class, args);
}
}
2.2 修改公共API模块
(1) 改pom
修改 cloud-api-commons 模块的 pom.xml 文件,添加 OpenFeign 的依赖。
<!--OpenFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2) 创建Feign接口|@FeignClient
在 cloud-api-commons 模块中创建 apis包 中的 PayFeignApi接口,并添加 @FeignClient 注解标识Feign接口。
/**
* 对外暴露的客户端调用接口
*/
@FeignClient("cloud-payment-service") // Consul中注册的服务名
public interface PayFeignApi {
// ========== 对应服务提供者8001/8002模块的PayController中的CRUD方法 ==========
@PostMapping(value = "/pay/add")
ResultData<String> addPay(@RequestBody PayDTO payDTO);
@GetMapping(value = "/pay/del/{id}")
ResultData<Integer> deletePay(@PathVariable("id") Integer id);
@PutMapping(value = "/pay/update")
ResultData<String> updatePay(@RequestBody PayDTO payDTO);
@GetMapping(value = "/pay/get/{id}")
ResultData<PayDTO> getById(@PathVariable("id") Integer id);
@GetMapping(value = "/pay/get")
ResultData<List<PayDTO>> getAll();
// ========================= Feign的负载均衡测试方法 =========================
@GetMapping(value = "/pay/get/info")
String feignLoadBalancer();
}
2.3 业务类实现
在 cloud-consumer-feign-order80 模块中创建 controller包 中的 OrderController类。通过 PayFeignApi 接口调用服务提供者模块。
@RestController
public class OrderController {
@Resource
private PayFeignApi payFeignApi;
/**
* 增加订单
*/
@PostMapping("/feign/pay/add")
public ResultData<String> addOrder(@RequestBody PayDTO payDTO) {
// 添加支付记录
ResultData<String> resultData = payFeignApi.addPay(payDTO);
return resultData;
}
/**
* 删除订单
*/
@GetMapping(value = "/feign/pay/del/{id}")
public ResultData<Integer> deleteOrder(@PathVariable("id") Integer id) {
ResultData<Integer> resultData = payFeignApi.deletePay(id);
return resultData;
}
/**
* 修改订单
*/
@PutMapping(value = "/feign/pay/update")
public ResultData<String> updateOrder(@RequestBody PayDTO payDTO) {
ResultData<String> resultData = payFeignApi.updatePay(payDTO);
return resultData;
}
/**
* 通过id查询订单
*/
@GetMapping(value = "/feign/pay/get/{id}")
public ResultData<PayDTO> getById(@PathVariable("id") Integer id) {
ResultData<PayDTO> resultData = payFeignApi.getById(id);
return resultData;
}
/**
* 查询全部订单
*/
@GetMapping(value = "/feign/pay/get")
public ResultData<List<PayDTO>> getAll() {
ResultData<List<PayDTO>> resultData = payFeignApi.getAll();
return resultData;
}
/**
* Feign负载均衡
*/
@GetMapping("/feign/pay/lb")
public String feignLoadBalancer() {
return payFeignApi.feignLoadBalancer();
}
}
2.4 代码对应关系

3. OpenFeign高级特性
3.1 超时控制
官网说明:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#timeout-handling
[!NOTE]
默认OpenFeign客户端等待60秒钟,服务端处理超过规定时间会导致Feign客户端返回报错。
(1) 全局配置
修改服务消费者 cloud-consumer-feign-order80 模块的 application.yml 文件,添加超时控制。
spring:
cloud:
# openfeign配置
openfeign:
client:
config:
default:
# 连接超时时间(毫秒)
connect-timeout: 15000
# 读取超时时长(毫秒)
read-timeout: 15000
(2) 指定配置
指定针对某一个服务的超时时间设置。
修改服务消费者 cloud-consumer-feign-order80 模块的 application.yml 文件,添加超时控制。
spring:
cloud:
# openfeign配置
openfeign:
client:
config:
# 指定服务名
cloud-payment-service:
# 连接超时时间(毫秒)
connect-timeout: 30000
# 读取超时时长(毫秒)
read-timeout: 30000
[!NOTE]
全局配置
default和指定服务名配置的超时时间可以共存,但是指定服务名配置优先级更高。
3.2 重试机制
A bean of
Retryer.NEVER_RETRYwith the typeRetryeris created by default, which will disable retrying. Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions, treating them as transient network related exceptions, and any RetryableException thrown from an ErrorDecoder.【默认重试是关闭的】默认情况下会创建
Retryer.NEVER_RETRY类型为Retryer的 bean,这将禁用重试。请注意,这种重试行为与 Feign 默认行为不同,它会自动重试IOExceptions,将它们视为与网络相关的瞬态异常,以及从 ErrorDecoder 抛出的任何 RetryableException。
在 cloud-consumer-feign-order80 模块中创建 config包 中的 FeignConfig配置类。
@Configuration
public class FeignConfig {
@Bean
public Retryer myRetryer() {
// 最大请求次数为3(初始1次+重试2次),初始间隔时间为100ms,重试间最大间隔时间为1s
return new Retryer.Default(100, 1, 3);
// return Retryer.NEVER_RETRY; // Feign默认配置,不走重试策略
}
}
[!TIP]
OpenFeign 的重试机制可以和超时控制配合使用,当请求时长超过设定的超时时长后,就会触发重试机制进行重新请求。
3.3 🛠️HttpClient
OpenFeign中http client,如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求,由于默认HttpURLConnection没有连接池,性能和效率比较低。
需求:使用 Apache HttpClient5 替换 OpenFeign 默认的 HttpURLConnection。
[!IMPORTANT]
强烈建议替换!
(1) 改pom
修改 cloud-consumer-feign-order80 模块的 pom.xml 文件,添加 HC5 的依赖。
<!-- httpclient5 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>
(2) 改yml
修改 cloud-consumer-feign-order80 模块的 application.yml 文件,开启 HC5 配置。
# Apache HttpClient5 配置开启
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true
3.4 🛠️请求/响应压缩
-
对请求和响应进行GZIP压缩
Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。
通过下面的两个参数设置,就能开启请求与相应的压缩功能:# 开启请求压缩 spring.cloud.openfeign.compression.request.enabled=true # 开启响应压缩 spring.cloud.openfeign.compression.response.enabled=true -
细粒度化设置
对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限,只有超过这个大小的请求才会进行压缩:# 开启请求压缩 spring.cloud.openfeign.compression.request.enabled=true #触发压缩数据类型 spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #最小触发压缩的大小 spring.cloud.openfeign.compression.request.min-request-size=2048
修改 cloud-consumer-feign-order80 模块的 application.yml 文件,开启请求响应压缩配置。
spring:
cloud:
openfeign:
compression:
request:
enabled: true
min-request-size: 2048 #最小触发压缩的大小
mime-types: text/xml,application/xml,application/json #触发压缩数据类型
response:
enabled: true
3.5 日志打印
官网说明:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#feign-logging
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节,即对Feign接口的调用情况进行监控和输出。
日志级别:
NONE:默认的,不显示任何日志;BASIC:仅记录请求方法、URL、响应状态码及执行时间;HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
(1) 配置类
在 cloud-consumer-feign-order80 模块中创建 config包 中的 FeignConfig配置类。
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
(2) 改yml
修改 cloud-consumer-feign-order80 模块的 application.yml 文件,开启日志的Feign客户端。
配置格式:logging.level + 含有@FeignClient注解的完整带包名的接口名 + debug
[!IMPORTANT]
Feign的日志只响应
DEBUG级别。
# feign日志以什么级别监控哪个接口
logging:
level:
com:
example:
cloud:
apis:
PayFeignApi: debug
六、CircuitBreaker断路器
1. 概述
1.1 断路器概述
(1) 分布式系统面临的问题--服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
(2) 问题的解决
对有问题的节点,快速熔断(快速返回失败处理)或者服务降级(返回默认兜底数据)。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
(3) 断路器的作用
- 服务熔断:类比保险丝,保险丝闭合状态(
CLOSE)可以正常使用,当达到最大服务访问后,直接拒绝访问跳闸限电(OPEN),此刻调用方会接受服务降级的处理并返回友好兜底提示。 - 服务降级:不让客户端等待并立刻返回一个友好提示(
fallback),最经典的例子:弹窗提示“系统繁忙,请稍后再试”。 - 服务限流:对于高并发等操作,严禁一窝蜂地过来拥挤,大家排队,一秒钟N个,有序进行。
- 服务限时
- 服务预热
- 接近实时的监控
- 兜底的处理动作
- ......
1.2 CircuitBreaker熔断简介
(1) 简介
Spring Cloud CircuitBreaker 提供了跨不同断路器实现的抽象。它为您的应用程序提供了一致的API,允许开发人员选择最适合您应用程序需求的断路器实现。
CircuitBreaker 只是一套规范和接口,落地实现者是 Resilience4J。
CircuitBreaker官方文档:https://spring.io/projects/spring-cloud-circuitbreaker
(2) 实现原理
CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
(3) 三大状态的转换


1.3 Resilience4J简介
(1) 简介
Resilience4j是受到Netflix Hystrix的启发,为Java8和函数式编程所设计的轻量级容错框架。整个框架只是使用了Varr的库,不需要引入其他的外部依赖。与此相比,Netflix Hystrix对Archaius具有编译依赖,而Archaius需要更多的外部依赖,例如Guava和Apache Commons Configuration。
Resilience4j提供了提供了一组高阶函数(装饰器),包括断路器,限流器,重试机制,隔离机制。你可以使用其中的一个或多个装饰器对函数式接口,lambda表达式或方法引用进行装饰。这么做的优点是你可以选择所需要的装饰器进行装饰。
Resilience4J官方网站:https://resilience4j.readme.io/docs/getting-started
(2) 作用
- resilience4j-circuitbreaker: 熔断
- resilience4j-bulkhead: 隔离
- resilience4j-ratelimiter: 限流
- resilience4j-retry: 自动重试(同步,异步)
- resilience4j-cache: 结果缓存
- resilience4j-timelimiter: 超时处理
1.4 Bulkhead隔离简介
(1) 简介
Bulkhead:(船的)舱壁/(飞机的)隔板
隔板来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船。
官方文档:https://resilience4j.readme.io/docs/bulkhead
中文文档:https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/bulkhead.md
(2) 作用
依赖隔离+负载保护:用来限制对于下游服务的最大并发数量的限制。
Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量。
SemaphoreBulkhead使用了信号量。FixedThreadPoolBulkhead使用了有界队列和固定大小线程池。
1.5 RateLimiter限流简介
(1) 简介
限流就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。
所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。
官方文档:https://resilience4j.readme.io/docs/ratelimiter
中文文档:https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/ratelimiter.md
(2) 限流算法
① 漏斗算法(Leacky Bucket)
一个固定容量的漏桶,按照设定常量固定速率流出水滴。 如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。

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

② 令牌桶算法(Token Bucket)
SpringCloud默认使用该算法。

③ 滚动时间窗口(Tumbling time window)
允许固定数量的请求进入(比如1秒取4个数据相加,超过25值就超标)超过数量就拒绝或者排队,等下一个时间段进入。
由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。下图统计了3次。

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

④ 滑动时间窗口(Sliding time window)
顾名思义,该时间窗口是滑动的。所以,从概念上讲,这里有两个方面的概念需要理解:
- 窗口:需要定义窗口的大小
- 滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小
滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,
不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。下图统计了5次

2. 🛠️熔断实现(CircuitBreaker)
官方配置文档:https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker
| 常用配置属性 | 默认值 | 描述 |
|---|---|---|
| failure-rate-threshold | 50 | 以百分比配置失败率峰值 |
| sliding-window-type | COUNT_BASED | 断路器的滑动窗口期类型 可以基于“次数”(COUNT_BASED)或者“时间”(TIME_BASED)进行熔断,默认是COUNT_BASED。 |
| sliding-window-size | 100 | 滑动窗口的大小。例如若COUNT_BASED,则10次调用中有50%失败(即5次)打开熔断断路器;若为TIME_BASED则,此时还有额外的两个设置属性,含义为:在N秒内(sliding-window-size)100%(slow-call-rate-threshold)的请求超过N秒(slow-call-duration-threshold)打开断路器。 |
| slowCallRateThreshold | 100 | 以百分比的方式配置,断路器把调用时间大于slowCallDurationThreshold的调用视为慢调用,当慢调用比例大于等于峰值时,断路器开启,并进入服务降级。 |
| slowCallDurationThreshold | 60000(ms) | 配置调用时间的峰值,高于该峰值的视为慢调用。 |
| permitted-number-of-calls-in-half-open-state | 10 | 运行断路器在HALF_OPEN状态下时进行N次调用,如果故障或慢速调用仍然高于阈值,断路器再次进入打开状态。 |
| minimum-number-of-calls | 100 | 在每个滑动窗口期样本数,配置断路器计算错误率或者慢调用率的最小调用数。比如设置为5意味着,在计算故障率之前,必须至少调用5次。如果只记录了4次,即使4次都失败了,断路器也不会进入到打开状态。 |
| wait-duration-in-open-state | 60000(ms) | 从OPEN到HALF_OPEN状态需要等待的时间 |
2.0 案例准备
(1) 服务提供者模块新建测试controller
在 cloud-provider-payment8001 模块的 controller包 中创建 PayCircuitController类,模拟调用时的异常情况。
@RestController
public class PayCircuitController {
/**
* Resilience4j CircuitBreaker 测试例子
*/
@GetMapping(value = "/pay/circuit/{id}")
public String circuitTest(@PathVariable("id") Integer id) {
// 情况1:直接报错
if (id == -1) throw new RuntimeException("RuntimeException Error!");
// 情况2:超时(阻塞10秒,模拟慢调用)
if (id == -2) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 情况3:正确
return "Hello, circuit! inputId: " + id + " \t " + IdUtil.simpleUUID();
}
}
(2) 修改服务调用接口
修改 cloud-api-commons 模块下 apis包 中的 PayFeignApi类。
@FeignClient("cloud-payment-service")
public interface PayFeignApi {
// ......
// =============== Resilience4j CircuitBreaker 测试例子 ===============
@GetMapping(value = "/pay/circuit/{id}")
String circuitTest(@PathVariable("id") Integer id);
}
2.1 按照COUNT_BASED(基于次数的滑动窗口)【推荐】
(0) 案例需求说明
- 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
- 等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
- 如还是异常 CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
(1) 改pom
修改 cloud-consumer-feign-order80 模块的 pom.xml 文件,添加CircuitBreaker熔断器依赖。
<!--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>
(2) 改yml
修改 cloud-consumer-feign-order80 模块的 application.yml 文件,添加CircuitBreaker配置。
spring:
cloud:
openfeign:
# 开启circuitbreaker + 分组激活
circuitbreaker:
enabled: true
group:
# 默认不开启分组。若开启分组,默认的分组策略是精确优先。若设置了分组策略则按设置的来
enabled: true
# Resilience4j CircuitBreaker 按照COUNT_BASED(基于次数的滑动窗口)
resilience4j:
circuitbreaker:
configs:
# 更改默认的配置
default:
# 设置 50% 的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
failure-rate-threshold: 50
# 滑动窗口的类型
sliding-window-type: COUNT_BASED
# 滑动窗⼝的⼤⼩配置,COUNT_BASED下表示6个请求窗口
sliding-window-size: 6
# 断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
# 如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。
# 如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
# 一般 sliding-window-size 和 minimum-number-of-calls 设置相同的数值。
minimum-number-of-calls: 6
# 是否启用自动从开启状态过渡到半开状态,默认值为true。
# 如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常。
# 强烈建议设置为true。
automatic-transition-from-open-to-half-open-enabled: true
# 从OPEN到HALF_OPEN状态需要等待的时间
wait-duration-in-open-state: 5s
# 半开状态允许的最大请求数,默认值为10。
# 在半开状态下,CircuitBreaker将允许最多 permittedNumberOfCallsInHalfOpenState 个请求通过,
# 如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态(试探样本数)。
permitted-number-of-calls-in-half-open-state: 2
# 触发以下异常就会进入服务熔断
record-exceptions:
- java.lang.Exception
# 指定服务使用的配置
instances:
cloud-payment-service:
baseConfig: default
(3) 添加测试controller|@CircuitBreaker
在 cloud-consumer-feign-order80 模块的 controller包 中创建 OrderCircuitController类,配置使用熔断器。
@RestController
public class OrderCircuitController {
@Resource
private PayFeignApi payFeignApi;
/* @CircuitBreaker 注解添加熔断器
* name 和 application.yml 文件中 instances 属性配置的服务实例名一致
* fallbackMethod 指定服务降级的方法
*/
@CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "circuitFallback")
@GetMapping(value = "/feign/pay/circuit/{id}")
public String myCircuitBreaker(@PathVariable("id") Integer id) {
// 正常调用...
return payFeignApi.circuitTest(id);
}
/**
* 服务降级后的兜底处理方法
* @param id 和调用者保持相同参数(方法重载)(可以不写)
* @param t 抛出的异常
*/
public String circuitFallback(Integer id, Throwable t) {
// 这里是容错处理逻辑,返回备用结果...
return "系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
}
2.2 按照TIME_BASED(基于时间的滑动窗口)
基于时间的滑动窗口算法原理:
基于时间的滑动窗口是通过有N个桶的环形数组实现。
如果滑动窗口的大小为10秒,这个环形数组总是有10个桶,每个桶统计了在这一秒发生的所有调用的结果(部分统计结果),数组中的第一个桶存储了当前这一秒内的所有调用的结果,其他的桶存储了之前每秒调用的结果。
滑动窗口不会单独存储所有的调用结果,而是对每个桶内的统计结果和总的统计值进行增量的更新,当新的调用结果被记录时,总的统计值会进行增量更新。
检索快照(总的统计值)的时间复杂度为O(1),因为快照己经预先统计好了,并且和滑动窗口大小无关。
关于此方法实现的空间需求(内存消耗)约等于O(n)。由于每次调用结果(元组)不会被单独存储,只是对N个桶进行单独统计和一次总分的统计。
每个桶在进行部分统计时存在三个整型,为了计算,失败调用数、慢调用数、总调用数。还有一个long类型变量,存储所有调用的响应时间。
(1) 改pom
同上一节 [2.1按照COUNT_BASED(基于次数的滑动窗口)](#2.1 按照COUNT_BASED(基于次数的滑动窗口)【推荐】)。
(2) 改yml
修改 cloud-consumer-feign-order80 模块的 application.yml 文件,添加CircuitBreaker配置。
spring:
cloud:
openfeign:
# 开启circuitbreaker + 分组激活
circuitbreaker:
enabled: true
group:
# 默认不开启分组。若开启分组,默认的分组策略是精确优先。若设置了分组策略则按设置的来
enabled: true
# Resilience4j CircuitBreaker 按照TIME_BASED(基于时间的滑动窗口)
resilience4j:
timelimiter:
configs:
default:
# timelimiter默认限制远程1s,超于1s就超时异常,马上就会配置了降级,然后直接走降级逻辑
timeout-duration: 10s
circuitbreaker:
configs:
default:
# 设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
failure-rate-threshold: 50
# 慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例。
slow-call-duration-threshold: 5s
# 慢调用百分比峰值
# 断路器把调用时间⼤于slow-call-duration-threshold视为慢调用,
# 当慢调用次数的比例高于slow-call-rate-threshold阈值,断路器打开,并开启服务降级
slow-call-rate-threshold: 30
# 滑动窗口的类型
sliding-window-type: TIME_BASED
# 滑动窗口的大小配置,TIME_BASED下表示2秒
sliding-window-size: 2
# 断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
minimum-number-of-calls: 2
# 半开状态允许的最大请求数(试探样本数),默认值为10。
permitted-number-of-calls-in-half-open-state: 2
# 从OPEN到HALF_OPEN状态需要等待的时间
wait-duration-in-open-state: 5s
# 触发以下异常就会进入服务熔断
recordExceptions:
- java.lang.Exception
# 指定服务使用的配置
instances:
cloud-payment-service:
baseConfig: default
(3) 测试controller
同上一节 [2.1按照COUNT_BASED(基于次数的滑动窗口)](#2.1 按照COUNT_BASED(基于次数的滑动窗口)【推荐】)。
2.3 总结:断路器开启或闭合的条件

- 当满足一定的峰值和失败率达到一定条件后,断路器将会进入
OPEN状态,服务熔断。 - 当
OPEN的时候,所有请求都不会调用主业务逻辑方法,而是直接走FallBack兜底背锅方法,服务降级 - 一段时间之后,这个时候断路器会从
OPEN进入到HALF_OPEN半开状态,会放几个请求过去探探链路是否通?- 如成功,断路器会闭合
CLOSE。 - 如失败,继续开启。重复上述。
- 如成功,断路器会闭合
3. 🛠️隔离实现(Bulkhead)
3.0 案例准备
(1) 服务提供者模块添加测试controller
修改 cloud-provider-payment8001 模块下 controller包 中的 PayCircuitController类,添加 Bulkhead 的测试代码。
@RestController
public class PayCircuitController {
// ......
/**
* Resilience4j BulkHead 测试例子
*/
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id) {
// 情况1:直接报错
if (id == -1) throw new RuntimeException("RuntimeException Error!");
// 情况2:超时(阻塞10秒,模拟慢调用)
if (id == -2) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 情况3:正确
return "Hello, bulkhead! inputId: " + id + " \t " + IdUtil.simpleUUID();
}
}
(2) 修改服务调用接口
修改 cloud-api-commons 模块下 apis包 中的 PayFeignApi类。
@FeignClient("cloud-payment-service") // Consul中注册的服务名
public interface PayFeignApi {
// ......
// =============== Resilience4j Bulkhead 测试例子 ===============
@GetMapping(value = "/pay/bulkhead/{id}")
String myBulkhead(@PathVariable("id") Integer id);
}
3.1 实现SemaphoreBulkhead信号量隔离
信号量隔离(SemaphoreBulkhead)原理:
当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器。
- 如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。
- 若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。
(1) 改pom
修改 cloud-consumer-feign-order80 模块的 pom.xml 文件,添加Bulkhead依赖。
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
(2) 改yml
| 配置属性 | 默认值 | 描述 |
|---|---|---|
| maxConcurrentCalls | 25 | 隔离允许线程并发执行的最大数量。 |
| maxWaitDuration | 0 | 当达到并发调用数量时,新的线程执行时将被阻塞,这个属性表示最长的等待时间。 |
修改 cloud-consumer-feign-order80 模块的 application.yml 文件,添加Bulkhead信号量隔离配置。
spring:
cloud:
openfeign:
# 开启circuitbreaker + 分组激活
circuitbreaker:
enabled: true
group:
# 默认不开启分组。若开启分组,默认的分组策略是精确优先。若设置了分组策略则按设置的来
enabled: true
# Resilience4J Bulkhead -Semaphore
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 20s
bulkhead:
configs:
default:
# 隔离允许并发线程执行的最大数量
max-concurrent-calls: 2
# 当达到并发调用数量时,新的线程的阻塞时间(1s),如果超时直接走Fallback兜底方法
max-wait-duration: 1s
# 指定服务使用的配置
instances:
cloud-payment-service:
base-config: default
(3) 添加测试controller|@Bulkhead
修改 cloud-consumer-feign-order80 模块下 controller包 中的 OrderCircuitController类,添加Bulkhead-Samephore测试代码。
@RestController
public class OrderCircuitController {
@Resource
private PayFeignApi payFeignApi;
// ......
/* @Bulkhead 注解添加隔离
* name 和 application.yml 文件中 instances 属性配置的服务实例名一致
* fallbackMethod 指定服务降级的方法
* type 指定Bulkhead的方式(这里为信号量)
*/
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service", fallbackMethod = "samephoreFallback", type = Bulkhead.Type.SEMAPHORE)
public String myBulkhead(@PathVariable("id") Integer id) {
return payFeignApi.myBulkhead(id);
}
/**
* Bulkhead的服务降级后的兜底处理方法
*
* @param id 和调用者保持相同参数(方法重载)(可以不写)
* @param t 抛出的异常
*/
public String samephoreFallback(Integer id, Throwable t) {
return "系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
}
3.2 实现FixedThreadPoolBulkhead固定线程池隔离
JUC线程池原理:
固定线程池舱壁(FixedThreadPoolBulkhead)原理:
FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现隔离。
当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。
当线程池中无空闲时时,接下来的请求将进入等待队列。
- 若等待队列仍然无剩余空间时接下来的请求将直接被拒绝。
- 在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
[!NOTE]
ThreadPoolBulkhead只对
CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法。
(1) 改pom
同上一节 [3.1实现SemaphoreBulkhead信号量隔离](#3.1 实现SemaphoreBulkhead信号量隔离)。
(2) 改yml
| 配置属性 | 默认值 | 描述 |
|---|---|---|
| maxThreadPoolSize | Runtime.getRuntime() .availableProcessors() | 配置线程池的大小。 |
| coreThreadPoolSize | Runtime.getRuntime() .availableProcessors() - 1 | 配置核心线程池大小。 |
| queueCapacity | 100 | 配置队列的容量。 |
| keepAliveDuration | 20 [ms] | 当线程数大于核心数时,这是多余空闲线程在终止前等待新任务的最长时长。 |
修改 cloud-consumer-feign-order80 模块的 application.yml 文件,添加Bulkhead固定线程池隔离配置。
spring:
cloud:
openfeign:
# 开启circuitbreaker + 分组激活
circuitbreaker:
enabled: true
group:
# 设置为false,新启线程和原来主线程脱离
enabled: false
# Resilience4J bulkhead -THREADPOOL
resilience4j:
timelimiter:
configs:
default:
# timelimiter默认限制远程1s,超过报错不好演示效果所以改为10秒
timeout-duration: 10s
thread-pool-bulkhead:
configs:
default:
# 最多容纳7个线程(5个maxpool+2个阻塞队列)
core-thread-pool-size: 2
max-thread-pool-size: 5
queue-capacity: 2
# 指定服务使用的配置
instances:
cloud-payment-service:
baseConfig: default
(3) 添加测试controller
修改 cloud-consumer-feign-order80 模块下 controller包 中的 OrderCircuitController类,添加Bulkhead-ThreadPool测试代码。
[!NOTE]
注意返回值类型为
CompletableFuture。
@RestController
public class OrderCircuitController {
@Resource
private PayFeignApi payFeignApi;
/* @Bulkhead 注解添加隔离
* name 和 application.yml 文件中 instances 属性配置的服务实例名一致
* fallbackMethod 指定服务降级的方法
* type 指定Bulkhead的方式(这里为线程池)
*/
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service", fallbackMethod = "threadpoolFallback", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<String> myBulkheadTHREADPOOL(@PathVariable("id") Integer id) {
// 模拟处理内容
System.out.println(Thread.currentThread().getName() + "\t" + "开始进入!");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "准备退出!");
return CompletableFuture.supplyAsync(() -> payFeignApi.myBulkhead(id) );
}
/**
* Bulkhead的服务降级后的兜底处理方法
*
* @param id 和调用者保持相同参数(方法重载)(可以不写)
* @param t 抛出的异常
*/
public CompletableFuture<String> threadpoolFallback(Integer id, Throwable t) {
return CompletableFuture.supplyAsync(() -> "系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
}
}
4. 🛠️限流实现(RateLimiter)
4.1 案例准备
(1) 服务提供者模块添加测试controller
在 cloud-provider-payment8001 模块的 controller包 中创建 PayCircuitController类,模拟调用时的异常情况。
@RestController
public class PayCircuitController {
// ......
/**
* Resilience4j RateLimiter 测试例子
*/
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id) {
return "Hello, myRatelimit欢迎到来 inputId: " + id + " \t " + IdUtil.simpleUUID();
}
}
(2) 修改服务调用接口
修改 cloud-api-commons 模块下 apis包 中的 PayFeignApi类。
@FeignClient("cloud-payment-service")
public interface PayFeignApi {
// ......
// =============== Resilience4j Ratelimit 测试例子 ===============
@GetMapping(value = "/pay/ratelimit/{id}")
String myRatelimit(@PathVariable("id") Integer id);
}
4.2 实现RateLimiter限流
(1) 改pom
修改 cloud-consumer-feign-order80 模块的 pom.xml 文件,添加RateLimiter依赖。
<!--resilience4j-ratelimiter-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
(2) 改yml
| 属性 | 默认值 | 描述 |
|---|---|---|
| timeoutDuration | 5秒 | 线程等待权限的默认等待时间 |
| limitRefreshPeriod | 500纳秒 | 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod。 |
| limitForPeriod | 50 | 在一次刷新周期内,允许执行的最大请求数 |
修改 cloud-consumer-feign-order80 模块的 application.yml 文件,添加RateLimiter配置。
# resilience4j ratelimiter
resilience4j:
ratelimiter:
configs:
default:
# 在一次刷新周期内,允许执行的最大请求数
limit-for-period: 2
# 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
limit-refresh-period: 1s
# 线程等待权限的默认等待时间
timeout-duration: 1
# 指定服务使用的配置
instances:
cloud-payment-service:
base-config: default
(3) 添加测试controller|@RateLimiter
修改 cloud-consumer-feign-order80 模块下 controller包 中的 OrderCircuitController类,添加RateLimiter测试代码。
@RestController
public class OrderCircuitController {
@Resource
private PayFeignApi payFeignApi;
/* @RateLimiter 注解添加隔离
* name 和 application.yml 文件中 instances 属性配置的服务实例名一致
* fallbackMethod 指定服务降级的方法
*/
@GetMapping(value = "/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-payment-service", fallbackMethod = "ratelimitFallback")
public String myBulkhead(@PathVariable("id") Integer id) {
return payFeignApi.myRatelimit(id);
}
/**
* Bulkhead的服务降级后的兜底处理方法
*
* @param id 和调用者保持相同参数(方法重载)(可以不写)
* @param t 抛出的异常
*/
public String ratelimitFallback(Integer id, Throwable t) {
return "流量过大,禁止访问/(ㄒoㄒ)/~~";
}
}
七、MicrometerTracing+Zipkin分布式链路追踪
1. 概述
1.1 分布式链路追踪
在分布式与微服务场景下,我们需要解决如下问题:
- 如何实时观测系统的整体调用链路情况。
- 如何快速发现并定位到问题。
- 如何尽可能精确的判断故障对系统的影响范围与影响程度。
- 如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。
- 如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。
- 如何尽可能精确的分析系统的存储瓶颈与容量规划。
分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
1.2 MicrometerTracing简介
MicrometerTracing 提供了一套完整的分布式链路追踪解决方案且兼容支持了zipkin展现,替代了旧版本的 Sleuth。
官方文档:https://docs.micrometer.io/micrometer/reference/
1.3 分布式链路追踪原理
假定三个微服务调用的链路如下图所示:Service 1 调用 Service 2,Service 2 调用 Service 3 和 Service 4。
那么一条链路追踪会在每个服务调用的时候加上 Trace ID 和 Span ID。
- 链路通过TraceId唯一标识。【全局ID】
- Span标识发起的请求信息,各span通过 parent id 关联起来(Span表示调用链路来源,通俗的理解span就是一次请求信息)【每次请求标识的ID】

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

| 结点 | 描述 |
|---|---|
| 1 | 第一个节点:Span ID = A,Parent ID = null,Service 1 接收到请求。 |
| 2 | 第二个节点:Span ID = B,Parent ID= A,Service 1 发送请求到 Service 2 返回响应给Service 1 的过程。 |
| 3 | 第三个节点:Span ID = C,Parent ID= B,Service 2 的 中间解决过程。 |
| 4 | 第四个节点:Span ID = D,Parent ID= C,Service 2 发送请求到 Service 3 返回响应给Service 2 的过程。 |
| 5 | 第五个节点:Span ID = E,Parent ID= D,Service 3 的中间解决过程。 |
| 6 | 第六个节点:Span ID = F,Parent ID= C,Service 3 发送请求到 Service 4 返回响应给 Service 3 的过程。 |
| 7 | 第七个节点:Span ID = G,Parent ID= F,Service 4 的中间解决过程。 |
| 总结 | 通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。 |
1.4 Zipkin简介
(1) 简介
Zipkin是一种分布式链路跟踪系统的图形化工具,Zipkin 是 Twitter 开源的分布式跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调用链路信息展示到Web图形化界面上供开发人员分析,开发人员能够从Zipkin中分析出调用链路中的性能瓶颈,识别出存在问题的应用程序,进而定位问题和解决问题。
(2) 下载安装
官网下载:https://zipkin.io/pages/quickstart
下载安装后运行,再在浏览器中输入 http://localhost:9411/zipkin/ 查看控制台。
2. 🛠️分布式链路监控实现
2.1 修改根父工程的pom
由于Micrometer Tracing是一个门面工具自身并没有实现完整的链路追踪系统,具体的链路追踪另外需要引入的是第三方链路追踪系统的依赖:
| 次序 | 包名 | 描述 |
|---|---|---|
| 1 | micrometer-tracing-bom | 导入链路追踪版本中心,体系化说明,链路的数据收集。 |
| 2 | micrometer-tracing | 指标追踪,产生id。 |
| 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框架的一个模块用于监视和管理应用程序。 |
修改根项目父工程的 pom.xml 文件,添加上述1-6号依赖。
<properties>
<!-- micrometer-tracing -->
<micrometer-tracing.version>1.2.0</micrometer-tracing.version>
<micrometer-observation.version>1.12.0</micrometer-observation.version>
<feign-micrometer.version>12.5</feign-micrometer.version>
<zipkin-reporter-brave.version>2.17.0</zipkin-reporter-brave.version>
</properties>
<dependencyManagement>
<dependencies>
<!--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>
</dependencies>
</dependencyManagement>
2.2 修改服务提供者模块
(1) 改pom
修改 cloud-provider-payment8001 模块的 pom.xml 文件,添加上述2-6号依赖。
<!--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>
(2) 改yml
修改 cloud-provider-payment8001 模块的 application.yml 文件,添加链路追踪配置。
# ===================zipkin===================
management:
zipkin:
tracing:
# zipkin的地址(固定写法)
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
# 采样率默认为0.1(表示10次只能有一次被记录下来),值越大收集越及时。
probability: 1.0
(3) 服务提供者模块新建测试controller
在 cloud-provider-payment8001 模块的 controller包 中创建 PayMicrometerController类,模拟调用时的异常情况。
@RestController
public class PayMicrometerController {
/**
* Micrometer进行链路监控的测试例子
*/
@GetMapping(value = "/pay/micrometer/{id}")
public String myMicrometer(@PathVariable("id") Integer id) {
return "Hello, 欢迎到来myMicrometer inputId: " + id + " \t 服务返回:" + IdUtil.simpleUUID();
}
}
2.3 修改服务调用接口
修改 cloud-api-commons 模块下 apis包 中的 PayFeignApi类。
@FeignClient("cloud-payment-service")
public interface PayFeignApi {
// ......
/**
* Micrometer进行链路监控的测试例子
*/
@GetMapping(value = "/pay/micrometer/{id}")
String myMicrometer(@PathVariable("id") Integer id);
}
2.4 修改服务调用者模块
(1) 改pom
修改 cloud-consumer-feign-order80 模块的 pom.xml 文件,添加上述2-6号依赖。
<!--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>
(2) 改yml
修改 cloud-consumer-feign-order80 模块的 application.yml 文件,添加链路追踪配置。
# ===================zipkin===================
management:
zipkin:
tracing:
# zipkin的地址(固定写法)
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
# 采样率默认为0.1(表示10次只能有一次被记录下来),值越大收集越及时。
probability: 1.0
(3) 业务类实现
在 cloud-consumer-feign-order80 模块的 controller包 中创建 OrderMicrometerController类。
@RestController
public class OrderMicrometerController {
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/micrometer/{id}")
public String myMicrometer(@PathVariable("id") Integer id) {
return payFeignApi.myMicrometer(id);
}
}
八、Gateway网关
1. 概述
1.1 简介
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring6,Spring Boot 3和Project Reactor等技术。它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,并为它们提供跨领域的关注点,例如:安全性、监控/度量和恢复能力。
官方文档:https://docs.spring.io/spring-cloud-gateway/reference/
1.2 体系定位
Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关。但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关SpringCloud Gateway替代Zuul。

1.3 微服务架构中的网关定位

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

2. Gateway三大核心

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

客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。
- 在
pre类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。 - 在
post类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
4. 🛠️Gateway基础配置

4.1 建Module
4.2 改pom
修改 cloud-gateway9527 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloud-gateway9527</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>
<!--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>
</project>
4.3 写yml
在 cloud-gateway9527 模块的 src/main/resources目录 中创建 application.yml 文件。
server:
port: 9527
spring:
application:
# 以微服务注册进consul服务列表内
name: cloud-gateway
cloud:
# 配置consul
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
4.4 主启动
在 cloud-gateway9527 模块中修改(创建)Main9527类。
@SpringBootApplication
@EnableDiscoveryClient // 服务注册和发现
public class Main9527 {
public static void main(String[] args) {
SpringApplication.run(Main9527.class, args);
}
}
5. 🛠️网关路由映射实现
5.1 服务提供者模块新建测试controller
在 cloud-provider-payment8001 模块的 controller包 中创建 PayGateWayController类。
@RestController
public class PayGateWayController {
@Resource
PayService payService;
@GetMapping(value = "/pay/gateway/get/{id}")
public ResultData<Pay> getById(@PathVariable("id") Integer id) {
Pay pay = payService.getById(id);
return ResultData.success(pay);
}
@GetMapping(value = "/pay/gateway/info")
public ResultData<String> getGatewayInfo() {
return ResultData.success("gateway info test:" + IdUtil.simpleUUID());
}
}
5.2 网关yml配置
修改 cloud-gateway9527 模块的 application.yml 文件,添加网关相关配置。
spring:
cloud:
# 配置网关
gateway:
# 配置路由(对应服务提供者模块的controller)
routes:
- id: pay_routh1 # 路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 # 匹配后提供服务的路由地址(这里先写死,硬编码)
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 # 路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 # 匹配后提供服务的路由地址(这里先写死,硬编码)
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
5.3 修改服务调用接口
修改 cloud-api-commons 模块下 apis包 中的 PayFeignApi类。
[!CAUTION]
在服务调用接口 Api 中,如果该服务是对外暴露的,则需要将
@FeignClient注解写网关的服务名,而不是直接写后端的微服务名。如果仅仅只是内部服务调用,可以直接写微服务名。
网关的服务名是在网关模块(
cloud-gateway9527)的 application.yml 文件中定义的。
// @FeignClient("cloud-payment-service")
@FeignClient("cloud-gateway") // 修改为网关服务名
public interface PayFeignApi {
// ......
// ============================== Gateway 测试例子 ==============================
/**
* GateWay进行网关测试案例01
*/
@GetMapping(value = "/pay/gateway/get/{id}")
ResultData getGatewayById(@PathVariable("id") Integer id);
/**
* GateWay进行网关测试案例02
*/
@GetMapping(value = "/pay/gateway/info")
ResultData<String> getGatewayInfo();
}
5.4 服务消费者模块新建测试controller
在 cloud-consumer-feign-order80 模块的 controller包 中创建 OrderGateWayController类。
@RestController
public class OrderGateWayController {
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Integer id) {
return payFeignApi.getGatewayById(id);
}
@GetMapping(value = "/feign/pay/gateway/info")
public ResultData<String> getGatewayInfo() {
return payFeignApi.getGatewayInfo();
}
}
6. 🛠️核心之一:Route路由
该
ReactiveLoadBalancerClientFilter会查找交换属性ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR中的 URI。如果 URL 包含 lb 方案(例如 lb://myservice),则使用 Spring Cloud ReactorLoadBalancer 解析名称(例如 myservice),并将其转换为实际的主机和端口,并将相同的 URI 替换到该属性中。原始 URL 不作修改,并作为列表添加到交换属性ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR中。该过滤器还会检查交换属性ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR是否等于 lb。如果是,则适用相同的规则。下面的列表配置了ReactiveLoadBalancerClientFilter
修改 cloud-gateway9527 模块的 application.yml 文件,将硬编码写死的uri改为服务名调用。
spring:
cloud:
# 配置网关
gateway:
# 配置路由(对应服务提供者模块的controller)
routes:
- id: pay_routh1
# uri: http://localhost:8001
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/**
- id: pay_routh2
# uri: http://localhost:8001
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/**
7. 🛠️核心之二:Predicate断言
Predicate就是为了实现一组匹配规则,让请求过来找到对应的路由进行处理。
7.1 配置方式
有两种方法可以配置谓词和过滤器:
- 快捷方式(shortcuts)
- 完全展开参数(fully expanded arguments)
大多数都使用了快捷方式。名称和参数名称作为代码列在每个部分的第一个或两个句子中。参数通常按照快捷方式配置所需的顺序列出。
(1) 快捷方式配置【推荐】
快捷配置由过滤器名称识别,后面跟着等号(=),后面跟着以逗号(,)分隔的参数值。
例如:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Cookie=mycookie,mycookievalue
该例子用两个参数定义了Cookie路由断言工厂:Cookie名称、mycookie和与mycookievalue匹配的值。
(2) 完全展开参数配置
完全展开的参数看起来更像是带有name/value对的标准yaml配置。通常,会有一个name键和一个args键。args键是键值对的映射,用于配置断言或过滤器。
例如:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- name: Cookie
args:
name: mycookie
regexp: mycookievalue
该例子就是是上面显示的Cookie断言的快捷方式配置的完整配置。
7.2 常用断言API
(1) After
在指定时间之后才能请求。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- After=2023-11-20T17:38:13.586918800+08:00[Asia/Shanghai]
[!NOTE]
时间格式获取方式:
ZonedDateTime zdj = ZonedDateTime.now(); // 默认时区 System.out.println(zdj);
(2) Before
在指定时间之前才能请求。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- Before=2023-11-20T17:38:13.586918800+08:00[Asia/Shanghai]
(3) Between
在指定时间之间才能请求。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- Between=2023-11-20T17:38:13.586918800+08:00[Asia/Shanghai], 2024-11-20T17:38:13.586918800+08:00[Asia/Shanghai]
(4) Cookie
Cookie Route Predicate需要两个参数:一个是 Cookie name,一个是正则表达式。
路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
# username是cookie的键,zhangsan是键的值的正则表达式
- Cookie=username,zhangsan
测试方法:
方法1:原生命令
# 不带cookie访问: curl http://localhost:9527/pay/gateway/get/1 # 带cookie访问: curl http://localhost:9527/pay/gateway/get/1 --cookie "username=zhangsan"方法2:Postman
方法3:浏览器url访问
(5) Header
Header Route Predicate 需要两个参数:一个是属性名称和一个正则表达式。
当这个属性值和正则表达式匹配时才执行路由。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
# X-Request-Id是header的键,\d+是键的值的正则表达式(表示至少一位整数)
- Header=X-Request-Id, \d+
原生命令方式测试:
# 不带Header访问: curl http://localhost:9527/pay/gateway/get/1 # 带正确Header访问: curl http://localhost:9527/pay/gateway/get/1 -H "X-Request-Id:123" # 带错误Header访问: curl http://localhost:9527/pay/gateway/get/1 -H "X-Request-Id:abc"
(6) Host
Host Route Predicate 接收一组参数(一组匹配的域名列表)这个模板是一个ant分隔的模板,用.号作为分隔符。
它通过参数中的主机地址作为匹配规则。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- Host=**.baidu.com,**.bing.com
原生命令方式测试:
curl http://localhost:9527/pay/gateway/get/1 -H "Host:www.baidu.com"
(7) Method
Method Route Predicate Factory 接受一个或多个方法参数,该参数是要匹配的HTTP方法。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- Method=GET,POST
(8) Path
路径相匹配的进行路由。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
(9) Query
Query Route Predicate Factory 接受两个参数:一个请求中的param参数和一个可选的正则表达式。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- Query=uid, \d+ # 表示请求路径中要有参数名uid且值还要是整数
测试:
http://localhost:9527/pay/gateway/get/1?uid=123
(10) RemoteAddr
以 CIDR 格式限制访问ip。
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- RemoteAddr=192.168.1.1/24
测试:
http://192.168.1.2:9527/pay/gateway/get/1
7.3 自定义断言
(1) 自定义断言步骤
- 创建一个
XxxRoutePredicateFactory的类,该类必须以 RoutePredicateFactory 为后缀,而Xxx就是yml文件中断言配置的属性。 - 写一个
Config配置类,这个配置类就是我们的路由断言规则。 - 该类继承自
AbstractRoutePredicateFactory<Config>抽象工厂类。 - 重写
RoutePredicateFactory接口中的apply()方法(继承的AbstractRoutePredicateFactory已经实现了该接口)。 - 空参构造器,内部调用
super。
以 AfterRoutePredicateFactory 类的源码为例:
public class AfterRoutePredicateFactory extends AbstractRoutePredicateFactory<Config> {
public static final String DATETIME_KEY = "datetime";
public AfterRoutePredicateFactory() {
super(Config.class);
}
public List<String> shortcutFieldOrder() {
return Collections.singletonList("datetime");
}
public Predicate<ServerWebExchange> apply(final Config config) {
return new GatewayPredicate() {
public boolean test(ServerWebExchange serverWebExchange) {
ZonedDateTime now = ZonedDateTime.now();
return now.isAfter(config.getDatetime());
}
public Object getConfig() {
return config;
}
public String toString() {
return String.format("After: %s", config.getDatetime());
}
};
}
public static class Config {
private @NotNull ZonedDateTime datetime;
public Config() {
}
public ZonedDateTime getDatetime() {
return this.datetime;
}
public void setDatetime(ZonedDateTime datetime) {
this.datetime = datetime;
}
}
}
(2) 实现自定义断言
需求说明:自定义配置会员类型(vipType),按照 钻石/黄金/白银 和 yml文件中配置的会员类型,以断言是否可以访问。
在 cloud-gateway9527 模块中创建 gateway包 中的 MyRoutePredicateFactory类。
// 1. 继承AbstractRoutePredicateFactory抽象工厂类
@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config> {
// 2. 空参构造器
public MyRoutePredicateFactory() {
super(MyRoutePredicateFactory.Config.class);
}
// 3. 重写apply()方法
@Override
public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
// 获取请求中的vipType参数
String vipType = serverWebExchange
.getRequest()
.getQueryParams()
.getFirst("vipType");
// 参数为空
if (vipType == null || vipType.isBlank()) {
return false;
}
// 请求参数中的会员类型和yml配置的断言会员类型类型匹配
if (vipType.equalsIgnoreCase(config.getVipType())) {
return true;
}
return false;
}
};
}
// 4. 开启支持yml快捷方式配置断言(如果不开启,则在yml中需要使用完全展开参数方式配置断言)
public static final String VIPTYPE_KEY = "vipType";
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList(VIPTYPE_KEY);
}
// 5. 配置类
@Validated
public static class Config {
@Setter
@Getter
@NotEmpty
private String vipType; // 按照会员类型断言
}
}
修改 cloud-gateway9527 模块的 application.yml 文件,添加自定义断言。
# 快捷方式的配置(必须重写 shortcutFieldOrder() 方法)
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
# 自定义断言,钻石类型会员
- My=diamond
# 完全展开参数方式的配置
spring:
cloud:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
# 自定义断言,钻石类型会员
- name: My
args:
vipType: diamond
测试:
http://localhost:9527/pay/gateway/get/1?vipType=diamond
8. 🛠️核心之三:Filter过滤
8.1 Filter概述
(1) Filter处理流程
pre和post分别会在请求被执行前调用和被执行后调用,用来修改请求和响应信息。

(2) Filter的作用
- 请求鉴权
- 异常处理
- 记录接口调用时长统计
- ......
(3) Filter的类型
- 全局默认过滤器Global Filters
Gateway出厂默认已有的,直接用即可,主要作用于所有的路由。
不需要在配置文件中配置,作用在所有的路由上,实现GlobalFilter接口即可
官方文档:https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/global-filters.html - 单一内置过滤器GatewayFilter
也可以称为网关过滤器,这种过滤器主要是作用于单一路由或者某个路由分组。
官方文档:https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories.html - 自定义过滤器
8.2 Gateway内置的过滤器
即单一内置过滤器GatewayFilter。
以下是常用内置过滤器。
(1) 请求头相关(RequestHeader)
① AddRequestHeader
按照name指定默认请求头的内容。
-
修改
cloud-provider-payment8001模块下 controller包 中的 PayGateWayController类。@RestController public class PayGateWayController { // ...... @GetMapping(value = "/pay/gateway/filter") public ResultData<String> getGatewayFilter(HttpServletRequest request) { String result = ""; Enumeration<String> headers = request.getHeaderNames(); // 依次打印出请求头 while (headers.hasMoreElements()) { String headName = headers.nextElement(); String headValue = request.getHeader(headName); System.out.println("请求头名: " + headName + "\t\t\t" + "请求头值: " + headValue); if (headName.equalsIgnoreCase("X-Request-example1") || headName.equalsIgnoreCase("X-Request-example2")) { result = result + headName + "\t " + headValue + " "; } } return ResultData.success("getGatewayFilter 过滤器 test: " + result + " \t " + DateUtil.now()); } } -
修改
cloud-gateway9527模块的 application.yml 文件。spring: cloud: # 配置网关 gateway: # 配置路由(对应服务提供者模块的controller) routes: - id: pay_routh3 uri: lb://cloud-payment-service # 匹配后提供服务的路由地址 predicates: - Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由 filters: # 设置默认请求头kv,若含有多参则重写一行设置 - AddRequestHeader=X-Request-example1,value1 - AddRequestHeader=X-Request-example2,value2 -
测试:
http://localhost:9527/pay/gateway/filter控制台输出内容:
请求头名: sec-ch-ua 请求头值: "Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129" 请求头名: sec-ch-ua-mobile 请求头值: ?0 请求头名: sec-ch-ua-platform 请求头值: "macOS" 请求头名: upgrade-insecure-requests 请求头值: 1 请求头名: user-agent 请求头值: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 请求头名: accept 请求头值: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 请求头名: sec-fetch-site 请求头值: none 请求头名: sec-fetch-mode 请求头值: navigate 请求头名: sec-fetch-user 请求头值: ?1 请求头名: sec-fetch-dest 请求头值: document 请求头名: accept-encoding 请求头值: gzip, deflate, br, zstd 请求头名: accept-language 请求头值: zh-CN,zh;q=0.9,en;q=0.8 请求头名: cookie 请求头值: Idea-f13dca7=f1ba841e-70ef-44b6-a265-791496848c43; Webstorm-b71f26af=cdf915d0-5c53-42f9-a00a-b2b7aafae732 请求头名: x-request-example1 请求头值: value1 请求头名: x-request-example2 请求头值: value2 请求头名: forwarded 请求头值: proto=http;host="localhost:9527";for="[0:0:0:0:0:0:0:1]:57284" 请求头名: x-forwarded-for 请求头值: 0:0:0:0:0:0:0:1 请求头名: x-forwarded-proto 请求头值: http 请求头名: x-forwarded-port 请求头值: 9527 请求头名: x-forwarded-host 请求头值: localhost:9527 请求头名: host 请求头值: 192.168.5.2:8001 请求头名: content-length 请求头值: 0
② RemoveRequestHeader
按照name删除请求头的内容。
修改 cloud-gateway9527 模块的 application.yml 文件。
spring:
cloud:
# 配置网关
gateway:
# 配置路由(对应服务提供者模块的controller)
routes:
- id: pay_routh3
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由
filters:
- RemoveRequestHeader=sec-fetch-site # 删除请求头sec-fetch-site
③ SetRequestHeader
按照name修改请求头的内容。
修改 cloud-gateway9527 模块的 application.yml 文件。
spring:
cloud:
# 配置网关
gateway:
# 配置路由(对应服务提供者模块的controller)
routes:
- id: pay_routh3
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由
filters:
- SetRequestHeader=sec-fetch-mode, ABC # 将请求头sec-fetch-mode对应的值修改为ABC
(2) 请求参数相关(RequestParameter)
① AddRequestParameter
设置url中默认传递的请求参数。
-
修改
cloud-provider-payment8001模块下 controller包 中的 PayGateWayController类。@RestController public class PayGateWayController { // ...... @GetMapping(value = "/pay/gateway/filter") public ResultData<String> getGatewayFilter(HttpServletRequest request) { // 打印请求参数 System.out.println("============================================="); String customerId = request.getParameter("customerId"); System.out.println("request Parameter customerId: "+customerId); System.out.println("============================================="); return ResultData.success("getGatewayFilter 过滤器 test: " + result + " \t " + DateUtil.now()); } } -
修改
cloud-gateway9527模块的 application.yml 文件。spring: cloud: # 配置网关 gateway: # 配置路由(对应服务提供者模块的controller) routes: - id: pay_routh3 uri: lb://cloud-payment-service # 匹配后提供服务的路由地址 predicates: - Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由 filters: # 新增默认请求参数Param:k, v - AddRequestParameter=customerId,100001 -
测试和打印结果。
http://localhost:9527/pay/gateway/filter ============================================= request Parameter customerId: 100001 =============================================http://localhost:9527/pay/gateway/filter?customerId=9999 ============================================= request Parameter customerId: 9999 =============================================
② RemoveRequestParameter
设置url中默认传递的请求参数。
-
修改
cloud-provider-payment8001模块下 controller包 中的 PayGateWayController类。@RestController public class PayGateWayController { // ...... @GetMapping(value = "/pay/gateway/filter") public ResultData<String> getGatewayFilter(HttpServletRequest request) { // 打印请求参数 System.out.println("============================================="); String customerName = request.getParameter("customerName"); System.out.println("request Parameter customerName: " + customerName); System.out.println("============================================="); return ResultData.success("getGatewayFilter 过滤器 test: " + result + " \t " + DateUtil.now()); } } -
修改
cloud-gateway9527模块的 application.yml 文件。spring: cloud: # 配置网关 gateway: # 配置路由(对应服务提供者模块的controller) routes: - id: pay_routh3 uri: lb://cloud-payment-service # 匹配后提供服务的路由地址 predicates: - Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由 filters: # 删除url请求参数customerName,即使传递过来也是null - RemoveRequestParameter=customerName -
测试和打印结果。
http://localhost:9527/pay/gateway/filter ============================================= request Parameter customerName: null =============================================http://localhost:9527/pay/gateway/filter?customerName=zhangsan ============================================= request Parameter customerName: null =============================================
(3) 回应头相关(ResponseHeader)
① AddResponseHeader
指定回应头中的默认内容。
修改 cloud-gateway9527 模块的 application.yml 文件。
spring:
cloud:
# 配置网关
gateway:
# 配置路由(对应服务提供者模块的controller)
routes:
- id: pay_routh3
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由
filters:
# 新增默认回应参数X-Response-example并设值为ABCD
- AddResponseHeader=X-Response-example, ABCD
② SetResponseHeader
修改回应头中的内容。
修改 cloud-gateway9527 模块的 application.yml 文件。
spring:
cloud:
# 配置网关
gateway:
# 配置路由(对应服务提供者模块的controller)
routes:
- id: pay_routh3
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由
filters:
# 设置回应头Date值为2099-11-11
- SetResponseHeader=Date,2099-12-31
③ RemoveResponseHeader
删除回应头中的内容。
修改 cloud-gateway9527 模块的 application.yml 文件。
spring:
cloud:
# 配置网关
gateway:
# 配置路由(对应服务提供者模块的controller)
routes:
- id: pay_routh3
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由
filters:
# 将默认自带Date回应属性删除
- RemoveResponseHeader=Date
(4) 前缀和路径相关
① PrefixPath
自动添加路径前缀。
-
修改
cloud-gateway9527模块的 application.yml 文件。spring: cloud: gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: # 请求的路径 - Path=/gateway/filter/** filters: # 添加路径前缀(RealPath = PrefixPath + Path) - PrefixPath=/pay -
测试:
http://localhost:9527/gateway/filter
② SetPath
访问路径修改。
-
修改
cloud-gateway9527模块的 application.yml 文件。spring: cloud: gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: # 请求的路径,{segment}的内容最后被SetPath取代 - Path=/XYZ/abc/{segment} filters: # 设置路径,{segment}表示占位符 - SetPath=/pay/gateway/{segment} -
测试:
http://localhost:9527/XYZ/abc/filter
③ RedirectTo
重定向到某个路径。
修改 cloud-gateway9527 模块的 application.yml 文件。
spring:
cloud:
gateway:
routes:
- id: pay_routh3
uri: lb://cloud-payment-service
predicates:
# 请求的路径
- Path=/pay/gateway/filter/**
filters:
# 响应302,跳转到http://www.baidu.com/
- RedirectTo=302, http://www.baidu.com/
(5) 其他
① Default
配置默认filter,应用到所有路由,相当于全局配置。【不推荐】
修改 cloud-gateway9527 模块的 application.yml 文件。
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=...
- PrefixPath=...
- ...
8.3 Gateway自定义过滤器
(1) 自定义全局过滤器
-
在
cloud-gateway9527模块的 gateway包 中创建 MyGlobalFilter类。// 1. 实现 GlobalFilter 和 Ordered接口 @Component @Slf4j public class MyGlobalFilter implements GlobalFilter, Ordered { /** * 2. 自定义过滤器,实现运行时间统计,返回各个结果给后台 */ private static final String BEGIN_VISIT_TIME = "beginVisitTime"; // 开始访问时间的键 @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 记录下访问接口的开始时间(一个Map) 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("======================================================="); log.info("访问接口主机: " + exchange.getRequest().getURI().getHost()); log.info("访问接口端口: " + exchange.getRequest().getURI().getPort()); log.info("访问接口URL: " + exchange.getRequest().getURI().getPath()); log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery()); log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms"); log.info("======================================================="); } })); } /** * 3. 过滤器优先级(数字越小,优先级越高) */ @Override public int getOrder() { return -1; } } -
在
cloud-gateway9527模块的 application.yml 文件无需做任何配置,只需要配置服务路径和对应网关即可。例如使用之前配置的controller。
spring: cloud: gateway: routes: - id: pay_routh1 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/get/** -
测试:
http://localhost:9527/pay/gateway/info ======================================================= 访问接口主机: localhost 访问接口端口: 9527 访问接口URL: /pay/gateway/info 访问接口URL参数: null 访问接口时长: 133ms =======================================================
(2) 自定义条件过滤器
步骤类似[自定义断言](#7.3 自定义断言)的实现。
-
在
cloud-gateway9527模块的 gateway包 中创建 MyGatewayFilterFactory类。// 1. 继承AbstractGatewayFilterFactory抽象工厂类 @Component public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config> { // 2. 配置类 public static class Config { @Getter @Setter private String status; // 设定一个状态值(标志位),匹配正确才可以访问 } // 3. 构造器,内部调用super public MyGatewayFilterFactory() { super(MyGatewayFilterFactory.Config.class); } // 4. 重写apply()方法 @Override public GatewayFilter apply(MyGatewayFilterFactory.Config config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取请求 ServerHttpRequest request = exchange.getRequest(); System.out.println("进入了自定义网关过滤器,status:" + config.getStatus()); // yml中配置的status // 如果请求参数param中含有值叫example,则直接放行 if (request.getQueryParams().containsKey("example")) { return chain.filter(exchange); } // 否则回应非法请求 else { exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST); return exchange.getResponse().setComplete(); } } }; } // 5. 开启支持yml快捷方式配置断言(如果不开启,则在yml中需要使用完全展开参数方式配置断言) public static final String STATUS_KEY = "status"; @Override public List<String> shortcutFieldOrder() { return Arrays.asList(STATUS_KEY); } } -
修改
cloud-gateway9527模块的 application.yml 文件。spring: cloud: gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: # 自定义的过滤器(已在代码中设置了快捷方式配置) - My=example -
测试:
http://localhost:9527/pay/gateway/filter?example=AAA
#Part2 Spring Cloud Alibaba
一、Spring Cloud Alibaba简介
1. 官方网站
官方仓库:https://github.com/alibaba/spring-cloud-alibaba
Readme:https://github.com/alibaba/spring-cloud-alibaba/blob/2023.x/README-zh.md
官方文档(2023.0.x):https://sca.aliyun.com/docs/2023/overview/what-is-sca/
版本选择(2023.x):https://sca.aliyun.com/docs/2023/overview/version-explain/
电子书及原理:https://nacos.io/docs/ebook/kbyo6n/
2. 组件
Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。Nacos:个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。- RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。- Alibaba Cloud oss:阿里云对象存储服务 (Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- Alibaba cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精佳、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
- Alibaba Cloud SMS:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
3. 功能
- 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成对应 Spring Cloud 版本所支持的负载均衡组件的适配。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
二、Nacos服务注册和配置中心
1. Nacos简介
1.1 简介
Nacos 是 Dynamic Naming and Configuration Service的首字母简称。
一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
官方网站:https://nacos.io/
官方文档:https://nacos.io/docs/latest/what-is-nacos/
下载地址:https://nacos.io/download/nacos-server/
1.2 各种注册中心比较
| 服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
|---|---|---|---|
| Eureka | AP | 支持 | 低(2.x版本闭源) |
| Zookeeper | CP | 不支持 | 中 |
| Consul | CP | 支持 | 高 |
| Nacos | AP | 支持 | 高 |
1.3 Nacos启动
官网说明:https://nacos.io/docs/latest/quickstart/quick-start/
-
解压下载好的压缩包。
unzip nacos-server-$version.zip # 或者 tar -xvf nacos-server-$version.tar.gz -
进入解压后的 bin 目录。
cd ${nacos_home}/bin -
启动 Nacos 服务器。以单机模式运行(非集群模式):
注意是在 bin 目录中。
-
Mac/Linux
sh startup.sh -m standalone -
Windows
startup.cmd -m standalone
-
-
验证Nacos服务是否启动成功。
进入解压后的 logs 目录下,并查看日志。
cd ${nacos_home}/logs/ tail -f start.out如果看到如下日志,说明服务启动成功:
Nacos started successfully in stand alone mode. use embedded storage -
关闭 Nacos 服务器。
进入解压后的 bin 目录,再执行:
-
Mac/Linux
sh shutdown.sh -
Windows
shutdown.cmd
-
-
在浏览器中访问 Nacos 控制台:
http://localhost:8848/nacos
2. 🛠️Nacos Discovery服务注册中心
2.1 基于Nacos的服务提供者9001模块
(1) 建Module
(2) 改pom
修改 cloudalibaba-provider-payment9001 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloudalibaba-provider-payment9001</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.example.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3) 写yml
在 cloudalibaba-provider-payment9001 模块的 src/main/resources目录 中创建 application.yml 文件。
server:
port: 9001
spring:
application:
# 入住nacos中的服务名
name: nacos-payment-provider
cloud:
nacos:
discovery:
# 配置Nacos地址
server-addr: localhost:8848
(4) 主启动
在 cloudalibaba-provider-payment9001 模块中修改(创建)Main9001类。
@SpringBootApplication
@EnableDiscoveryClient
public class Main9001 {
public static void main(String[] args) {
SpringApplication.run(Main9001.class, args);
}
}
(5) 业务类
在 cloudalibaba-provider-payment9001 模块的 controller包 中创建 PayAlibabaController类。
@SpringBootApplication
@EnableDiscoveryClient
public class Main9001 {
public static void main(String[] args) {
SpringApplication.run(Main9001.class, args);
}
}
(6) 测试
启动Nacos服务,在浏览器中输入 http://localhost:8848/nacos/。

2.2 基于Nacos的服务消费者83模块
[!WARNING]
本节内容是基于 LoadBalancer + RestTemplate 实现的,若要通过 OpenFeign 实现微服务接口调用,具体见 [#Part2三、5.OpenFeign和Sentinel集成实现服务降级](#5. 🛠️OpenFeign和Sentinel集成实现服务降级)。
(1) 建Module
(2) 改pom
修改 cloudalibaba-consumer-nacos-order83 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloudalibaba-consumer-nacos-order83</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3) 写yml
在 cloudalibaba-consumer-nacos-order83 模块的 src/main/resources目录 中创建 application.yml 文件。
server:
port: 83
spring:
application:
# 本服务入住nacos中的服务名
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 消费者将要去访问的微服务名称(nacos中注册的微服务提供者名)
service-url:
nacos-user-service: http://nacos-payment-provider
(4) 主启动
在 cloudalibaba-consumer-nacos-order83 模块中修改(创建)Main83类。
@EnableDiscoveryClient
@SpringBootApplication
public class Main83 {
public static void main(String[] args) {
SpringApplication.run(Main83.class, args);
}
}
(5) 业务类
[!WARNING]
本节内容是基于 LoadBalancer + RestTemplate 实现的,若要通过 OpenFeign 实现微服务接口调用,具体见 [#Part2三、5.3(3)修改微服务消费者83模块](#(3) 修改微服务消费者83模块) OpenFeign 和 Sentinel 整合。
① 配置类
在 cloudalibaba-consumer-nacos-order83 模块中创建 config包 中的 RestTemplateConfig类。
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced //赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
② controller
在 cloudalibaba-consumer-nacos-order83 模块中创建 controller包 中的 OrderNacosController类。
@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调用者。。。。。。";
}
}
2.3 负载均衡
(1) 虚拟端口映射9001模块建立9002模块
可以新建 cloudalibaba-provider-payment9002 模块,但这里使用取巧的方式虚拟端口映射新建9002模块。
(2) 测试
-
启动
Main83、Main9001、Main9002。
-
在 Nacos 中查询服务: http://localhost:8848/nacos

-
在浏览器中输入服务url:
http://localhost:83/consumer/pay/nacos/1观察到输出的端口号9001/9002交替出现,负载均衡实现。
3. 🛠️Nacos Config服务配置中心
3.1 新建服务配置3377模块
(1) 建Module
(2) 改pom
修改 cloudalibaba-config-nacos-client3377 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloudalibaba-config-nacos-client3377</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>
<!--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>
</project>
(3) 写yml
① bootstrap.yml
在 cloudalibaba-config-nacos-client3377 模块的 src/main/resources目录 中创建 bootstrap.yml 文件。
# nacos配置
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos服务注册中心地址
config:
server-addr: localhost:8848 # Nacos作为配置中心地址
file-extension: yaml # 指定yaml格式的配置
② application.yml
在 cloudalibaba-config-nacos-client3377 模块的 src/main/resources目录 中创建 application.yml 文件。
server:
port: 3377
spring:
profiles:
active: dev # 表示开发环境
# active: prod # 表示生产环境
# active: test # 表示测试环境
(4) 主启动
在 cloudalibaba-config-nacos-client3377 模块中修改(创建)Main3377类。
@EnableDiscoveryClient
@SpringBootApplication
public class Main3377 {
public static void main(String[] args) {
SpringApplication.run(Main3377.class, args);
}
}
(5) 业务类
在 cloudalibaba-config-nacos-client3377 模块的 controller包 中创建 NacosConfigClientController类。
@RestController
@RefreshScope // 在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
public class NacosConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
3.2 在Nacos中添加配置信息
(1) Nacos中的匹配规则
Nacos 端配置文件 DataID 的命名规则是:
${prefix}-${spring.profiles.active}.${file-extension}
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
prefix默认为spring.application.name的值,也可以通过配置项spring.cloud.nacos.config.prefix来配置。spring.profiles.active即为当前环境对应的 profile。
注意:当spring.profiles.active为空时,对应的连接符-也将不存在,DataID 的拼接格式变成${prefix}.${file-extension}file-exetension为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension来配置。目前只支持properties和yaml类型。

(2) 添加配置信息


4. Nacos数据模型之Namespace-Group-DataID
1.1 Nacos数据模型


- 是什么
类似Java里面的package名和类名,最外层的Namespace是可以用于区分部署环境的,Group和DataID逻辑上区分两个目标对象。 - 默认值
- 默认情况:
Namespace=public,Group=DEFAULT_GROUP - Nacos默认的命名空间是public,Namespace主要用来实现隔离。比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去。
- 默认情况:
Service就是微服务
一个Service可以包含一个或者多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。
1.2 三种方案加载配置
(1) DataID方案
指定spring.profile.active和配置文件的DataID来使不同环境下读取不同的配置。
-
默认命名空间public + 默认分组DEFAULT_GROUP + 新建test配置的DataID。

-
修改
cloudalibaba-config-nacos-client3377模块的 application.yml 文件,将环境修改为test测试环境。server: port: 3377 spring: profiles: # active: dev # 表示开发环境 # active: prod # 表示生产环境 active: test # 表示测试环境 -
测试:
http://localhost:3377/config/info
(2) Group方案
通过 Group 实现环境区分。
-
默认命名空间public + 新建PROD_GROUP + 新建prod配置的DataID。

-
修改
cloudalibaba-config-nacos-client3377模块的 bootstrap.yml 文件,添加一条group的配置。# nacos配置 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 config: server-addr: localhost:8848 file-extension: yaml group: PROD_GROUP # 指定group -
修改
cloudalibaba-config-nacos-client3377模块的 application.yml 文件,将环境修改为prod生产环境。server: port: 3377 spring: profiles: # active: dev # 表示开发环境 active: prod # 表示生产环境 # active: test # 表示测试环境 -
测试:
http://localhost:3377/config/info
(3) Namespace方案
-
通过Namespace实现命名空间环境区分。

-
新建命名空间Prod_Namespace + 新建PROD_GROUP + 新建prod配置的DataID。


-
修改
cloudalibaba-config-nacos-client3377模块的 bootstrap.yml 文件,添加group的配置和namespace的配置。# nacos配置 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 config: server-addr: localhost:8848 file-extension: yaml group: PROD_GROUP # 指定group namespace: Prod_Namespace # 指定Namespace的ID -
修改
cloudalibaba-config-nacos-client3377模块的 application.yml 文件,将环境修改为prod生产环境。server: port: 3377 spring: profiles: # active: dev # 表示开发环境 active: prod # 表示生产环境 # active: test # 表示测试环境 -
测试:
http://localhost:3377/config/info
三、Sentinel服务熔断与限流
1. Sentinel简介
1.1 简介
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
官方网站:https://sentinelguard.io/zh-cn/
官方文档:https://sentinelguard.io/zh-cn/docs/introduction.html
下载地址:https://github.com/alibaba/Sentinel/releases
1.2 特性
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
1.3 作用
-
流量控制
-
熔断降级
-
系统负载保护
1.4 流控降级与容错标准

1.5 微服务相关概念
(1) 服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
(2) 服务降级
服务降级,说白了就是一种服务兜底方案,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。
例如,在商品详情页一般都会展示商品的介绍信息,一旦商品详情页系统出现故障无法调用时,会直接获取缓存中的商品介绍信息返回给前端页面。
(3) 服务熔断
在分布式与微服务系统中,如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会暂时断开与下游服务的调用连接。这种方式就是熔断。类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
服务熔断一般情况下会有三种状态:闭合、开启和半熔断。
- 闭合状态(保险丝闭合通电OK):服务一切正常,没有故障时,上游服务调用下游服务时,不会有任何限制。
- 开启状态(保险丝断开通电Error):上游服务不再调用下游服务的接口,会直接返回上游服务中预定的方法。
- 半熔断状态:处于开启状态时,上游服务会根据一定的规则,尝试恢复对下游服务的调用。此时,上游服务会以有限的流量来调用下游服务,同时,会监控调用的成功率。如果成功率达到预期,则进入关闭状态。如果未达到预期,会重新进入开启状态。
(4) 服务限流
服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量失效),造成不可用;还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如令牌桶算法和漏牌桶算法就是时间窗口的限流算法。
(5) 服务隔离
有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。
互联网行业常用的服务隔离方式有:线程池隔离和信号量隔离。
(6) 服务超时
整个系统采用分布式和微服务架构后,系统被拆分成一个个小服务,就会存在服务与服务之间互相调用的现象,从而形成一个个调用链。
形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。服务超时就是在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源。
2. Sentinel下载安装
Sentinel组件由2部分构成:
- 后台8719默认。
- 前台8080开启。
下载地址:https://github.com/alibaba/Sentinel/releases
-
下载jar包。
-
运行jar包:
java -jar sentinel-dashboard-1.8.8.jar -
反问sentinel管理界面,默认用户名和密码都是
sentinel。http://localhost:8080
3. 🛠️实现Sentinel整合
3.1 新建微服务8401模块
(1) 建Module
(2) 改pom
修改 cloudalibaba-sentinel-service8401 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloudalibaba-sentinel-service8401</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.example.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3) 写yml
在 cloudalibaba-sentinel-service8401 模块的 src/main/resources目录 中创建 application.yml 文件。
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 # 配置Sentinel前台dashboard控制台服务地址
port: 8719 # Sentinel后台默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
(4) 主启动
在 cloudalibaba-sentinel-service8401 模块中修改(创建)Main8401类。
@EnableDiscoveryClient
@SpringBootApplication
public class Main8401 {
public static void main(String[] args) {
SpringApplication.run(Main8401.class, args);
}
}
(5) 业务类
在 cloudalibaba-sentinel-service8401 模块的 controller包 中创建 FlowLimitController类。
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
3.2 测试
依次启动:
- Nacos8848
- Sentinel8080
- 微服务8401
Sentinel采用的懒加载说明:想使用Sentinel对某个接口进行限流和降级等操作,一定要先访问下接口,使Sentinel检测出相应的接口。
执行一次访问即可在Sentinel中查看情况:
http://localhost:8401/testA
http://localhost:8401/testB

4. 🛠️Sentinel规则
4.1 流控规则
(1) 基本介绍

| 选项 | 描述 |
|---|---|
| 资源名 | 资源的唯一名称,默认就是请求的接口路径,可以自行修改,但是要保证唯一。 |
| 针对来源 | 具体针对某个微服务进行限流,默认值为default,表示不区分来源,全部限流。 |
| 阈值类型 | QPS表示每秒查询,通过QPS进行限流,并发线程数表示通过并发线程数限流。 |
| 单机阈值 | 与阈值类型组合使用。如果阈值类型选择的是QPS,表示当调用接口的QPS达到阈值时,进行限流操作。如果阈值类型选择的是并发线程数,则表示当调用接口的并发线程数达到阈值时,进行限流操作。 |
| 是否集群 | 选中则表示集群环境,不选中则表示非集群环境。 |
(2) 流控模式
① 直接
默认的流控模式,当接口达到限流条件(阈值)时,直接开启限流功能。
② 关联
当关联的资源达到阈值时,就限流自己。当与A关联的资源B达到阀值后,就限流A自己。
③ 链路
来自不同链路的请求对同一个目标访问时,实施针对性的不同限流措施。比如C请求来访问就限流,D请求来访问就是OK。
-
新增service和controller,添加C和D请求。
-
在
cloudalibaba-sentinel-service8401模块中创建 service包 中的 FlowLimitService类。@Service public class FlowLimitService { @SentinelResource(value = "common") public void common() { System.out.println("------FlowLimitService come in"); } } -
修改
cloudalibaba-sentinel-service8401模块下 controller包 中的 FlowLimitController类,添加/testC和/testD访问路径。@RestController public class FlowLimitController { @GetMapping("/testA") public String testA() { return "------testA"; } @GetMapping("/testB") public String testB() { return "------testB"; } // 流控-链路演示例子: // C和D两个请求都访问FlowLimitService中的common()方法,阈值到达后对C限流,对D不管。 @Resource private FlowLimitService flowLimitService; @GetMapping("/testC") public String testC() { flowLimitService.common(); return "------testC"; } @GetMapping("/testD") public String testD() { flowLimitService.common(); return "------testD"; } } -
修改
cloudalibaba-sentinel-service8401模块的 application.yml 文件,添加上下文链路配置。
web-context-unify属性默认为 true,表示上下午调用属于同一个链路。spring: cloud: sentinel: # 设置为false,表示controller层的方法对service层调用不认为是同一个根链路 web-context-unify: false
-
-
Sentinel配置。
(3) 流控效果--QPS
① 快速失败
默认的流控处理,直接失败,抛出异常。
该方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
② WarmUp
WarmUp(冷启动、预热模式),该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过“冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。
应用场景如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
冷启动的公式:
冷却因子coldFactor默认值为3,设定阈值threshold和预热时长time(秒)。
初始的阈值为threshold / coldFactor,经过time秒内逐渐将阈值升到threshold。
③ 排队等待
排队等待(匀速器方式),这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
4.2 熔断规则
(1) 基本介绍
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,
让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

(2) 熔断策略
① 慢调用比例
慢调用比例:选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
进入熔断状态判断依据:在统计时长内,实际请求数目>设定的最小请求数 且 实际慢调用比例>比例阈值,进入熔断状态。
- 调用:一个请求发送到服务器,服务器给与响应,一个响应就是一个调用。
- 最大RT:即最大的响应时间,指系统对请求作出响应的业务处理时间。
- 慢调用:处理业务逻辑的实际时间>设置的最大RT时间,这个调用叫做慢调用。
- 慢调用比例:在所以调用中,慢调用占有实际的比例=慢调用次数/总调用次数。
- 比例阈值:自己设定的,比例阈值=慢调用次数/调用次数。
- 统计时长:时间的判断依据。
- 最小请求数:设置的调用最小请求数,上图比如1秒钟打进来10个线程(大于我们配置的5个了)调用被触发。
熔断的状态:
- 熔断状态(保险丝跳闸断电,不可访问):在接下来的熔断时长内请求会自动被熔断
- 探测恢复状态(探路先锋):熔断时长结束后进入探测恢复状态
- 结束熔断(保险丝闭合恢复,可以访问):在探测恢复状态,如果接下来的一个请求响应时间小于设置的慢调用 RT,则结束熔断,否则继续熔断。
② 异常比例
异常比例:当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
③ 异常数
异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
4.* @SentinelResource注解
@SentineiResource是一个流量防卫防护组件注解,用于指定防护资源,对配置的资源进行流量控制、熔断降级等功能。
/**
* @SentinelResource注解源码说明
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {
// 资源名称
String value() default "";
// entry类型,标记流量的方向,取值IN/OUT,默认是OUT
EntryType entryType() default EntryType.OUT;
// 资源分类
int resourceType() default 0;
// 处理BlockException的函数名称,函数要求:
// 1. 必须是 public
// 2. 返回类型、参数与原方法一致
// 3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置blockHandlerClass ,并指定blockHandlerClass里面的方法。
String blockHandler() default "";
// 存放blockHandler的类,对应的处理函数必须static修饰。
Class<?>[] blockHandlerClass() default {};
// 用于在抛出异常的时候提供fallback处理逻辑。 fallback函数可以针对所
// 有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求:
// 1. 返回类型与原方法一致
// 2. 参数类型需要和原方法相匹配
// 3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定fallbackClass里面的方法。
String fallback() default "";
// 存放fallback的类。对应的处理函数必须static修饰。
String defaultFallback() default "";
// 用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进
// 行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:
// 1. 返回类型与原方法一致
// 2. 方法参数列表为空,或者有一个 Throwable 类型的参数。
// 3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定 fallbackClass 里面的方法。
Class<?>[] fallbackClass() default {};
// 需要trace的异常
Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};
// 指定排除忽略掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。
Class<? extends Throwable>[] exceptionsToIgnore() default {};
}
(1) 按照rest地址限流+默认限流返回
通过访问的rest地址来限流,会返回Sentinel自带默认的限流处理信息。
[!NOTE]
该方式为不使用
@SentinelResource注解实现 Sentinel 的默认控制。
-
在
cloudalibaba-sentinel-service8401模块的 controller包 中创建 RateLimitController类。@RestController public class RateLimitController { /** * 按rest地址限流测试 */ @GetMapping("/rateLimit/byUrl") public String byUrl() { return "按rest地址限流测试OK"; } } -
在Sentinel中配置流控规则。
-
在浏览器中访问测试:
http://localhost:8401/rateLimit/byUrl -
被限流后浏览器中显示:
Blocked by Sentinel (flow limiting)
(2) 按资源名称限流+自定义限流返回
使用 @SentinelResource 注解可以实现返回自定义的限流提示。
-
修改
cloudalibaba-sentinel-service8401模块下 controller包 中的 RateLimitController类。@RestController public class RateLimitController { // ...... /** * 按@SentinelResource资源名称限流测试 */ // value设定资源名称,blockHandler指定限流时触发的方法 @SentinelResource(value = "byResourceSentinelResource", blockHandler = "handleException") @GetMapping("/rateLimit/byResource") public String byResource() { return "按资源名称SentinelResource限流测试OK"; } /** * 限流时触发的方法 * 如果byResource()方法中有形参,则该方法中也要对应加上 */ public String handleException(BlockException exception) { return "服务被限流o(╥﹏╥)o"; } } -
在Sentinel中配置流控规则。
-
在浏览器中访问测试:
http://localhost:8401/rateLimit/byResource -
被限流后浏览器中显示:
服务被限流o(╥﹏╥)o -
对应关系:
(3) 按资源名称限流+自定义限流返回+服务降级处理
使用 @SentinelResource 注解可以实现返回自定义的限流提示,同时实现程序异常返回fallback服务降级。
-
修改
cloudalibaba-sentinel-service8401模块下 controller包 中的 RateLimitController类。@RestController public class RateLimitController { // ...... /** 按资源名称限流+自定义限流返回+服务降级处理测试 */ @GetMapping("/rateLimit/doAction/{p1}") @SentinelResource( value = "doActionSentinelResource", // sentinel资源名 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) { return "sentinel配置自定义限流了"; } /** 发生异常时的服务降级兜底方法 */ public String doActionFallback(@PathVariable("p1") Integer p1, Throwable e) { return "程序逻辑异常了:" + "\t" + e.getMessage(); } }blockHandler:主要针对sentinel配置后出现的违规情况处理。fallback:程序异常了JVM抛出的异常服务降级。- 两者可以共存。
-
在Sentinel中配置流控规则。
-
在浏览器中正常访问限流测试:
http://localhost:8401/rateLimit/doAction/1浏览器中返回:
sentinel配置自定义限流了 -
在浏览器中异常访问测试:
http://localhost:8401/rateLimit/doAction/0浏览器中返回:
程序逻辑异常了: p1等于零直接异常 -
对应关系:
4.3 热点规则
(1) 基本介绍
热点,即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制


(2) 配置实现
-
需求说明:方法
testHotKey()里面第一个参数p1只要QPS超过每秒1次,马上降级处理。 -
修改
cloudalibaba-sentinel-service8401模块下 controller包 中的 RateLimitController类。@RestController public class RateLimitController { // ...... /** 热点规则测试 */ @GetMapping("/testHotKey") @SentinelResource(value = "testHotKey", blockHandler = "dealHandler_testHotKey") public String testHotKey( @RequestParam(value = "p1", required = false) String p1, @RequestParam(value = "p2", required = false) String p2 ) { return "------testHotKey"; } /** 热点规则测试限流时触发的方法 */ public String dealHandler_testHotKey(String p1, String p2, BlockException exception) { return "-----dealHandler_testHotKey"; } } -
在Sentinel中配置热点规则。
-
含参数
p1限流测试:http://localhost:8401/testHotKey?p1=xxx浏览器中返回:
-----dealHandler_testHotKey -
对应关系:
(3) 参数例外项
当热点限流的参数是某个约定的特殊值时,该参数的限流规则就失效。
注意:配置参数例外项的参数必须是基本类型或者String。
-
需求说明:方法
testHotKey()里面第一个参数p1只要QPS超过每秒1次,马上降级处理。但是当p1=5时,它的阈值可以达到200。 -
在Sentinel中配置热点规则:
4.4 授权规则
(1) 基本介绍
在某些场景下,需要根据调用接口的来源判断是否允许执行本次请求。此时就可以使用Sentinel提供的授权规则来实现,Sentinel的授权规则能够根据请求的来源判断是否允许本次请求通过。
在Sentinel的授权规则中,提供了白名单与黑名单两种授权类型。
- 若配置白名单则只有请求来源位于白名单内时才可通过。
- 若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
调用方信息通过 ContextUtil.enter(resourceName, origin) 方法中的 origin 参数传入。

(2) 配置实现
-
在
cloudalibaba-sentinel-service8401模块的 controller包 中创建 EmpowerController类。/** * 授权规则,用来处理请求的来源 */ @RestController public class EmpowerController { @GetMapping(value = "/empower") public String requestSentinel() { return "Sentinel授权规则"; } } -
在
cloudalibaba-sentinel-service8401模块中创建 handler包 中的 MyRequestOriginParser类,实现 RequestOriginParser接口。@Component public class MyRequestOriginParser implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest httpServletRequest) { return httpServletRequest.getParameter("blackOrWhite"); } } -
在Sentinel中配置授权规则:
-
在浏览器中访问测试:
http://localhost:8401/empower?blackOrWhite=AAA http://localhost:8401/empower?blackOrWhite=BBB浏览器中返回:
Blocked by Sentinel (flow limiting)
4.5 规则持久化
将限流配置规则持久化进Nacos保存,只要Nacos里面的配置不删除,sentinel上的流控规则持续有效。
(1) 持久化配置实现
-
修改
cloudalibaba-sentinel-service8401模块的 pom.xml 文件,添加 sentinel 持久化到 nacos 的依赖。<!--SpringCloud ailibaba sentinel-datasource-nacos --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> -
修改
cloudalibaba-sentinel-service8401模块的 application.yml 文件,添加 sentinel 持久化到 nacos 的配置。spring: application: name: cloudalibaba-sentinel-service cloud: sentinel: datasource: ds1: # 自定义key nacos: server-addr: localhost:8848 # nacos地址 dataId: ${spring.application.name} groupId: DEFAULT_GROUP data-type: json rule-type: flow # 流控规则 -
在Nacos中添加业务规则配置:


JSON中的配置内容:
[ { "resource": "/rateLimit/byUrl", "limitApp": "default", "grade": 1, "count": 1, "strategy": 0, "controlBehavior": 0, "clusterMode": false } ]resource:在controller中设置的资源名称(路径);limitApp:来源应用;grade:阈值类型,0表示线程数,1表示QPS;count:单机阈值;strategy:流控模式,0表示直接,1表示关联,2表示链路;controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;clusterMode:是否集群。
(2) yml配置详解
spring:
cloud:
sentinel:
datasource:
# 名称随意
flow:
nacos:
server-addr: localhost:8848 # nacos地址
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow # 规则类型
degrade:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
rule-type: degrade
system:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-system-rules
groupId: SENTINEL_GROUP
rule-type: system
authority:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-authority-rules
groupId: SENTINEL_GROUP
rule-type: authority
param-flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-param-flow-rules
groupId: SENTINEL_GROUP
rule-type: param-flow
rule-type 是 com.alibaba.cloud.sentinel.datasource.RuleType 枚举类中的类型:
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) JSON参数详解
① 流控规则
[
{
// 资源名
"resource": "/test",
// 针对来源,若为 default 则不区分调用来源
"limitApp": "default",
// 限流阈值类型(1:QPS;0:并发线程数)
"grade": 1,
// 阈值
"count": 1,
// 是否是集群模式
"clusterMode": false,
// 流控效果(0:快速失败;1:Warm Up(预热模式);2:排队等待)
"controlBehavior": 0,
// 流控模式(0:直接;1:关联;2:链路)
"strategy": 0,
// 预热时间(秒,预热模式需要此参数)
"warmUpPeriodSec": 10,
// 超时时间(排队等待模式需要此参数)
"maxQueueingTimeMs": 500,
// 关联资源、入口资源(关联、链路模式)
"refResource": "rrr"
}
]
② 降级规则
[
{
// 资源名
"resource": "/test1",
// 针对来源,若为 default 则不区分调用来源
"limitApp": "default",
// 熔断策略(0:慢调用比例,1:异常比率,2:异常计数)
"grade": 0,
// 最大RT、比例阈值、异常数
"count": 200,
// 慢调用比例阈值,仅慢调用比例模式有效
"slowRatioThreshold": 0.2,
// 最小请求数
"minRequestAmount": 5,
// 当单位统计时长(类中默认1000)
"statIntervalMs": 1000,
// 熔断时长
"timeWindow": 10
}
]
③ 热点规则
[
{
// 基础设置----------------------------------------
// 资源名
"resource": "/test1",
// 针对来源,若为 default 则不区分调用来源
"limitApp": "default",
// 限流模式(QPS 模式,不可更改)
"grade": 1,
// 参数索引
"paramIdx": 0,
// 单机阈值/均摊阈值/集群阈值
"count": 13,
// 统计窗口时长
"durationInSec": 6,
// 集群设置----------------------------------------
// 是否集群 默认false
"clusterMode": false,
// 应对突发请求时额外允许的请求数目
"burstCount": 0,
// 集群模式配置
"clusterConfig": {
"fallbackToLocalWhenFail": true,
"flowId": 2,
"sampleCount": 10,
"thresholdType": 0,
"windowIntervalMs": 1000
},
// 流控效果设置----------------------------------------
// 流控效果(支持快速失败和匀速排队模式)
"controlBehavior": 0,
// 超时时间(排队等待模式需要此参数)
"maxQueueingTimeMs": 0,
// 参数例外项----------------------------------------
"paramFlowItemList": [
{
// 参数类型
"classType": "int",
// 限流阈值
"count": 222,
// 参数值
"object": "2"
}
]
}
]
④ 系统规则
[
{
// RT
"avgRt": 1,
// CPU 使用率
"highestCpuUsage": -1,
// LOAD
"highestSystemLoad": -1,
// 线程数
"maxThread": -1,
// 入口 QPS
"qps": -1
}
]
⑤ 授权规则
[
{
// 资源名
"resource": "sentinel_spring_web_context",
// 流控应用
"limitApp": "/test",
// 授权类型(0代表白名单;1代表黑名单。)
"strategy": 0
}
]
5. 🛠️OpenFeign和Sentinel集成实现服务降级
5.1 需求说明
- OpenFeign接口的统一fallback服务降级处理。
- Sentinel访问触发了自定义的限流配置,在注解
@SentinelResource里面配置的blockHandler方法。

5.2 代码实现
(1) 修改微服务提供者9001模块
① 改pom
修改 cloudalibaba-provider-payment9001 模块的 pom.xml 文件,添加sentinel依赖。
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
② 改yml
修改 cloudalibaba-provider-payment9001 模块的 application.yml 文件,添加sentinel配置。
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
# 配置Sentinel
sentinel:
transport:
dashboard: localhost:8080
port: 8719
③ 业务类
修改 cloudalibaba-provider-payment9001 模块下 controller包 中的 PayAlibabaController类,添加整合案例代码。
@RestController
public class PayAlibabaController {
// ......
// =============== OpenFeign+Sentinel进行服务降级和流量监控的整合处理案例 ===============
/** 模拟查询数据 */
@GetMapping("/pay/nacos/get/{orderNo}")
@SentinelResource(value = "getPayByOrderNo", blockHandler = "handlerBlockHandler")
public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo) {
// 模拟从数据库查询出数据并赋值给DTO
PayDTO payDTO = new PayDTO();
payDTO.setId(1024);
payDTO.setOrderNo(orderNo);
payDTO.setAmount(BigDecimal.valueOf(9.9));
payDTO.setPayNo("pay:" + IdUtil.fastUUID());
payDTO.setUserId(1);
return ResultData.success("查询返回值:" + payDTO);
}
/** 限流时触发的方法 */
public ResultData handlerBlockHandler(@PathVariable("orderNo") String orderNo, BlockException exception) {
return ResultData.fail(ReturnCodeEnum.RC500.getCode(),
"getPayByOrderNo服务不可用,触发sentinel流控配置规则o(╥﹏╥)o");
}
}
(2) 修改公共服务模块
① 改pom
修改 cloud-api-commons 模块的 pom.xml 文件,添加 OpenFeign 和 Sentinel 的依赖。
<!--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>
② 创建服务降级处理类
在 cloud-api-commons 模块的 apis包 中创建 PayFeignSentinelApiFallBack类。
@Component
public class PayFeignSentinelApiFallBack implements PayFeignSentinelApi {
@Override
public ResultData getPayByOrderNo(String orderNo) {
return ResultData.fail(ReturnCodeEnum.RC500.getCode(), "对方服务宕机或不可用,FallBack服务降级o(╥﹏╥)o");
}
}
③ 新建服务调用接口
在 cloud-api-commons 模块的 apis包 中创建 PayFeignSentinelApi接口。
@FeignClient(value = "nacos-payment-provider", fallback = PayFeignSentinelApiFallBack.class)
public interface PayFeignSentinelApi {
@GetMapping("/pay/nacos/get/{orderNo}")
ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo);
}
(3) 修改微服务消费者83模块
① 改pom
修改 cloudalibaba-consumer-nacos-order83 模块的 pom.xml 文件。
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.example.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
② 改yml
修改 cloudalibaba-consumer-nacos-order83 模块的 application.yml 文件,添加激活 Feign 和 Sentinel 的整合。
# 激活 Feign 和 Sentinel 的整合
feign:
sentinel:
enabled: true
③ 主启动
修改 cloudalibaba-consumer-nacos-order83 模块的 Main83类,添加 @EnableFeignClients 注解启动 Feign 的功能。
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class Main83 {
public static void main(String[] args) {
SpringApplication.run(Main83.class, args);
}
}
④ 业务类
修改 cloudalibaba-consumer-nacos-order83 模块下 controller包 中的 OrderNacosController类,通过 api 接口调用业务类方法。
@RestController
public class OrderNacosController {
// ......
@Resource
private PayFeignSentinelApi payFeignSentinelApi;
@GetMapping(value = "/consumer/pay/nacos/get/{orderNo}")
public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo) {
return payFeignSentinelApi.getPayByOrderNo(orderNo);
}
}
6. 🛠️Gateway和Sentinel集成实现服务限流
6.1 需求说明
建立网关9528模块,保护服务提供者9001模块。
6.2 代码实现
官网案例:https://sentinelguard.io/zh-cn/docs/api-gateway-flow-control.html
(1) 建Module
(2) 改pom
修改 cloudalibaba-sentinel-gateway9528 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cloudalibaba-sentinel-gateway9528</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- springcloud gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- alibaba-sentinel transport simple http-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.8</version>
</dependency>
<!-- alibaba-sentinel gateway适配器 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.8</version>
</dependency>
<!-- 注解api -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(3) 写yml
在 cloudalibaba-sentinel-gateway9528 模块的 src/main/resources目录 中创建 application.yml 文件。
server:
port: 9528
spring:
application:
name: cloudalibaba-sentinel-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: pay_routh1 # 路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:9001 # 匹配后提供服务的路由地址
predicates:
- Path=/pay/** # 断言,路径相匹配的进行路由
[!NOTE]
如果要实现 uri 以
lb://服务名的方式配置,需要在服务消费者模块的 pom.xml 文件中加入LoadBalancer依赖:<!--LoadBalancer--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>再在服务消费者模块的 controller 中通过
PayFeignSentinelApi接口调用服务提供者模块的方法,最后在浏览器中访问服务消费者模块的路径。但本例直接访问网关,没有创建服务消费者,所以使用具体 ip 地址的形式访问。
(4) 主启动
在 cloudalibaba-sentinel-gateway9528 模块中修改(创建)Main9528类。
@SpringBootApplication
@EnableDiscoveryClient
public class Main9528 {
public static void main(String[] args) {
SpringApplication.run(Main9528.class, args);
}
}
(5) 配置类
在 cloudalibaba-sentinel-gateway9528 模块中创建 config包 中的 GatewayConfiguration类。
更多详情见官网配置类示例。
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct // javax.annotation.PostConstruct
public void doInit() {
initBlockHandler();
}
/**
* 限流处理+自定义返回限流后提示信息
* */
private void initBlockHandler() {
// 设置流控规则(更多配置见官网示例)
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("pay_routh1") // yml文件中配置的spring.cloud.gateway.routes.id
.setCount(2) // 单机阈值
.setIntervalSec(1) // 统计时长
// 上面配置表示1秒内仅允许2个请求
);
// 加载规则
GatewayRuleManager.loadRules(rules);
// 定义服务降级处理器
BlockRequestHandler handler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
Map<String, String> map = new HashMap<>();
map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
map.put("errorMessage", "请求太过频繁,系统忙不过来,触发限流");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
// 回调
GatewayCallbackManager.setBlockHandler(handler);
}
}
(6) 测试
在浏览器中输入:
http://localhost:9528/pay/nacos/111
正常返回:
nacos registry, serverPort: 9001 id111
限流返回:
{"errorMessage":"请求太过频繁,系统忙不过来,触发限流","errorCode":"Too Many Requests"}
四、Seata分布式事务
1. 概述
1.1 分布式事务
(1) 场景
在订单支付成功后,交易中心会调用订单中心的服务把订单状态更新,并调用物流中心的服务通知商品发货,同时还要调用积分中心的服务为用户增加相应的积分。
如何保障分布式事务一致性,成为了确保订单业务稳定运行的核心诉求之一。

(2) 问题
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。但是,关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
1.2 Seata简介
(1) 简介
Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架。
官方网站:https://seata.apache.org/zh-cn/
官方文档:https://seata.apache.org/zh-cn/docs/overview/what-is-seata
官方仓库:https://github.com/apache/incubator-seata
(2) 作用
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
1.3 Seata的工作流程
(1) Seata的分布式交易解决方案
纵观整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知。
只需要使用一个 @GlobalTransactional 注解在业务方法上即可。

(2) Seata对分布式事务的协调和控制
1个XID + 3个概念(TC-->TM-->RM)
XID是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。

① TC
TC(Transaction Coordinator)事务协调者【仅有一个】。维护全局和分支事务的状态,驱动全局事务提交或回滚。
就是Seata本身,负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
② TM
TM(Transaction Manager)事务管理器【仅有一个】。定义全局事务的范围:开始全局事务、提交或回滚全局事务。
标注全局 @GlobalTransactional 启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据 TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议
③ RM
RM(Resource Manager)资源管理器【可有多个】。管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
就是mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。
(3) 分布式事务的执行流程
三个组件相互协作,TC 以 Seata 服务器(Server)形式独立部署,TM和RM则是以Seata Client的形式集成在微服务中运行,流程如下:

- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
2. 🆕Seata下载安装
下载地址:https://seata.apache.org/zh-cn/unversioned/download/seata-server/
部署指南:https://seata.apache.org/zh-cn/docs/ops/deploy-guide-beginner
步骤一:启动包
步骤二:建表(仅db)
全局事务会话信息由3块内容构成,全局事务-->分支事务-->全局锁,对应表global_table、branch_table、lock_table步骤三:修改store.mode
- 启动包: seata-->conf-->application.yml,修改store.mode="db或者redis"
- 源码: 根目录-->seata-server-->resources-->application.yml,修改store.mode="db或者redis"
步骤四:修改数据库连接|redis属性配置
- 启动包: seata-->conf-->application.example.yml中附带额外配置,将其db|redis相关配置复制至application.yml,进行修改store.db或store.redis相关属性。
- 源码: 根目录-->seata-server-->resources-->application.example.yml中附带额外配置,将其db/redis相关配置复制至application.yml,进行修改store.db或store.redis相关属性。
步骤五:启动
- 源码启动: 执行ServerApplication.java的main方法
- 命令启动: seata-server.sh -h 127.0.0.1 -p 8091 -m db
注: 堆内存建议分配2G,堆外内存1G
2.1 在MySQL数据库中建库建表
(1) 创建seata数据库
CREATE DATABASE seata;
USE seata;
(2) 在seata库中建表
官方建表脚本:https://github.com/apache/incubator-seata/blob/2.x/script/server/db/mysql.sql
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
CREATE TABLE IF NOT EXISTS `vgroup_table`
(
`vGroup` VARCHAR(255),
`namespace` VARCHAR(255),
`cluster` VARCHAR(255),
UNIQUE KEY `idx_vgroup_namespace_cluster` (`vGroup`,`namespace`,`cluster`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
2.2 修改配置文件
将下载的 Seata 源码中 seata-server 模块 的 application.yml 文件拷贝备份一份,然后参考 application.example.yml 文件,修改 application.yml 文件。
# Seata2.2.0版本配置
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
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP # 后续自己在nacos里面新建SEATA_GROUP
namespace:
cluster: default
username: nacos
password: nacos
store:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
# url中指定seata库(jdbc:mysql://localhost:3306/seata?...)
url: jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
user: root # 数据库用户名
password: 123456 # 数据库密码
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
vgroup-table: vgroup_table
query-limit: 1000
max-wait: 5000
# -----------------------------------------------------------------------------
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
csrf-ignore-urls: /metadata/v1/**
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/version.json,/health,/error,/vgroup/v1/**
2.3 启动SeataServer
在 IDEA 中连接上 seata 数据库后,运行 seata-server 模块的 ServerApplication类。
启动后再在 nacos 服务注册中心查看 seata 是否注册成功。
再在浏览器中输入以下地址,访问seata服务,默认用户名和密码均为seata:
http://localhost:7091

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

3.1 数据库和表准备
[!WARNING]
本节内容前三小节为拆分讲解,实际操作可以直接执行[第(4)节](#(4) 总建库建表语句)的 sql 脚本内容。
(1) 创建业务数据库
- seata_order:存储订单的数据库;
- seata_storage:存储库存的数据库;
- seata_account:存储账户信息的数据库。
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
(2) 创建回滚日志表|undo_log
在上述三个数据库中分别都要创建 undo_log 回滚日志表。该表用于 seata 记录数据库操作过程中的回滚链。
官方建表语句:https://github.com/apache/incubator-seata/blob/2.x/script/client/at/db/mysql.sql
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`);
(3) 创建业务表
在上述三个数据库中分别创建业务表。
-
seata_order库中:CREATE TABLE t_order( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id', `product_id` BIGINT(11)DEFAULT NULL COMMENT '产品id', `count` INT(11) DEFAULT NULL COMMENT '数量', `money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额', `status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结' )ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; SELECT * FROM t_order; -
seata_account库中:CREATE TABLE t_account( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id', `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id', `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度', `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用账户余额', `residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度' )ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000'); SELECT * FROM t_account; -
seata_storage库中:CREATE TABLE t_storage( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id', `total` INT(11) DEFAULT NULL COMMENT '总库存', `used` INT(11) DEFAULT NULL COMMENT '已用库存', `residue` INT(11) DEFAULT NULL COMMENT '剩余库存' )ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100'); SELECT * FROM t_storage;
(4) 总建库建表语句
-
seata_order库 +t_order表 +undo_log表-- order CREATE DATABASE seata_order; USE seata_order; CREATE TABLE t_order ( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id', `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id', `count` INT(11) DEFAULT NULL COMMENT '数量', `money` DECIMAL(11, 0) DEFAULT NULL COMMENT '金额', `status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结' ) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8; SELECT * FROM t_order; -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table'; ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`); -
seata_storage库 +t_storage表 +undo_log表-- storage CREATE DATABASE seata_storage; USE seata_storage; CREATE TABLE t_storage ( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id', `total` INT(11) DEFAULT NULL COMMENT '总库存', `used` INT(11) DEFAULT NULL COMMENT '已用库存', `residue` INT(11) DEFAULT NULL COMMENT '剩余库存' ) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8; INSERT INTO t_storage(`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100'); SELECT * FROM t_storage; -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table'; ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`); -
seata_account库 +t_account表 +undo_log表-- account create database seata_account; use seata_account; CREATE TABLE t_account ( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id', `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id', `total` DECIMAL(10, 0) DEFAULT NULL COMMENT '总额度', `used` DECIMAL(10, 0) DEFAULT NULL COMMENT '已用余额', `residue` DECIMAL(10, 0) DEFAULT '0' COMMENT '剩余可用额度' ) ENGINE = INNODB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8; INSERT INTO t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000'); SELECT * FROM t_account; -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table'; ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
最终效果:
3.2 微服务开发
(1) 修改公共服务模块
① 新建Storage库存的OpenFeign接口
在 cloud-api-commons 模块的 apis包 中创建 StorageFeignApi接口。
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi {
/**
* 扣减库存
*/
@PostMapping(value = "/storage/decrease")
ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
② 新建Account账户的OpenFeign接口
在 cloud-api-commons 模块的 apis包 中创建 AccountFeignApi接口。
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi {
/**
* 扣减账户余额
*/
@PostMapping("/account/decrease")
ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}
(2) 新建订单Order微服务2001模块
① 建Module
② 改pom
修改 seata-order-service2001 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>seata-order-service2001</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- nacos -->
<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.example.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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
③ 写yml
在 seata-order-service2001 模块的 src/main/resources目录 中创建 application.yml 文件。
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: 123456
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
# 对应Nacos的数据模型
# 1. Namespace
namespace: ""
# 2. Group
group: SEATA_GROUP
# 3. Service/DataId
application: seata-server
# 4. Cluster
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称,和源码相同
service:
vgroup-mapping:
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT # seata数据源代理模式(默认AT)
logging:
level:
io:
seata: info
附:seata 部分的详细配置。(了解即可,太详细也不好维护)
seata: registry: # seata注册配置 type: nacos # seata注册类型 nacos: application: seata-server #seata应用名称 server-addr: 127.0.0.1:8848 namespace: "" group: SEATA_GROUP cluster: default config: # seata配置抓取 nacos: server-addr: 127.0.0.1:8848 namespace: "" group: SEATA_GROUP username: nacos password: nacos tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称 service: vgroup-mapping: default_tx_group: default # 事务群组的映射配置关系 data-source-proxy-mode: AT application-id: seata-server
④ 主启动
在 seata-order-service2001 模块中修改(创建)SeataOrderMainApp2001类。
@SpringBootApplication
@MapperScan("com.example.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@EnableFeignClients
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
⑤ Mapper4一键生成
-
修改
mybatis_generator2024模块的 config.properties 关于Jdbc的配置文件,注释(删除)原配置信息,新增关于seata_order库的配置信息。# 表生成在的包名 package.name=com.example.cloud # mysql8.0 jdbc.driverClass=com.mysql.cj.jdbc.Driver # 1. localhost是ip地址 # 2. seata_order是数据库名 jdbc.url=jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true jdbc.user=root jdbc.password=123456 -
修改
mybatis_generator2024模块的 generatorConfig.xml 关于mybatis-generator的配置文件,注释(删除)原配置信息,新增关于seata_order库中相关表的配置信息。<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <properties resource="config.properties"/> <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/> <property name="caseSensitive" value="true"/> </plugin> <!-- 读取jdbc驱动的配置 --> <jdbcConnection driverClass="${jdbc.driverClass}" connectionURL="${jdbc.url}" userId="${jdbc.user}" password="${jdbc.password}"> </jdbcConnection> <!-- 生成实体类 --> <javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/> <!-- 生成Mapper接口 --> <sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/> <!-- 生成MapperXML实现 --> <javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/> <!-- 相较于原配置修改部分 --> <!-- seata_order --> <table tableName="t_order" domainObjectName="Order"> <generatedKey column="id" sqlStatement="JDBC"/> </table> </context> </generatorConfiguration> -
使用插件生成实体类。
-
剪切在
mybatis_generator2024模块中生成的实体类到seata-order-service2001模块中。
-
对
seata-order-service2001模块中生成的实体类使其实现Serializable序列化接口,并添加@ToString注解。@Table(name = "t_order") @ToString public class Order implements Serializable { // ...... } -
在
seata-order-service2001模块的 src/main/resources目录 下创建 mapper目录,并将生成的 MapperXML 文件剪切到此目录中。
⑥ Service|@GlobalTransactional
-
在
seata-order-service2001模块中创建 service包 中的 OrderService接口。public interface OrderService { /** * 创建订单 */ void create(Order order); } -
在
seata-order-service2001模块中创建 service/impl包 中的 OrderServiceImpl实现类。在分布式事务的方法上使用
@GlobalTransactional注解,保证数据的一致性。因为此案例中的订单 Order 微服务模块,[既是TM也是RM](#3.0 案例需求说明),所以在 TM 中加上该注解以实现分布式事务。
@Service public class OrderServiceImpl implements OrderService { @Resource private OrderMapper orderMapper; @Resource//订单微服务通过OpenFeign去调用库存微服务 private StorageFeignApi storageFeignApi; @Resource//订单微服务通过OpenFeign去调用账户微服务 private AccountFeignApi accountFeignApi; @Override // name属性自定义,可在Seata控制中心里查看 // rollbackFor指定异常的类型 @GlobalTransactional(name = "my-create-order", rollbackFor = Exception.class) // AT模式 //@GlobalTransactional @Transactional(rollbackFor = Exception.class) // XA模式 public void create(Order order) { // 0. 从seata上下文中获取xid,全局事务的检查 String xid = RootContext.getXID(); // 1. 新建订单 // 订单状态status:0:创建中;1:已完结 order.setStatus(0); int result = orderMapper.insertSelective(order); // 插入订单成功后,获得插入mysql的实体对象 Order orderFromDB = null; if (result > 0) { // 从mysql中查出刚插入的记录 orderFromDB = orderMapper.selectOne(order); //orderFromDB = orderMapper.selectByPrimaryKey(order.getId()); // 2. 扣减库存 storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount()); // 3. 扣减账号余额 accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney()); // 4. 修改订单状态 //订单状态status:0:创建中;1:已完结 orderFromDB.setStatus(1); // 5. 更新到数据库中 Example whereCondition = new Example(Order.class); Example.Criteria criteria = whereCondition.createCriteria(); criteria.andEqualTo("userId", orderFromDB.getUserId()); criteria.andEqualTo("status", 0); int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition); } } }
⑦ Controller
在 seata-order-service2001 模块中创建 controller包 中的 OrderController类。
@RestController
public class OrderController {
@Resource
private OrderService orderService;
/**
* 创建订单
*/
@GetMapping("/order/create")
public ResultData create(Order order) {
orderService.create(order);
return ResultData.success(order);
}
}
(3) 新建库存Storage微服务2002模块
① 建Module
② 改pom
修改 seata-storage-service2002 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>seata-storage-service2002</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- nacos -->
<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_commons_utils-->
<dependency>
<groupId>com.example.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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
③ 写yml
在 seata-storage-service2002 模块的 src/main/resources目录 中创建 application.yml 文件。
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: 123456
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================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
④ 主启动
在 seata-storage-service2002 模块中修改(创建)SeataStorageMainApp2002类。
@SpringBootApplication
@MapperScan("com.example.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@EnableFeignClients
public class SeataStorageMainApp2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageMainApp2002.class, args);
}
}
⑤ Mapper4一键生成
-
修改
mybatis_generator2024模块的 config.properties 关于Jdbc的配置文件,注释(删除)原配置信息,新增关于seata_storage库的配置信息。# 表生成在的包名 package.name=com.example.cloud # mysql8.0 jdbc.driverClass=com.mysql.cj.jdbc.Driver # 1. localhost是ip地址 # 2. seata_storage是数据库名 jdbc.url = jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true jdbc.user = root jdbc.password =123456 -
修改
mybatis_generator2024模块的 generatorConfig.xml 关于mybatis-generator的配置文件,注释(删除)原配置信息,新增关于seata_storage库中相关表的配置信息。<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <properties resource="config.properties"/> <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/> <property name="caseSensitive" value="true"/> </plugin> <!-- 读取jdbc驱动的配置 --> <jdbcConnection driverClass="${jdbc.driverClass}" connectionURL="${jdbc.url}" userId="${jdbc.user}" password="${jdbc.password}"> </jdbcConnection> <!-- 生成实体类 --> <javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/> <!-- 生成Mapper接口 --> <sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/> <!-- 生成MapperXML实现 --> <javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/> <!-- 相较于原配置修改部分 --> <!--seata_storage--> <table tableName="t_storage" domainObjectName="Storage"> <generatedKey column="id" sqlStatement="JDBC"/> </table> </context> </generatorConfiguration> -
使用插件生成实体类。
-
剪切在
mybatis_generator2024模块中生成的实体类到seata-storage-service2002模块中。
-
对
seata-storage-service2002模块中生成的实体类使其实现Serializable序列化接口,并添加@ToString注解。@Table(name = "t_storage") @ToString public class Storage implements Serializable { // ...... } -
在
seata-storage-service2002模块的 src/main/resources目录 下创建 mapper目录,并将生成的 MapperXML 文件剪切到此目录中。
⑥ Mapper
-
修改
seata-storage-service2002模块下 mapper包 中的 StorageMapper接口,添加扣减库存的方法。import com.example.cloud.entities.Storage; import tk.mybatis.mapper.common.Mapper; import org.apache.ibatis.annotations.Param; public interface StorageMapper extends Mapper<Storage> { /** * 扣减库存 */ void decrease(@Param("productId") Long productId, @Param("count") Integer count); } -
修改
seata-storage-service2002模块下 resources/mapper目录 中的 StorageMapper.xml 文件,添加对应的扣减库存的方法。<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.cloud.mapper.StorageMapper"> <resultMap id="BaseResultMap" type="com.example.cloud.entities.Storage"> <!-- WARNING - @mbg.generated --> <id column="id" jdbcType="BIGINT" property="id"/> <result column="product_id" jdbcType="BIGINT" property="productId"/> <result column="total" jdbcType="INTEGER" property="total"/> <result column="used" jdbcType="INTEGER" property="used"/> <result column="residue" jdbcType="INTEGER" property="residue"/> </resultMap> <!-- update方法 --> <update id="decrease"> UPDATE t_storage SET used = used + #{count}, residue = residue - #{count} WHERE product_id = #{productId} </update> </mapper>
⑦ Service
-
在
seata-storage-service2002模块中创建 service包 中的 StorageService接口。public interface StorageService { /** * 扣减库存 */ void decrease(Long productId, Integer count); } -
在
seata-storage-service2002模块中创建 service/impl包 中的 StorageServiceImpl实现类。@Service public class StorageServiceImpl implements StorageService { @Resource private StorageMapper storageMapper; /** * 扣减库存 */ @Override public void decrease(Long productId, Integer count) { storageMapper.decrease(productId, count); } }
⑧ Controller
在 seata-storage-service2002 模块中创建 controller包 中的 StorageController类。
@RestController
public class StorageController {
@Resource
private StorageService storageService;
/**
* 扣减库存
*/
@RequestMapping("/storage/decrease")
public ResultData decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return ResultData.success("扣减库存成功!");
}
}
(4) 新建账户Account微服务2003模块
① 建Modulle
② 改pom
修改 seata-account-service2003 模块的 pom.xml 文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>springcloud-learn</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>seata-account-service2003</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- nacos -->
<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_commons_utils-->
<dependency>
<groupId>com.example.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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
③ 写yml
在 seata-account-service2003 模块的 src/main/resources目录 中创建 application.yml 文件。
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: 123456
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================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
④ 主启动
在 seata-account-service2003 模块中修改(创建)SeataAccountMainApp2003类。
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.example.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
public class SeataAccountMainApp2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
⑤ Mapper4一键生成
-
修改
mybatis_generator2024模块的 config.properties 关于Jdbc的配置文件,注释(删除)原配置信息,新增关于seata_account库的配置信息。# 表生成在的包名 package.name=com.example.cloud # mysql8.0 jdbc.driverClass=com.mysql.cj.jdbc.Driver # 1. localhost是ip地址 # 2. seata_account是数据库名 jdbc.url = jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true jdbc.user = root jdbc.password =123456 -
修改
mybatis_generator2024模块的 generatorConfig.xml 关于mybatis-generator的配置文件,注释(删除)原配置信息,新增关于seata_account库中相关表的配置信息。<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <properties resource="config.properties"/> <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/> <property name="caseSensitive" value="true"/> </plugin> <!-- 读取jdbc驱动的配置 --> <jdbcConnection driverClass="${jdbc.driverClass}" connectionURL="${jdbc.url}" userId="${jdbc.user}" password="${jdbc.password}"> </jdbcConnection> <!-- 生成实体类 --> <javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/> <!-- 生成Mapper接口 --> <sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/> <!-- 生成MapperXML实现 --> <javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/> <!-- 相较于原配置修改部分 --> <!--seata_account--> <table tableName="t_account" domainObjectName="Account"> <generatedKey column="id" sqlStatement="JDBC"/> </table> </context> </generatorConfiguration> -
使用插件生成实体类。
-
剪切在
mybatis_generator2024模块中生成的实体类到seata-account-service2003模块中。
-
对
seata-account-service2003模块中生成的实体类使其实现Serializable序列化接口,并添加@ToString注解。@Table(name = "t_account") @ToString public class Account implements Serializable { // ...... } -
在
seata-account-service2003模块的 src/main/resources目录 下创建 mapper目录,并将生成的 MapperXML 文件剪切到此目录中。
⑥ Mapper
-
修改
seata-account-service2003模块下 mapper包 中的 AccountMapper接口,添加扣减金额的方法。import com.example.cloud.entities.Account; import tk.mybatis.mapper.common.Mapper; import org.apache.ibatis.annotations.Param; public interface AccountMapper extends Mapper<Account> { /** * 扣减账户金额 * * @param userId 用户id * @param money 本次消费金额 */ void decrease(@Param("userId") Long userId, @Param("money") Long money); } -
修改
seata-account-service2003模块下 resources/mapper目录 中的 AccountMapper.xml 文件,添加对应的扣减金额的方法。<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.cloud.mapper.AccountMapper"> <resultMap id="BaseResultMap" type="com.example.cloud.entities.Account"> <!-- WARNING - @mbg.generated --> <id column="id" jdbcType="BIGINT" property="id"/> <result column="user_id" jdbcType="BIGINT" property="userId"/> <result column="total" jdbcType="DECIMAL" property="total"/> <result column="used" jdbcType="DECIMAL" property="used"/> <result column="residue" jdbcType="DECIMAL" property="residue"/> </resultMap> <!-- money:本次消费金额 t_account:数据库表 total总额度 = 累计已消费金额(used) + 剩余可用额度(residue) --> <update id="decrease"> UPDATE t_account SET residue = residue - #{money}, used = used + #{money} WHERE user_id = #{userId}; </update> </mapper>
⑦ Service
-
在
seata-account-service2003模块中创建 service包 中的 AccountService接口。import org.apache.ibatis.annotations.Param; public interface AccountService { /** * 扣减账户余额 * * @param userId 用户id * @param money 本次消费金额 */ void decrease(@Param("userId") Long userId, @Param("money") Long money); } -
在
seata-account-service2003模块中创建 service/impl包 中的 AccountServiceImpl实现类。@Service public class AccountServiceImpl implements AccountService { @Resource AccountMapper accountMapper; /** * 扣减账户余额 */ @Override public void decrease(Long userId, Long money) { accountMapper.decrease(userId, money); // 1. 超时演示 // myTimeOut(); // 2. 异常演示 // int age = 10/0; } /** * 模拟超时异常,全局事务回滚 */ private static void myTimeOut() { try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); } } }
⑧ Controller
在 seata-account-service2003 模块中创建 controller包 中的 AccountController类。
@RestController
public class AccountController {
@Resource
AccountService accountService;
/**
* 扣减账户余额
*/
@RequestMapping("/account/decrease")
public ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money) {
accountService.decrease(userId, money);
return ResultData.success("扣减账户余额成功!");
}
}
3.3 代码测试
(1) 启动服务
- 启动Nacos
- 启动Seata
- 启动订单微服务2001
- 启动库存微服务2002
- 启动账户微服务2003
(2) 版本匹配问题
截止目前2024年10月,Seata2.2.0版本,仍无法匹配 SpringBoot3.2.0 和 SpringCloud2023.0.0 版本,需要降级使用。
修改根父工程的 pom.xml 文件:
<!--<spring.boot.version>3.2.0</spring.boot.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>-->
<spring.boot.version>3.1.7</spring.boot.version>
<spring.cloud.version>2022.0.4</spring.cloud.version>
(3) 浏览器中访问
在浏览器中访问如下url,并在 seata-account-service2003 模块的 AccountServiceImpl实现类 中模拟出错情况,观察数据库表中数据的状态。
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
-
版本未降级时的报错:
-
不加
@GlobalTransactional注解:{"code":"500","message":"[500] during [POST] to [http://seata-storage-service/storage/decrease?productId=1&count=10] [StorageFeignApi#decrease(Long,Integer)]: [{\"code\":\"500\",\"message\":\"Name for argument of type [java.lang.Long] not specified, and parameter name information not found in class file either.\",\"data\":null,\"timestamp\":1729769218774}]","data":null,"timestamp":1729769218807} -
添加
@GlobalTransactional注解:{"code":"500","message":"try to proceed invocation error","data":null,"timestamp":1729771681407}
-
-
版本降级后的成功信息:
{"code":"200","message":"success","data":{"id":30,"userId":1,"productId":1,"count":10,"money":100,"status":0},"timestamp":1729771143823}
超时演示过程中Seata控制台的信息:


4. Seata原理之AT模式
AT模式(自动事务Auto Transaction),是自动型的分布式事务解决方案。这个自动体现在无需代码入侵,也就是说我们不需要再编写多余的代码来实现这个模式,只需要在方法中添加上指定的注解即可。
AT模式的整体机制由两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志(undo_log)进行反向补偿。
4.1 一阶段加载
在一阶段,Seata 会拦截“业务SQL”。
- 解析 SQL 语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,
- 执行“业务 SQL”更新业务数据,在业务数据更新之后,
- 其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

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

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

#Part附录 最终工程文件结构
〇、整体结构
一、公共服务模块
1. cloud-api-commons
二、实体类生成模块
1. mybatis_generator2024
三、SpringCloud相关模块
1. cloud-consumer-order80
2. cloud-provider-payment8001
3. cloud-provider-payment8002
4. cloud-consumer-feign-order80
5. cloud-gateway9527
四、Nacos相关模块
1. cloudalibaba-provider-payment9001
2. cloudalibaba-consumer-nacos-order83
3. cloudalibaba-config-nacos-client3377
五、Sentinel相关模块
1. cloudalibaba-sentinel-service8401
2. cloudalibaba-sentinel-gateway9528
六、Seata相关模块
1. seata-order-service2001
2. seata-storage-service2002
3. seata-account-service2003
很高兴本文对你有用(*^_^*),如需转载请记得标明出处哟(☆▽☆):
本文来自博客园,作者:雪与锄,原文链接:https://www.cnblogs.com/corianderfiend/articles/18501817






浙公网安备 33010602011771号