文章目录
- 1. 简介
- 1.1 基础知识
- 1.2 组件更替与升级
- 2. 微服务基础项目构建
- 2.1 创建项目
- 2.2 Mapper4生成代码
- 2.3 支付模块编码
- 2.4 项目完善
- 2.5 订单模块编码
- 2.6 工程重构
- 3. consul服务注册与发现
- 3.1 consul简介
- 3.2 consul下载安装
- 3.3 微服务入驻
- 3.4 order订单微服务入驻
- 3.5 其他注册中心的对比
- 3.6 分布式配置
- 3.7 动态刷新
- 3.8 Consul数据持久化配置
- 4. LoadBalancer负载均衡服务调用
- 4.1 简介
- 4.2 负载均衡实现
- 4.3 负载均衡算法切换
- 5. OpenFeign服务调用
- 5.1 介绍
- 5.2 通用步骤
- 5.3 超时控制
- 5.4 重试机制
- 5.5 性能优化HttpClient5
- 5.6 请求响应压缩
- 5.7 日志打印
- 6. Resilience4J服务熔断与降级
- 6.1 简介
- 6.2 Circuit Breaker
- 6.3 案例实战
- 6.3.1 熔断(服务熔断+服务降级)
- 6.3.2 隔离 bulkhead
- 6.3.3 限流 ratelimiter
- 6.4 总结
- 7. Micrometer分布式链路追踪
- 7.1 介绍
- 7.2 分布式链路追踪原理
- 7.2 ZipKin
- 7.3 Micrometer+ZipKin搭建链路监控
- 8. GateWay服务网关
- 8.1 简介
- 8.2 三大核心
- 8.3 工作流程
- 8.4 入门配置
- 8.5 路由映射
- 8.6 Route以微服务名-动态获取服务URI
- 8.7 Predicate断言
- 8.8 自定义Predicate
- 8.9 Filter过滤
- 8.9.1 介绍
- 8.9.2 内置的过滤器
- 8.9.3 自定义过滤器
- 9. SpringCloud Alibaba
- 10. Nacos服务注册和配置中心
- 10.1 介绍
- 10.2 下载安装
- 10.3 服务注册中心
- 10.4 服务配置中心
- 10.5 数据模型之Namespace-Group-DataId
- 11. Sentinel服务熔断与限流
- 11.1 介绍
- 11.2 下载与安装
- 11.3 整合Sentinel案例
- 11.4 流控规则
- 11.4.1 基本介绍
- 11.4.2 流控模式-直接
- 11.4.3 流控模式-关联
- 11.4.4 流控模式-链路
- 11.4.5 流控效果-快速失败
- 11.4.6 流控效果-Warm UP
- 11.4.7 流控效果-排队等待
- 11.4.8 流控效果-并发线程数
- 11.5 熔断规则
- 11.5.1 熔断规则案例 - 慢调用比例
- 11.5.2 熔断规则案例 - 异常比例
- 11.5.3 熔断规则案例 - 异常数
- 11.6 SentinelResource注解
- 11.6.1 按照rest地址限流+默认限流返回
- 11.6.2 按SentinelResource资源名称限流+自定义限流返回
- 11.6.3 按SentinelResource资源名称限流+自定义限流返回+服务降级处理
- 11.7 热点规则
- 11.8 授权规则
- 11.9 规则持久化
- 11.10 集成OpenFeign
- 11.10.1 修改`cloudalibaba-provider-payment9001`
- 11.10.2 修改`cloud-api-commons`
- 11.10.3 修改`cloudalibaba-consumer-nacos-order83`
- 11.10.4 测试
- 11.11 集成GateWay
- 12. Seata分布式事务
- 12.1 简介
- 12.2 seata工作流程
- 12.3 seata下载安装
- 12.4 案例实战
- 12.4.1 数据库准备
- 12.4.2 编码实现
- 12.4.3 测试
- 12.5 面试题
在本笔记中,使用Java17+、SpringBoot3.2.x、SpringCloud2023.0.0、SpringCloud Alibaba2022.0.0.0、Maven3.9+、MySQL8.0+
springcloud官网:https://spring.io/projects/spring-cloud
springcloud源码地址:https://github.com/spring-cloud/
springcloud alibaba官网:https://sca.aliyun.com/en-us/
springcloud alibaba源码地址:https://github.com/alibaba/spring-cloud-alibaba
1. 简介
1.1 基础知识
SpringCloud是什么?有什么用?
比如,我们要做一个项目,功能实现下订单,做支付,但是又要保障系统的数据、网关、服务、安全、调用等等。这使得开发人员无法专注于业务开发。所以,此时SpringCloud就相当于一个第三方来帮我们解决这些事情,开发人员就可以专注于业务的开发。
1.2 组件更替与升级
- 服务注册与发现:Consul,Alibaba Nacos
- 服务调用与负载均衡:LoadBalancer,OpenFeign
- 分布式事务:Alibaba Seata、LCN、Hmily
- 服务熔断和降级:Circuit Breaker(Resilience4J),Alibaba Sentinel
- 服务链路追踪:Micrometer Tracing
- 服务网关:GateWay
- 分布式配置管理:Consul,Alibaba Nacos
2. 微服务基础项目构建
业务需求:订单->支付
- 先做一个通用的boot服务
- 逐步引入cloud组件
2.1 创建项目
创建Maven父工程:
-
新建项目
新建maven项目,删除目录下的src目录和
.gitignore
文件,只留下pom.xml
-
聚合总父工程名字
-
字符编码
打开IDEA的设置,输出
File Encoding
查询,将编码全部设置为UTF-8
-
注解生效激活
打开IDEA设置,搜索
Annotation Processors
,勾选Enable Annotation Processing
即可 -
java编译版本选17
打开IDEA设置,搜素
Java Compiler
,选择模块的Target bytecode version
为17 -
FileType过滤
打开IDEA设置,搜素
File Types
,选择Ignored Files and Folders
进行填写,可以将某些不想在IDEA中看到的文件隐藏
父工程pom文件内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.codewei</groupId>
<artifactId>springcloudStudy</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.22</hutool.version>
<lombok.version>1.18.26</lombok.version>
<druid.version>1.1.20</druid.version>
<mybatis.springboot.version>3.0.2</mybatis.springboot.version>
<mysql.version>8.0.11</mysql.version>
<swagger3.version>2.2.0</swagger3.version>
<mapper.version>4.2.3</mapper.version>
<fastjson2.version>2.0.40</fastjson2.version>
<persistence-api.version>1.0.2</persistence-api.version>
<spring.boot.test.version>3.1.5</spring.boot.test.version>
<spring.boot.version>3.2.0</spring.boot.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
<spring.cloud.alibaba.version>2022.0.0.0</spring.cloud.alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.springboot.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
<!-- 数据持久化-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>${persistence-api.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger3.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.test.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2.2 Mapper4生成代码
mybatis-generator官网:https://mybatis.org/generator/
Mapper4官网:https://mapper.mybatis.io/
一键生成步骤:
- 创建数据库
cloudstudy
,并建立表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');
-
在项目中,建立maven子工程
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>cn.codewei</groupId>
<artifactId>springcloudStudy</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>mybatis_generator2024</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
<configuration>
<configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.2.3</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
-
配置
在
src\main\resource
路径下新建config.properties
和generatorConfig.xml
config.properties文件
# t_pay表包名
package.name=cn.codewei
# mysql8.0
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/cloudstudy?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user=root
jdbc.password=密码
generatorConfig.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="config.properties"/>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
</plugin>
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.url}"
userId="${jdbc.user}"
password="${jdbc.password}">
</jdbcConnection>
<javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>
<javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<table tableName="t_pay" domainObjectName="Pay">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
</context>
</generatorConfiguration>
在maven窗口中,双击mybatis-generator
下的mybatis-generator:generate
。
这样就可以看到,entity和对应的mapper自动生成了。
2.3 支付模块编码
如何构建一个完整的微服务?
建module、改pox、写yaml、主启动、业务类
新建子工程cloud-provider-payment8001
导入依赖
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
编写yaml配置文件
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/cloudstudy?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 密码
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.codewei.entities
configuration:
map-underscore-to-camel-case: true
将主启动类改名为Main8001
@SpringBootApplication
@MapperScan("cn.cdoewei.mapper")
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class,args);
}
}
然后将一件生成的代码拷贝进该子项目中,然后新建实体类PayDTO
用户数据传输(暴露给前端的数据)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PayDTO implements Serializable {
private Integer id;
// 支付流水号
private String payNo;
// 订单流水号
private String orderNo;
// 用户账号ID
private Integer userId;
// 交易金额
private BigDecimal amount;
}
创建对应的业务类和Controller层,然后使用Postman工具进行测试。
在项目中整合Swagger3
在创建项目时,我们已经引入了swagger的依赖。
常用注解:
- @Tag,标注位置:controller类,作用:标识controller
- @Parameter,标注位置:参数,作用:标识参数
- @Parameters,标注位置:参数,作用:参数多重说明
- @Schema,标注位置:model层的JavaBean,作用:描述模型作用及每个属性
- @Operation,标注位置:方法,作用:描述方法作用
- @ApiResponse,标注位置:方法,作用描述响应状态码等
示例:
要启用Swagger3,要创建含分组迭代的Config配置类,在config包下,创建Swagger3Config
类,进行配置。
@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 OpenAPI docsOpenApi()
{
return new OpenAPI()
.info(new Info().title("cloudStudy")
.description("通用设计rest")
.version("v1.0"))
.externalDocs(new ExternalDocumentation()
.description("www.codewei.cn")
.url("https://yiyan.baidu.com/"));
}
}
访问http://localhost:8001/swagger-ui/index.html即可进入项目的swagger页面
2.4 项目完善
时间格式化
两种处理方法:
- 方法一:在相应的属性上使用
@JsonFormat
注解
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date createTime;
- 方法二:在SpringBoot项目的application.yaml中进行配置
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
统一返回结果格式
为了让前端接受到的结果为一个固定格式的结果,所以我们要对返回值进行统一。
定义返回标准格式,3大标配:
- code状态值:由后端统一定义各种返回结果的状态码
- message描述:本次结果调用的结果描述
- data数据:本次返回的数据
扩展值:
- timestamp:接口调用时间
实现步骤
1.新建枚举类ReturnCodeEnum
-
要与HTTP请求返回状态码相对应
@Getter
public enum ReturnCodeEnum
{
/**操作失败**/
RC999("999","操作XXX失败"),
/**操作成功**/
RC200("200","success"),
/**服务降级**/
RC201("201","服务开启降级保护,请稍后再试!"),
/**热点参数限流**/
RC202("202","热点参数限流,请稍后再试!"),
/**系统规则不满足**/
RC203("203","系统规则不满足要求,请稍后再试!"),
/**授权规则不通过**/
RC204("204","授权规则不通过,请稍后再试!"),
/**access_denied**/
RC403("403","无访问权限,请联系管理员授予权限"),
/**access_denied**/
RC401("401","匿名用户访问无权限资源时的异常"),
RC404("404","404页面找不到的异常"),
/**服务异常**/
RC500("500","系统异常,请稍后重试"),
RC375("375","数学运算异常,请稍后重试"),
INVALID_TOKEN("2001","访问令牌不合法"),
ACCESS_DENIED("2003","没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED("1001","客户端认证失败"),
USERNAME_OR_PASSWORD_ERROR("1002","用户名或密码错误"),
BUSINESS_ERROR("1004","业务逻辑异常"),
UNSUPPORTED_GRANT_TYPE("1003", "不支持的认证模式");
// 定义枚举类:举值-构造-遍历
/**自定义状态码**/
private final String code;
/**自定义描述**/
private final String message;
ReturnCodeEnum(String code, String message){
this.code = code;
this.message = message;
}
}
2.新建统一返回对象ResultData<T>
@Data
@Accessors(chain = true) // 链式编程
public class ResultData<T> {
private String code;/** 结果状态 ,具体状态码参见枚举类ReturnCodeEnum.java*/
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。
全局异常处理
我们系统可能会出现很多异常,不能直接将报错信息展示给用户,所以就需要全局的异常处理。
当然也可以使用try...catch...
进行处理,但是不建议这么做了
新建全局异常处理类GlobalExceptionHandler
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(Exception e){
log.error("全局异常信息:{}",e.getMessage(),e);
return ResultData.fail(ReturnCodeEnum.RC500.getCode(), e.getMessage());
}
}
这样就可自动捕获全局的异常信息了。
2.5 订单模块编码
新建cloud-consumer-order80子工程
,为微服务调用者订单模块
pom文件导入依赖
<dependencies>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
编写yaml配置文件
server:
port: 80
spring:
application:
name: cloud-consumer-order
新建注意启动类,命名为Main80
@SpringBootApplication
public class Main80 {
public static void main(String[] args) {
SpringApplication.run(Main80.class);
}
}
那么订单微服务如何调用到支付微服务呢?
这时候就要借助到RestTemplate
此外,订单模块调用微服务模块时应该使用PayDTO
数据类进行传递数据,避免暴露敏感数据。
RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷访问restful服务模板类,是spring提供的用于访问Rest服务的客户端模板工具集。
官方文档:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html
RestTemplate常用API
(url,requestMap,ResponseBean.class)
这三个参数分别代表REST请求地址,请求参数,HTTP响应转换被转换成的对象。
getForEntity
与getForObject
的不同:
getForEntity
:返回对象为ResponseEntity
对象,包含了一些响应中的一些重要信息,比如响应头、响应状态码、响应体等。getForObject
:返回对象为响应体中数据转化成的对象,基本可以理解为Json。
一般我们会对RestTemplate设置一个配置类,将其放入Spring容器中,方便使用。
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
编写订单模块的Controller,并将在支付模块中定义好的返回结果类拷贝到该子工程中来(正确应该放置在一个common通用子工程中)
@RequestMapping("/consumer")
@RestController
@Slf4j
@Tag(name="订单模块",description = "订单模块")
public class OrderController {
@Resource
private RestTemplate restTemplate;
private static final String PaymentSer_URL = "http://localhost:8001";
@GetMapping("/pay/add")
public ResultData addOrder(PayDTO payDTO){
return restTemplate.postForObject(PaymentSer_URL + "/pay/add", payDTO, ResultData.class);
}
@GetMapping("/pay/delete/{id}")
public ResultData deleteOrder(@PathVariable("id") Integer id){
return restTemplate.getForObject(PaymentSer_URL + "/pay/delete/"+id,ResultData.class);
}
@GetMapping("/pay/update")
public ResultData updateOrder(PayDTO payDTO){
return restTemplate.postForObject(PaymentSer_URL + "/pay/update", payDTO, ResultData.class);
}
@GetMapping("/pay/getById/{id}")
public ResultData getPayInfoById(@PathVariable("id") Integer id){
return restTemplate.getForObject(PaymentSer_URL + "/pay/getById/"+id, ResultData.class);
}
@GetMapping("/pay/getAll")
public ResultData getPayInfoAll(){
return restTemplate.getForObject(PaymentSer_URL + "/pay/getAll", ResultData.class);
}
}
启动两个微服务,然后进行测试。
2.6 工程重构
因为在支付和订单模块中存在着重复的代码,如数据传输对象PayDTO
以及结果返回对象。所以需要对项目进行重构,以减少代码冗余。
新建子工程cloud-api-commons
,用来对外暴露通用的组件/api/接口/工具类等
引入依赖
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
数据传输对象PayDTO
以及结果返回对象复制到该子工程中,并从其他子工程中删除。
全局异常处理类也可以放在通用子工程中,酌情!可放入也可以不放入。在这里我们也将其放入通用子工程中。
在其他工程中的pom.xml
中引入这个通用子工程。
<dependency>
<groupId>cn.codewei</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
然后,执行cloud-api-commons
中maven的clean
和install
在前面,我们将private static final String PaymentSer_URL = "http://localhost:8001";
服务的请求地址写死在了代码里面。这样会存在非常多的问题。
- 如果订单微服务和支付微服务的IP地址或者端口号发生了改变,则支付微服务将变得不可用。需要同步修改订单微服务中调用支付微服务的IP地址和端口号。
- 如果系统中提供了多个订单微服务和支付微服务,则无法实现微服务的负载均衡功能。
- 如果系统需要支持更高的并发,需要部署更多的订单微服务和支付微服务,硬编码订单微服务则后续的维护变得异常复杂。
所以,在微服务开发的过程中,需要引入服务治理功能,实现微服务之间的动态注册与发现。
从下面开始,正式进入SpringCloud!
3. consul服务注册与发现
3.1 consul简介
官网:https://www.consul.io/
HashiCorp Consul 是一种服务网络解决方案,使团队能够管理服务之间以及跨本地和多云环境和运行时的安全网络连接。 Consul 提供服务发现、服务网格、流量管理和网络基础设施设备的自动更新。我们可以在单个 Consul 部署中单独或一起使用这些功能。
3.2 consul下载安装
下载地址:https://developer.hashicorp.com/consul/install?product_intent=consul
下载后,得到一个consul的可执行文件,使用命令行窗口进入到该文件同一级别目录,使用consul --version
命令查看consul的版本信息,如果可以正确输出,那么consul的版本正确。
在该目录下,如下命令,启动consul
consul agent -dev
启动consul后,访问http://localhost:8500
即可访问consul的首页
3.3 微服务入驻
修改支付子工程的pom.xml
文件,引入consul
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
修改支付子工程的yaml配置文件,加入如下内容
spring:
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
修改主启动类,开启服务发现,添加注解@EnableDiscoveryClient
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("cn.codewei.mapper")
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class,args);
}
}
访问http://localhost:8500
,可以看到我们的服务已经成功注册
目前版本,springclou 4.1.0
可以不需要使用@EnableDiscoveryClient
也可以成功注册。
3.4 order订单微服务入驻
使用同样方法,我们也将订单微服务模块进行入驻
yaml配置文件中有略微不同,使用prefer-ip-address: true
来设定优先使用服务ip进行注册
spring:
application:
name: cloud-consumer-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
prefer-ip-address: true # 优先使用服务ip进行注册
注册完成后,修改Controller中前面写死的服务地址
将
private static final String PaymentSer_URL = "http://localhost:8001";
改为
private static final String PaymentSer_URL = "http://cloud-payment-service";
因为,我们通过使用RestTemplate
进行服务调用,并使用cloud-payment-service
名字进行调用服务时,这个名字可能会对应很多服务,所以我们就要用到负载均衡。
在我们之前定义的RestTemplateConfig类中的restTemplate方法上增加注解@LoadBalanced,开启负载均衡
@Configuration
public class RestTemplateConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
测试
访问http://localhost:80/consumer/pay/getAll
,可以通过订单微服务向支付微服务发送请求获取数据,然后结果返回到页面正常显示。
3.5 其他注册中心的对比
经典面试题!
要从CAP三个方面进行论述。
CAP:
- C:Consistency(强一致性)
- A:Availability(可用性)
- P:Partition tolerance(分区容错性)
经典CAP图:
最多只能同时较好的满足两个。
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
-
CA:单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
-
CP:满足一致性,分区容忍性的系统,通常性能不是特别高。
-
AP:满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
分布式微服务注册中心对比
组件名 | 语言 | CAP | 服务健康检查 | 对外暴露接口 | SpringCloud集成 |
---|---|---|---|---|---|
Eureka | Java | AP | 可配支持 | HTTP | 已集成 |
Consul | Go | CP | 支持 | HTTP/DNS | 已集成 |
Zookeeper | Java | CP | 支持 | 客户端 | 已集成 |
AP架构
当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。
当数据出现不一致时,虽然A, B上的注册信息不完全相同,但每个Eureka节点依然能够正常对外提供服务,这会出现查询服务信息时如果请求A查不到,但请求B就能查到。如此保证了可用性但牺牲了一致性结论:违背了一致性C的要求,只满足可用性和分区容错,即AP。
CP架构
当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性,Consul 遵循CAP原理中的CP原则,保证了强一致性和分区容错性,且使用的是Raft算法,比zookeeper使用的Paxos算法更加简单。虽然保证了强一致性,但是可用性就相应下降了,例如服务注册的时间会稍长一些,因为 Consul 的 raft 协议要求必须过半数的节点都写入成功才认为注册成功 ;在leader挂掉了之后,重新选举出leader之前会导致Consul 服务不可用。结论:违背了可用性A的要求,只满足一致性和分区容错,即CP。
3.6 分布式配置
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。比如某些配置文件中的内容大部分都是相同的,只有个别的配置项不同。就拿数据库配置来说吧,如果每个微服务使用的技术栈都是相同的,则每个微服务中关于数据库的配置几乎都是相同的,有时候主机迁移了,我希望一次修改,处处生效。
当下我们每一个微服务自己带着一个application.yml,上百个配置文件的管理。
需求:
- 通用全局配置信息,直接注册进Consul服务器,从Consul获取
- 既然从Consul获取自然要遵守Consul的配置规则要求
步骤:
在cloud-provider-payment8001
进行修改
修改pom.xml,新增依赖
<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>
注意:
如果设置了
spring.cloud.bootstrap.enabled=true
或spring.config.use-legacy-processing=true
或 包含spring-cloud-starter-bootstrap
,则需要配置放入bootstrap.yml
而不是 中application.yml
。
bootstrap.yaml配置文件
新增配置文件bootstrap.yaml
bootstrap.yaml是什么?
-
applicaiton.yml是用户级的资源配置项
-
bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的Application Context
的父上下文。初始化的时候,Bootstrap Context
负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment
。
Bootstrap
属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap context
和Application Context
有着不同的约定,所以新增了一个bootstrap.yml
文件,保证Bootstrap Context
和Application Context
配置的分离。
application.yml文件改为bootstrap.yml,这是很关键的或者两者共存。
因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml。
也就是,归服务器管的配置写在bootstrap.yaml中,主机工程服务的配置写在application.yaml中。
新建编写 boostrap.yaml,将application.yaml中的部分配置写入到boostrap.yaml中
spring:
application:
name: cloud-payment-service
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
profile-separator: '-' # default value is ",",we update '-'
format: YAML
修改application.yaml配置文件
删除掉移动至bootstap.yaml
中的相关配置
server:
port: 8001
spring:
profiles:
active: dev # 多环境配置加载内容dev/prod,不写就是默认default配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/cloudstudy?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 密码
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.codewei.entities
configuration:
map-underscore-to-camel-case: true
Consul服务器key/lvalue配置填写
参考规则:
# config/工程应用名/data
config/cloud-payment-service/data
/cloud-payment-service-dev/data
/cloud-payment-service-prod/data
1.创建config文件夹,以/
结尾
2.config文件夹下分别创建其他3个文件夹,以/结尾
cloud-payment-service
cloud-payment=service-dev
cloud-payment-service-prod
3.上述3个文件夹下分别创建data
内容,data
不再是文件夹
同样,建立其他几个data文件
修改controller,进行测试,添加如下内容
@Value("${server.port}")
private String port;
@GetMapping("/getRemoteConfig")
public ResultData<String> getRemoteConfig(@Value("${cn.codewei}") String codeweiStr){
return ResultData.success(codeweiStr + port);
}
启动8001服务进行测试,访问http://localhost:8001/pay/getRemoteConfig
可以看到,我们定义的配置已经获取到了。
3.7 动态刷新
如果consul上的配置变更了,我们希望本地项目读取到的配置也随之进行变更。
现在,我们改动consul中的配置文件,但是刷新页面,页面的返回结果信息还是没有发生改变的。这时,我们就需要配置动态刷新。
步骤:
1.在主启动类上添加@RefreshScop
注解
@EnableDiscoveryClient
@SpringBootApplication
@RefreshScope
@MapperScan("cn.codewei.mapper")
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class,args);
}
}
2.修改bootstrap.yaml
,可以不修改,新增watch: wait-time: 50
,将监控的间隔时间设置为50秒,默认为55秒。
spring:
application:
name: cloud-payment-service
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
profile-separator: '-' # default value is ",",we update '-'
format: YAML
watch:
wait-time: 30
此时进行测试,修改consul中的对应的配置文件,只需要等待30秒,就可以从网页上看到读取到的配置信息已经发生变化!
这样配置文件的动态刷新就配置好了。
注意:
当consul关闭重启时,我们之前配置的key-value信息全部都会清空!
所以,在这时,我们就需要对consul配置进行持久化操作。
3.8 Consul数据持久化配置
现在,我们将Consul重启后,之前配置的KV值就会丢失。
我们要解决这个问题,实现Consul关机重启后,我们的配置还能够存在。
持久化:要么存进数据库,要么保存到某个文件。
在这里,我们存储到文件中。
- 在
consul
到目录下新建一个文件夹data
,之后我们写进consul到KV键值对的数据将会保存至这个目录中。 - 新建文件
consul_start.sh
文件
echo "Service Starting"
nohup /Users/shihongwei/myfile/environment/consul-1.18.1/consul agent -server -ui -bind=127.0.0.1 -client=0.0.0.0 -bootstrap-expect 1 -data-dir /Users/shihongwei/myfile/environment/consul-1.18.1/data > /dev/null 2>&1 &
echo "Consul Start Success"
- 新建
consul_stop.sh
文件
kill $(ps -e | grep "consul" | awk '{print $1}')
echo $(ps -e | grep "consul" | awk '{print $1}') \n $(ps -e | grep "consul" | awk '{print $4}')
echo "stop success"
编写这两个文件,用来方便我们启动和停止Consul
。
在启动consul时,我们指定了data
目录,实现了数据的持久化。
启动consul,重新进行key/value的配置。然后重启Consul。发现我们刚刚配置的key/value还存在。
删除consul中的某个服务
有时某个服务已经不可用了但是依旧存在于Consul中,导致调用服务时,出现调用失败的情况,这时可以手动删除该服务。
发送请求:
PUT: http://ip:port/v1/agent/service/deregister/实例id
4. LoadBalancer负载均衡服务调用
LoadBalancer
是存在于Spring-Cloud-Commonos
中。
4.1 简介
官网:https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/loadbalancer.html
LB负载均衡(Load Balance)是什么
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用),常见的负载均衡有软件Nginx,LVS,硬件 F5等
spring-cloud-starter-loadbalancer组件是什么
Spring Cloud LoadBalancer是由SpringCloud官方提供的一个开源的、简单易用的客户端负载均衡器,它包含在SpringCloud-commons中用它来替换了以前的Ribbon组件。相比较于Ribbon,SpringCloud LoadBalancer不仅能够支持RestTemplate,还支持WebClient(WeClient是Spring Web Flux中提供的功能,可以实现响应式异步请求)
面试题:客户端负载和服务器端负载区别是什么?
-
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的。
-
loadbalancer本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
4.2 负载均衡实现
架构说明:
要求80服务通过轮询负载访问8001/8002/8003
LoadBalancer 在工作时分成两步:
-
先选择ConsulServer从服务端查询并拉取服务列表,知道了它有多个服务(上图3个服务),这3个实现是完全一样的,默认轮询调用谁都可以正常执行。类似生活中求医挂号,某个科室今日出诊的全部医生,客户端你自己选一个。
-
按照指定的负载均衡策略从server取到的服务注册列表中由客户端自己选择一个地址,所以LoadBalancer是一个客户端的负载均衡器。
1.在80项目中的RestTemplate中的restTemplate方法加上@LoadBalanced注解
在前面学习consul对服务注册与发现时,我们已经加上了这个注解。
2.新建微服务子工程8002
所有功能实现全部与8001一致。注意配置文件中的端口号区别,改为8001,另外,应用名称与8001保持一致。
3.启动consul,并启动8001和8002两个子工程,成功入驻到consul中
4.修改80子工程的pom文件,新增loadbalancer组件
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
5.修改controller,来验证我们是请求的哪个服务,并启动80,添加如下方法
@GetMapping("/pay/getRemoteConfig")
public ResultData getConfigByConsul(){
return restTemplate.getForObject(PaymentSer_URL + "/pay/getRemoteConfig", ResultData.class);
}
6.请求http://localhost:80/consumer/pay/getRemoteConfig
进行测试
可以发现,我们请求的服务在8001和8002之间来回切换
这样就实现了负载均衡!
了解:
负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。
4.3 负载均衡算法切换
提供了两种负载均衡算法:随机、轮询。
默认算法:轮询算法
public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer // 轮询
public class RandomLoadBalancer implements ReactorServiceInstanceLoadBalancer // 随机
当然,可以通过实现ReactorServiceInstanceLoadBalancer
接口,自定义负载均衡算法。
默认为轮询算法,那么我们如何实现将算法切换为随机算法呢?
在RestTemplate.class
中作出修改
@Configuration
@LoadBalancerClient(value = "cloud-payment-service",configuration = RestTemplateConfig.class)
public class RestTemplateConfig {
@LoadBalanced
@Bean
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);
}
}
这样就成功将负载均衡算法切换为随机算法了。
5. OpenFeign服务调用
5.1 介绍
官网:https://docs.spring.io/spring-cloud-openfeign/reference/spring-cloud-openfeign.html
Feign是一个声明式 Web 服务客户端。它使编写 Web 服务客户端变得更加容易。要使用 Feign 创建一个接口并对其进行注释。它具有可插入的注释支持,包括 Feign 注释和 JAX-RS 注释。 Feign 还支持可插入的编码器和解码器。 Spring Cloud 添加了对 Spring MVC 注释以及使用HttpMessageConverters
Spring Web 中默认使用的注释的支持。 Spring Cloud集成了Eureka、Spring Cloud CircuitBreaker以及Spring Cloud LoadBalancer,在使用Feign时提供负载均衡的http客户端。
OpenFeign
就是一个声明式的Web服务客户端- 只需要创建一个Rest接口并在该接口上添加
@FeignClient
注解即可OpenFeign
基本上就是当前微服务之间调用的事实标准
OpenFeign能做什么?
- 可插拔的直接支持,包括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只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。
5.2 通用步骤
微服务API接口+@FeignClient
注解
步骤:
1.新建子工程cloud-consumer-feign-order80
2.拷贝cloud-consumer-order80
的pom依赖文件,作出修改,删除banlancer
的依赖,增加oepnfeign
的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.编写yaml配置文件
server:
port: 80
spring:
application:
name: cloud-consumer-feign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
prefer-ip-address: true # 优先使用服务ip进行注册
4.将主启动类名称修改为MainFeign80
在主启动类上配置@EnableFeignClients
表示开启OpenFeign功能并激活
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class MainFeign80 {
public static void main(String[] args) {
SpringApplication.run(MainFeign80.class);
}
}
5.修改cloud-api-commons
通用模块
openfeign请求接口可能不止一个子工程用到,所以我们将其放在通用模块中
-
首先引入openfeign依赖
-
新建服务接口
PayFeignApi
,并加上@FeignClient
注解,注意@FeignClient
中的名称要和consul中服务注册的名称保持一致!其他方法的访问地址要与要访问的服务中定义的一致。方法名称可以不一致,但是一般为了便于对应都会保持一致
@FeignClient("cloud-payment-service")
public interface PayFeignApi {
@PostMapping("/pay/add")
public ResultData<String> addPay(@RequestBody PayDTO payDTO);
@GetMapping("/pay/delete/{id}")
public ResultData<String> delete(@PathVariable("id") Integer id);
@PostMapping("/pay/update")
public ResultData<String> updatePay(@RequestBody PayDTO payDTO);
@GetMapping("/pay/getById/{id}")
public ResultData<PayDTO> getById(@PathVariable("id") Integer id);
@GetMapping("/pay/getAll")
public ResultData<List<PayDTO>> getAll();
@GetMapping("/pay/getRemoteConfig")
public ResultData<String> getConfigInfo();
}
6.在cloud-consumer-feign-order80
中新建OrderController
,不再使用RestTemplate
进行接口的调用。
@RequestMapping("/feignconsumer")
@RestController
@Slf4j
@Tag(name="订单模块",description = "订单模块")
public class OrderController {
@Resource
private PayFeignApi payFeignApi;
@GetMapping("/pay/add")
public ResultData addOrder(PayDTO payDTO){
return payFeignApi.addPay(payDTO);
}
@GetMapping("/pay/delete/{id}")
public ResultData deleteOrder(@PathVariable("id") Integer id){
return payFeignApi.delete(id);
}
@GetMapping("/pay/update")
public ResultData updateOrder(PayDTO payDTO){
return payFeignApi.updatePay(payDTO);
}
@GetMapping("/pay/getById/{id}")
public ResultData getPayInfoById(@PathVariable("id") Integer id){
return payFeignApi.getById(id);
}
@GetMapping("/pay/getAll")
public ResultData getPayInfoAll(){
return payFeignApi.getAll();
}
@GetMapping("/pay/getRemoteConfig")
public ResultData getConfigByConsul(){
return payFeignApi.getConfigInfo();
}
}
启动consul,8001,8002以及feignconsumer80服务进行访问测试。成功访问!并且负载均衡也已经生效!
5.3 超时控制
在整个过程中,微服务提供者可能因为网络阻塞,网络抖动等,微服务消费者通过OpenFeign调用微服务提供者的时候,可能存在超时的可能。
在Spring Cloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会有多大问题的,但是如果是业务比较复杂,服务要进行比较繁杂的业务计算,那后台很有可能会出现Read Timeout这个异常,因此定制化配置超时时间就有必要了。
为了让项目出现超时现象,我们在项目中作出修改,使项目出现超时现象
写Bug,出现超时现象
1.服务提供方8001
,使用sleep
睡眠70秒。
@GetMapping("/getAll")
@Operation(summary = "查询所有流水",description = "查询所有流水方法")
public ResultData<List<Pay>> getAll(){
try {
TimeUnit.SECONDS.sleep(70); // 程序暂停
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
List<Pay> all = payService.getAll();
return ResultData.success(all);
}
2.服务调用方cloud-consumer-feign-order80
写好捕捉超时异常
@GetMapping("/pay/getAll")
public ResultData getPayInfoAll(){
ResultData<List<PayDTO>> all = null;
try {
System.out.println("调用开始:" + DateUtil.now());
all = payFeignApi.getAll();
} catch (Exception e) {
e.printStackTrace();
System.out.println("调用结束:" + DateUtil.now());
return ResultData.fail(ReturnCodeEnum.RC500.getCode(), e.getMessage());
}
return all;
}
启动服务,访问http://localhost:80/feignconsumer/pay/getAll
访问进行测试。
错误页面:
调用开始:2024-04-17 13:37:20
调用结束:2024-04-17 13:38:20
通过调用时间,我们可以发现,OpenFeign
的默认超时时间为60秒。
默认OpenFeign客户端等待60秒钟,但是服务端处理超过规定时间会导致Feign客户端返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制,默认60秒太长或者业务时间太短都不好
# yml文件中开启配置:
- connectTimeout 连接超时时间
- readTimeout 请求处理超时时间
修改cloud-consumer-fegin-order80
实现超时控制
方式1:通过全局配置
server:
port: 80
spring:
application:
name: cloud-consumer-feign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
prefer-ip-address: true # 优先使用服务ip进行注册
openfeign: # 配置超时
client:
config:
default:
read-timeout: 15000 # 15s
connect-timeout: 10000 # 10s
方式2:通过单个服务指定配置
将配置中的default
改为我们在FeignApi中注解中写的服务的名称
如:
@FeignClient("cloud-payment-service")
我们在cloud-consumer-fegin-order80
的yaml中配置:
server:
port: 80
spring:
application:
name: cloud-consumer-feign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
prefer-ip-address: true # 优先使用服务ip进行注册
openfeign: # 配置超时
client:
config:
cloud-payment-service: # 改为服务的名称
read-timeout: 15000 # 15s
connect-timeout: 10000 # 10s
注意:
如果全局的配置和指定服务的配置同时存在,指定服务名称的特定配置的优先级会更高!
5.4 重试机制
为了保证请求每次尽量不要空手而归。
默认重试机制是关闭的。
开启重试机制Retryer
在cloud-consumer-fegin-order80
中新增配置类FeignConfig
并修改Retryer
配置
@Configuration
public class FeignConfig {
@Bean
public Retryer retryer(){
// 最大请求次数:3,初始间隔时间:100ms,重试最大间隔时间:1s
return new Retryer.Default(100,1,3);
}
}
这样,重试机制就开启了。
测试发现:
8001工程中加入了睡眠,在8002工作中没有加入睡眠,我们开启重试机制后,当请求8001失败时,会去请求8002服务,会导致请求成功。
5.5 性能优化HttpClient5
OpenFeign中http client
如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求,
由于默认HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最强的。
步骤:修改cloud-consumer-fegin-order80
工程
- FeignConfig类里面的Retryer属性修改为默认,为了在我们测试时等待过长时间
@Configuration
public class FeignConfig {
@Bean
public Retryer retryer(){
return Retryer.NEVER_RETRY; // 不进行重试
// return new Retryer.Default(100,1,3);
}
}
- POM文件修改,引入新的依赖,注意版本的对应
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>
- yaml配置文件修改
server:
port: 80
spring:
application:
name: cloud-consumer-feign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
prefer-ip-address: true # 优先使用服务ip进行注册
openfeign:
client:
config:
default:
read-timeout: 30000 # 30s
connect-timeout: 10000 # 10s
httpclient: # 使用httpclient5
hc5:
enabled: true
这样就配置好了,启动项目进行测试。访问http://localhost:80/feignconsumer/pay/getAll
因为,前面我们在8001
服务中写了睡眠,然后又设置了项目的超时时间,这里超时,所以报了错误,刚好我们可以从错误中看到我们所使用的为client5
,说明已经替换成功。
5.6 请求响应压缩
对请求和响应进行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-fegin-order80
的yaml中进行配置
server:
port: 80
spring:
application:
name: cloud-consumer-feign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
prefer-ip-address: true # 优先使用服务ip进行注册
openfeign:
client:
config:
default:
read-timeout: 30000 # 30s
connect-timeout: 10000 # 10s
httpclient:
hc5:
enabled: true
compression: # 配置请求与响应压缩
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048
response:
enabled: true
5.7 日志打印
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节,说白了就是对Feign接口的调用情况进行监控和输出。
日志级别:
- NONE:默认的,不显示任何日志
- BASIC:仅记录请求方法、URL、响应状态码及执行时间
- HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息
- FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据
步骤:
1.在cloud-consumer-fegin-order80
的FeignConfig
中配置日志Bean
@Configuration
public class FeignConfig {
@Bean
public Retryer retryer(){
return Retryer.NEVER_RETRY; // 不进行重试
// return new Retryer.Default(100,1,3);
}
// 配置日志
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
2.在cloud-consumer-fegin-order80
的yalm配置文件中,开启日志的Feign客户端
logging:
level:
cn:
codewei:
apis:
PayFeignApi: debug
配置规则:logging.level + 含有@FeignClient注解的完整带报名的类接口名 + debug
3.启动项目,查看日志
从日志中我们也可以看到,我们配置的请求和响应压缩gzip
已经生效。
6. Resilience4J服务熔断与降级
6.1 简介
分布式系统面临的问题:复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
解决: 有问题的节点,快速熔断(快速返回失败处理或者返回默认兜底数据【服务降级】)。“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。出故障了“保险丝”跳闸,别把整个家给烧了。
如何解决上述问题,避免整个系统大面积故障?
服务熔断、服务降级、服务限流、服务限时、服务预热、接近实时的监控、兜底的处理动作等。
服务熔断
类比保险丝,保险丝闭合状态(CLOSE)可以正常使用,当达到最大服务访问后,直接拒绝访问,跳闸限电(OPEN),此刻调用方法会接受服务降级处理并返回友好兜底提示。
服务降级
服务器忙,请稍后再试。不让客户端等待,并立刻返回一个友好提示,fallback。
服务限流
秒杀高并发等操作,严禁一窝蜂的进来拥挤,大家排队,一秒钟N个,有序进行。
6.2 Circuit Breaker
官网:https://docs.spring.io/spring-cloud-circuitbreaker/reference/
Spring Cloud CircuitBreaker 项目包含 Resilience4J 和 Spring Retry 的实现。
CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
CircuitBreaker只是一套规范和接口,落地实现者是Resilience4J
Resilience4J介绍
官网:https://resilience4j.readme.io/docs/getting-started
Resilience4j 提供高阶函数(装饰器),以通过断路器、速率限制器、重试或隔板增强任何功能接口、lambda 表达式或方法引用。您可以在任何函数式接口、lambda 表达式或方法引用上堆叠多个装饰器。优点是您可以选择您需要的装饰器,而没有其他选择。
核心模块:
- resilience4j-Circuitbreaker:断路
- resilience4j-ratelimiter:速率限制
- resilience4j-bulkhead: 舱壁
- resilience4j-retry:自动重试(同步和异步)
- resilience4j-cache:结果缓存
- resilience4j-timelimiter:超时处理
Resilience4J断路器原理和状态转换分析
通过有限状态机实现,具有三种正常状态:关闭CLOSED、开启OPEN 和 半开HALF_OPEN 以及两种特殊状态 禁用DISABLED 和 强制开启FORCED_OPEN。
6.3 案例实战
6.3.1 熔断(服务熔断+服务降级)
- 当熔断器关闭时,所有的请求都会通过熔断器。
- 如果失败率超过设定的阀值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝。
- 当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率。
- 如果失败率超过阀值,则变为打开状态,如果失败率低于阀值,则变为关闭状态。
- CircuitBreaker 使用滑动窗口来存储和聚合调用的结果。可以在基于计数的滑动窗口和基于时间的滑动窗口之间进行选择。
- 基于计数的滑动窗口聚合最后 N 次调用的结果。
- 基于时间的滑动窗口聚合了最后 N 秒的调用结果。
- 除此之外,熔断器还会有两种特殊状态:DISABLED(始终允许访问)和FORCED_OPEN(始终拒绝访问)。
- 这两个状态不会生成熔断器事件(除状态转换外),并且不会记录时间的成功或者失败。
- 退出这两个状态唯一方法是触发状态转换或者重制熔断器。
断路器所有配置参数参考
Config property | Default Value | Description |
---|---|---|
failureRateThreshold | 50 | 配置故障率阈值(以百分比为单位)。当故障率等于或大于阈值时,CircuitBreaker 转换为打开并开始短路呼叫。 |
slowCallRateThreshold | 100 | 配置百分比阈值。当呼叫持续时间大于阈值时,CircuitBreaker 认为呼叫为慢速呼叫。 当慢速呼叫的百分比等于或大于阈值时,CircuitBreaker 转换为打开状态并开始短路呼叫。 |
slowCallDurationThreshold | 60000 [ms] | 配置持续时间阈值,超过该阈值的呼叫将被视为慢速,并增加慢速呼叫的速率。 |
permittedNumberOfCalls InHalfOpenState | 10 | 配置CircuitBreaker半开时允许的呼叫数量。 |
maxWaitDurationInHalfOpenState | 0 [ms] | 配置最大等待持续时间,控制断路器在切换到打开状态之前可以保持半打开状态的最长时间。值 0 表示断路器将在 HalfOpen 状态下无限等待,直到所有允许的调用完成。 |
slidingWindowType | COUNT_BASED | 配置CircuitBreaker关闭时记录调用结果的滑动窗口类型。滑动窗口可以是基于计数的,也可以是基于时间的。如果滑动窗口是 COUNT_BASED,则记录并聚合最后的调用。 如果滑动窗口是TIME_BASED,则记录并聚合最后几秒的调用。默认是按次数。 |
slidingWindowSize | 100 | 配置CircuitBreaker关闭时用于记录调用结果的滑动窗口的大小。 |
minimumNumberOfCalls | 100 | 配置 CircuitBreaker 计算错误率或慢速调用率之前所需的最小调用数(每个滑动窗口周期)。例如,如果minimumNumberOfCalls 为10,则必须记录至少10 个呼叫,然后才能计算失败率。如果仅记录了 9 个呼叫,即使所有 9 个呼叫都失败,CircuitBreaker 也不会转换为打开状态。 |
waitDurationInOpenState | 60000 [ms] | 断路器从打开状态转换为半打开状态之前应等待的时间。 |
automaticTransition FromOpenToHalfOpenEnabled | false | 如果设置为 true,则意味着 CircuitBreaker 将自动从打开状态转换为半打开状态,并且不需要调用来触发转换。创建一个线程来监视所有 CircuitBreaker 实例,以便在 waitDurationInOpenState 通过后将它们转换为 HALF_OPEN。然而,如果设置为 false,则仅在进行调用时才会发生到 HALF_OPEN 的转换,即使在传递 waitDurationInOpenState 之后也是如此。这里的优点是没有线程监视所有CircuitBreakers的状态。 |
recordExceptions | empty | 记录为故障并因此增加故障率的异常列表。任何匹配或继承列表之一的异常都将被视为失败,除非通过 显式忽略。 如果您指定异常列表,则所有其他异常都算作成功,除非它们被 显式忽略。 |
ignoreExceptions | empty | 被忽略的异常列表,既不计为失败也不计为成功。任何匹配或继承列表之一的异常都不会算作失败或成功,即使异常是. |
recordFailurePredicate | throwable -> true By default all exceptions are recored as failures. | 一个自定义谓词,用于评估是否应将异常记录为失败。如果异常应视为失败,则谓词必须返回 true。如果异常应视为成功,则谓词必须返回 false,除非 显式忽略异常。 |
ignoreExceptionPredicate | throwable -> false By default no exception is ignored. | 自定义谓词,用于评估是否应忽略异常并且既不计为失败也不计为成功。如果应忽略异常,则谓词必须返回 true。如果异常应视为失败,则谓词必须返回 false。 |
主要参数说明:
-
failureRateThreshold:以百分比配置失败率峰值
-
slidingWindowType:断路器的类型,COUNT_BASED或者TIME_BASED。默认是COUNT_BASED。
-
slidingWindowSize:如果这里填写数字10,上面的失败峰率值配置为50,那么就意味着:
当类型为基于计数时,当请求次数10次(
slidingWindowSize
)中,有50%(failureRateThreshold
)的请求失败,就打开断路器。当类型为基于时间时,还需要额外配置两个参数
slowCallRateThreshold
和slowCallDurationThreshold
,表示在10秒(slidingWindowSize
)内100%(slowCallRateThreshold
)的请求超过N秒(slowCallDurationThreshold
),就打开断路器。 -
slowCallDurationThreshold:配置请求调用时间阈值,超过该阈值的请求将被视为慢调用。
-
slowCallRateThreshold:配置百分比阈值。当呼叫持续时间大于阈值时,CircuitBreaker 认为呼叫为慢速呼叫。 当慢速呼叫次数的百分比等于或大于阈值时,CircuitBreaker 转换为打开状态并开始短路呼叫。
-
permittedNumberOfCallsInHalfOpenState:配置在半开时允许的呼叫数量,如果故障或慢调用仍然高于阀值,断路器再次进入打开状态。
-
minimumNumberOfCalls:配置 CircuitBreaker 计算错误率或慢速调用率之前所需的最小调用数(每个滑动窗口周期)。例如,如果minimumNumberOfCalls 为10,则必须记录至少10 个呼叫,然后才能计算失败率。如果仅记录了 9 个呼叫,即使所有 9 个呼叫都失败,CircuitBreaker 也不会转换为打开状态。
-
waitDurationInOpenState:断路器从打开状态转换为半打开状态之前应等待的时间。
熔断+降级案例需求说明
# 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
# 等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
# 如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
实战(按COUNT_BASED 计数的滑动窗口)
1.修改cloud-provider-payment8001
,新建PayCircuitController
@RestController
@RequestMapping("/pay")
public class PayCircuitController
{
@GetMapping(value = "/circuit/{id}")
public String myCircuit(@PathVariable("id") Integer id)
{
if(id == -4) throw new RuntimeException("----circuit id 不能负数");
if(id == 9999){
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
}
return "Hello, circuit! inputId: "+id+" \t " + IdUtil.simpleUUID();
}
}
2.修改PayFeignApi
接口,新增如下
@GetMapping("/pay/circuit/{id}")
public String myCircuit(@PathVariable("id") Integer id);
3.修改cloud-consumer-feign-order80
- 修改pom,新增依赖
<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>
<version>3.2.0</version>
</dependency>
-
修改yaml配置文件
首先将openfeign的连接超时时间和读取超时时间设置为20秒
read-timeout: 2000 # 20s
connect-timeout: 2000 # 20s
完整配置
server:
port: 80
spring:
application:
name: cloud-consumer-feign-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
prefer-ip-address: true # 优先使用服务ip进行注册
openfeign:
client:
config:
default:
read-timeout: 20000 # 20s
connect-timeout: 20000 # 20s
httpclient:
hc5:
enabled: true
compression: # 配置请求与响应压缩
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048
response:
enabled: true
# circuit breaker相关配置 (新增)
circuitbreaker:
enabled: true # 开启circuitbreaker
# 开启分组。没开分组的话永远不用分组配置,精确优先,分组次之,默认最后
group:
enabled: true
# (新增)
resilience4j:
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器。
slidingWindowType: COUNT_BASED # 滑动窗口的类型
slidingWindowSize: 6 #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
minimumNumberOfCalls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态。
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。
recordExceptions: # 出现一次如下异常就表示调用一次错误
- java.lang.Exception
instances: # 指定哪个服务使用哪个断路器配置
cloud-payment-service:
baseConfig: default
- 新建
OrderCircuitController
,使用@CircuitBreaker
注解
@RestController
public class OrderCircuitController
{
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/pay/circuit/{id}")
@CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
public String myCircuitBreaker(@PathVariable("id") Integer id)
{
return payFeignApi.myCircuit(id);
}
//myCircuitFallback就是服务降级后的兜底处理方法
public String myCircuitFallback(Integer id,Throwable t) {
// 这里是容错处理逻辑,返回备用结果
return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
}
4.启动8001
与feign80
进行测试
访问http://localhost/feign/pay/circuit/9999
实战(按TIME_BASED 时间的滑动窗口)
1.修改cloud-consumer-feign-order80
,修改YAML
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s # timelimiter默认限制远程1s,超过1s就超时异常,配置了降级,就会直接降级
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器。
slidingWindowType: TIME_BASED # 滑动窗口的类型
slidingWindowSize: 2 #滑动窗⼝的⼤⼩配置,配置TIME_BASED表示2秒
slow-call-duration-threshold:
seconds: 2s # 超过2秒着判定为慢调用
slow-call-rate-threshold: 30 # slidingWindowSize定义的时间内,超过30%的请求为慢调用,就打开断路器
minimumNumberOfCalls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态。
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数。
recordExceptions:
- java.lang.Exception
instances:
cloud-payment-service:
baseConfig: default
2.启动cloud-provider-payment8001
和cloud-consumer-feign-order80
服务。
3.访问http://localhost:80/feign/pay/circuit/123
,可以正常调用。
然后,访问http://localhost:80/feign/pay/circuit/9999
,此时虽然正常返回了结果,但是符合了慢调用的判定标准,所以被视为慢调用。
此时,我们开启3个窗口同时访问http://localhost:80/feign/pay/circuit/9999
,1个窗口访问http://localhost:80/feign/pay/circuit/123
。这时,将会有3个请求被判定为慢调用,达到了熔断的标准。此时再去访问http://localhost:80/feign/pay/circuit/123
就会看到服务降级所配置的。
等待5秒钟后,断路器到半开状态,判断服务是否正常了,如果正常则闭合断路器。
6.3.2 隔离 bulkhead
隔板来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船。
作用:用来限制对于下游服务的最大并发数量的限制。
Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量:信号量舱壁(SemaphoreBulkhead)、固定线程池舱壁(FixedThreadPoolBulkhead)。
信号量舱壁 SemaphoreBulkhead
基本上就是我们JUC信号灯内容的同样思想。
原理:
当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,
如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。
若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。
案例实战
1.cloud-provider-payment8001
中修改PayCircuitController
,新增如下方法
@GetMapping("/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id){
if(id == -4) throw new RuntimeException("----bulkhead id 不能负数");
if(id == 9999){
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
}
return "Hello, bulkhead! inputId: "+id+" \t " + IdUtil.simpleUUID();
}
2.PayFeignApi
接口新增舱壁api方法
@GetMapping("/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id);
3.修改cloud-consumer-feign-order80
- 引入依赖
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
- 修改yaml配置文件
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 20s # timelimiter默认限制远程1s,超过1s就超时异常,配置了降级,就会直接降级
bulkhead:
configs:
default:
max-concurrent-calls: 2 # 隔离允许并发线程执行的最大数量
max-wait-duration:
seconds: 1s # 当达到并发调用数量时,新的线程阻塞时间,等待1秒,如果无法访问服务,则进入fallback
instances: # 实例所使用的配置
cloud-payment-service:
base-config: default
- 修改
OrderCircuitController
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadFallback", type = Bulkhead.Type.SEMAPHORE)
public String myBulkhead(@PathVariable("id") Integer id)
{
return payFeignApi.myCircuit(id);
}
public String myBulkheadFallback(Integer id,Throwable t) {
return "myBulkheadFallback,舱位超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
4.测试
启动cloud-consumer-feign-order80
和cloud-provider-payment8001
服务。
此时,访问http://localhost/feign/pay/bulkhead/123
,可以正常调用。
但是,此时开启2个窗口同时访问http://localhost/feign/pay/bulkhead/9999
,1个窗口访问http://localhost/feign/pay/bulkhead/123
。先发起错误的请求,再发起正确的请求,可以发现,正确请求在等待1秒后,没有请求到服务,就会调用fallback。
等其中一个错误请求访问结束后,正确的请求再去访问,就会成功。
固定线程池舱壁 FixedThreadPoolBulkhead
固定线程池舱壁使用一个固定线程池和一个等待队列来实现舱壁。
当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。
当线程池中无空闲时时,接下来的请求将进入等待队列,若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
另外:固定线程池舱壁只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法
案例实战
1.修改cloud-consumer-feign-order80
- 引入依赖
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
- 修改yaml配置文件
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 20s
thread-pool-bulkhead:
configs:
default:
core-thread-pool-size: 1
max-thread-pool-size: 1
queue-capacity: 1 # max:1 + queue:1 = 2 当第3个请求来时,调用fallback
instances:
cloud-payment-service:
base-config: default
此外,注释掉
group:
enabled: true
- 修改
OrderCircuitController
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadPoolFallback", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<String> myBulkheadThreadPool(@PathVariable("id") Integer id) {
System.out.println(Thread.currentThread().getName()+"--------开始进入");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return CompletableFuture.supplyAsync(() -> payFeignApi.myCircuit(id)+"\t"+"Bulkhead.Type.THREADPOOL");
}
public CompletableFuture<String> myBulkheadPoolFallback(Integer id,Throwable t) {
//return "myBulkheadPoolFallback,舱位超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
return CompletableFuture.supplyAsync(() -> "myBulkheadPoolFallback,舱位超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
}
2.测试
启动cloud-consumer-feign-order80
和cloud-provider-payment8001
服务。
此时,访问http://localhost/feign/pay/bulkhead/123
,可以正常调用。
但是,此时开启3个窗口分别访问http://localhost/feign/pay/bulkhead/1
,http://localhost/feign/pay/bulkhead/2
和http://localhost/feign/pay/bulkhead/3
,可以发现,最后一个发送的请求,没有请求到服务,就会调用fallback。
6.3.3 限流 ratelimiter
限流就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。
比如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。
所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。
常见的限流算法:
-
漏斗算法(Leaky Bucket)
一个固定容量的漏桶,按照设定常量固定速率流出水滴,类似医院打吊针,不管你源头流量多大,我设定匀速流出。 如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。
缺点:这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
-
令牌桶算法(Token Bucket)
SpringCloud默认使用该算法。
-
滚动时间窗(tumbling time window)
允许固定数量的请求进入(比如1秒取4个数据相加,超过25值就over)超过数量就拒绝或者排队,等下一个时间段进入。
由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。
缺点:间隔临界的一段时间内的请求就会超过系统限制,可能导致系统被压垮,如
12:00-12:01
一分钟内允许100次请求,这100次请求可能全部在12:00:59
时访问服务,这是会压垮服务。 -
滑动时间窗(sliding time window)
顾名思义,该时间窗口是滑动的。所以,从概念上讲,这里有两个方面的概念需要理解:
窗口:需要定义窗口的大小
滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小
滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。
案例实战
修改cloud-provider-payment8001
微服务
- 修改
PayCircuitController
,新增myRatelimit
方法
@GetMapping("/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id){
return "Hello,myRatelimit! inputId: "+id+" \t " + IdUtil.simpleUUID();
}
修改cloud-api-common
PayFeignApi
接口新增限流api方法
@GetMapping("/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id);
修改cloud-consumer-feign-order80
微服务
- 修改pom,引入依赖
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
- 修改yaml配置文件
resilience4j:
ratelimiter:
configs:
default:
limit-for-period: 2 # 在一次周期内,允许执行的最大请求数
limit-refresh-period: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量置为limitForPeriod
timeout-duration: 1 # 线程等待权限的默认等待时间
instances:
cloud-payment-service:
base-config: default
- 修改
controller
@GetMapping("/feign/pay/ratelimit/{id}")
@RateLimiter(value = "cloud-payment-service",fallbackMethod = "myRateLimitFallBack" )
public String myRateLimit(@PathVariable("id") Integer id) {
return payFeignApi.myRatelimit(id);
}
public String myRateLimitFallBack(Integer id,Throwable t) {
return "你被限流了,禁止访问!";
}
测试
访问http://localhost/feign/pay/ratelimit/11
,正常访问。
但是,当我们快速点击刷新按钮,1秒超过两次,就会出现如下效果,说明限流已经生效。
6.4 总结
1.熔断
熔断是不关注、也不限制流量的,但是如果服务出错的概率达到了阈值、就直接拒绝访问。
2.壁舱
壁舱可以控制服务正确处理的请求的数量,比如服务能够处理请求的数量为 100 个,那么当服务已经有 100 个请求在处理了,新来的请求就得等到有先前的请求被处理完毕才能被处理。
3.限流
限流不关注服务正在处理的请求数量,只关注一段时间内服务能够接收并处理的请求数量,比如使用固定窗口算法限制服务 1 分钟能够接收 100 个请求,那么等到第 2 分钟,不管服务有没有处理完之前的请求,它此刻都可以再处理 100 个请求。
7. Micrometer分布式链路追踪
7.1 介绍
在如今的微服务过程中,分布式链路追踪必须要有,不可或缺。不然,对项目的后续优化、维护、升级带来非常多的麻烦,甚至没有相应的日志理论依据。
之前,对于分布式链路追踪使用的是Sleuth
。但是,目前Sleuth
也进入了维护模式。
Sleuth
改头换面,替代方案就是Micrometer Tracing
。
为什么需要分布式链路追踪?
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
在分布式与微服务场景下,我们需要解决如下问题:
- 在大规模分布式与微服务集群下,如何实时观测系统的整体调用链路情况。
- 在大规模分布式与微服务集群下,如何快速发现并定位到问题。
- 在大规模分布式与微服务集群下,如何尽可能精确的判断故障对系统的影响范围与影响程度。
- 在大规模分布式与微服务集群下,如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。
- 在大规模分布式与微服务集群下,如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。
- 在大规模分布式与微服务集群下,如何尽可能精确的分析系统的存储瓶颈与容量规划。
上述问题就是我们的落地议题答案:
分布式链路追踪技术要解决的问题,分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
zipkin是什么?
Spring Cloud Sleuth(micrometer)提供了一套完整的分布式链路追踪(Distributed Tracing)解决方案且兼容支持了zipkin展现。
也就是,zipkin是一个可视化的界面,用来展示链路追踪的数据信息。
小总结
将一次分布式请求还原成调用链路,进行日志记录和性能监控,并将一次分布式请求的调用情况集中web展示。
其他分布式链路追踪解决方案
- Cat
- ZipKin
- Pinpoint
- Skywalking
7.2 分布式链路追踪原理
假定3个微服务调用的链路:
假定三个微服务调用的链路如上所示:Service 1 调用 Service 2,Service 2 调用 Service 3 和 Service 4。
一条链路追踪会在每个服务调用的时候加上Trace ID
和 Span ID
。
链路通过TraceId唯一标识,Span标识发起的请求信息,各span通过parent id
关联起来 (Span:表示调用链路来源,通俗的理解span就是一次请求信息)。
一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来。
- 第一个节点:Span ID = A,Parent ID = null,Service 1 接收到请求。
- 第二个节点:Span ID = B,Parent ID= A,Service 1 发送请求到 Service 2 返回响应给Service 1 的过程。
- 第三个节点:Span ID = C,Parent ID= B,Service 2 的 中间解决过程。
- 第四个节点:Span ID = D,Parent ID= C,Service 2 发送请求到 Service 3 返回响应给Service 2 的过程。
- 第五个节点:Span ID = E,Parent ID= D,Service 3 的中间解决过程。
- 第六个节点:Span ID = F,Parent ID= C,Service 3 发送请求到 Service 4 返回响应给 Service 3 的过程。
- 第七个节点:Span ID = G,Parent ID= F,Service 4 的中间解决过程。
- 通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。
7.2 ZipKin
Zipkin是一种分布式链路跟踪系统图形化的工具,Zipkin 是 Twitter 开源的分布式跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调用链路信息展示到Web图形化界面上供开发人员分析,开发人员能够从ZipKin中分析出调用链路中的性能瓶颈,识别出存在问题的应用程序,进而定位问题和解决问题。
ZipKin:默认端口为9411,默认地址为:https://localhost:9411/
官网:https://zipkin.io/
进入官网后,下载ZipKin。
下载完成后,进入下载目录,执行:
java -jar zipkin-server-3.3.1-exec.jar
成功启动zipkin后,通过https://localhost:9411/zipkin
访问主页。
7.3 Micrometer+ZipKin搭建链路监控
Micrometer+ZipKin两者各自的分工:
- Micrometer:数据采样
- ZipKin:可视化展示
案例步骤
1.修改父工程POM文件,引入依赖
<micrometer-tracing.version>1.2.0</micrometer-tracing.version>
<micrometer-observation.version>1.12.0</micrometer-observation.version>
<feign-micrometer.version>12.5</feign-micrometer.version>
<zipkin-reporter-brave.version>2.17.0</zipkin-reporter-brave.version>
<!--micrometer-tracing-bom导入链路追踪版本中心 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bom</artifactId>
<version>${micrometer-tracing.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--micrometer-tracing指标追踪 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
<version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
<version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-observation 4-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
<version>${micrometer-observation.version}</version>
</dependency>
<!--feign-micrometer 5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
<version>${feign-micrometer.version}</version>
</dependency>
<!--zipkin-reporter-brave 6-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
<version>${zipkin-reporter-brave.version}</version>
</dependency>
由于Micrometer Tracing是一个门面工具自身并没有实现完整的链路追踪系统,具体的链路追踪另外需要引入的是第三方链路追踪系统的依赖:
依赖名称 | 描述 |
---|---|
micrometer-tracing-bom | 导入链路追踪版本中心,体系化说明 |
micrometer-tracing | 指标追踪 |
micrometer-tracing-bridge-brave | 一个Micrometer模块,用于与分布式跟踪工具 Brave 集成,以收集应用程序的分布式跟踪数据。Brave是一个开源的分布式跟踪工具,它可以帮助用户在分布式系统中跟踪请求的流转,它使用一种称为"跟踪上下文"的机制,将请求的跟踪信息存储在请求的头部,然后将请求传递给下一个服务。在整个请求链中,Brave会将每个服务处理请求的时间和其他信息存储到跟踪数据中,以便用户可以了解整个请求的路径和性能。 |
micrometer-observation | 一个基于度量库 Micrometer的观测模块,用于收集应用程序的度量数据。 |
feign-micrometer | 一个Feign HTTP客户端的Micrometer模块,用于收集客户端请求的度量数据。 |
zipkin-reporter-brave | 一个用于将 Brave 跟踪数据报告到Zipkin 跟踪系统的库。 |
补充包:
spring-boot-starter-actuator
是SpringBoot框架的一个模块用于监视和管理应用程序
2.修改cloud-provider-payment8001
微服务
一般是要在客户端侧引入依赖,进行配置,此时8001服务被80调用,但是,8001服务可能未来也会调用其他服务,所以也就进行了相应的配置。
- 引入依赖
<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 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<!--feign-micrometer -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
<!--zipkin-reporter-brave -->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
- 修改配置文件yaml
management:
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
- 新建
PayMicrometerController
@RestController
public class PayMicrometerController
{
@GetMapping(value = "/pay/micrometer/{id}")
public String myMicrometer(@PathVariable("id") Integer id)
{
return "Hello, 欢迎到来myMicrometer inputId: "+id+" \t 服务返回:" + IdUtil.simpleUUID();
}
}
3.修改cloud-api-commons
PayFeignApi
中新增Controller
@GetMapping("/pay/micrometer/{id}")
public String myMicrometer(@PathVariable("id") Integer id);
4.修改cloud-consumer-feign-order80
微服务
- 引入依赖
<!--micrometer-tracing指标追踪 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!--micrometer-observation 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<!--feign-micrometer 4-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
<!--zipkin-reporter-brave 5-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
- 修改配置文件yaml
management:
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
- 新建
OrderMicrometerController
@RestController
@Slf4j
public class OrderMicrometerController
{
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/micrometer/{id}")
public String myMicrometer(@PathVariable("id") Integer id)
{
return payFeignApi.myMicrometer(id);
}
}
测试
- 启动consul与zipkin
- 启动8001和feign-80两个服务
- 访问
http://localhost/feign/micrometer/1
接口 - 进入
http://localhost:9411
查看监控信息
点击右上角可以设置一些筛选条件。点击RUN QUERY
即可进行查询。
此外,可以选择某些服务进行筛选。
点击某一个请求右边的SHOW
按钮,即可查看调用链路的具体信息,如:
点击上方的依赖按钮,然后点击查询,即可查看到服务的依赖可视化图。
右方的饼图表示调用次数或者被调用次数。
8. GateWay服务网关
8.1 简介
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring6,Spring Boot 3和Project Reactor等技术。它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,并为它们提供跨领域的关注点,例如:安全性、监控/度量和恢复能力。
Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关;但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关SpringCloud Gateway替代Zuul,那就是SpringCloud Gateway一句话:gateway是原zuul1.x版的替代。
GateWay网关的功能:
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心。
8.2 三大核心
Route(路由)
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。
Predicate(断言)
参考的是Java8的java.util.function.Predicate
,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。
Filter(过滤)
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
总结
web前端请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是匹配条件。
filter就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
8.3 工作流程
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。
-
在
pre
类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。 -
在
post
类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
核心逻辑:路由转发+断言判断+执行过滤器链。
8.4 入门配置
1.新建Module,cloud-gateway9527
2.修改pom文件,引入依赖
网关是响应式编程,所以不需要引入web
依赖。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--用于检测系统的健康情况、当前的Beans、系统的缓存等-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.修改yaml配置文件
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
4.修改主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class Main9527 {
public static void main(String[] args) {
SpringApplication.run(Main9527.class,args);
}
}
5.测试
启动consul后,启动cloud-gateway9527
,查看是否成功入驻。
访问localhost:8500
,可以看到网关成功入驻。
8.5 路由映射
诉求:我们目前不想暴露8001端口,希望在8001真正的支付微服务外面套一层9527网关。
1.8001微服务新建PayGateWayController
@RestController
public class PayGateWayController
{
@Resource
PayService payService;
@GetMapping(value = "/pay/gateway/get/{id}")
public ResultData<Pay> getById(@PathVariable("id") Integer id)
{
Pay pay = payService.getById(id);
return ResultData.success(pay);
}
@GetMapping(value = "/pay/gateway/info")
public ResultData<String> getGatewayInfo()
{
return ResultData.success("gateway info test:"+ IdUtil.simpleUUID());
}
}
启动8001服务,此时可以正常访问http://localhost:8001/pay/gateway/get/1
和http://localhost:8001/pay/gateway/info
。
2.网关9527新增配置
gateway:
routes:
- id: pay_routh1 #pay_routh1 路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
3.测试1
启动consul。
启动8001服务和9527网关。
之前,我们通过直接访问8001的接口http://localhost:8001/pay/gateway/get/1
进行请求,此时,我们可以通过访问网关进行请求,http://localhost:9527/pay/gateway/get/1
,可以成功访问。
因为在配置文件中,我们配置了,如果匹配到
/pay/gateway/get/**
就会把我们的请求路由到http://localhost:8001
,即请求http://localhost:8001/pay/gateway/get/**
。
4.测试2
我们启动80订单微服务,它从Consul注册中心通过微服务名称找到8001支付微服务进行调用,80 → 9527 → 8001
要求访问9527网关后才能访问8001,如果我们此时启动80订单,可以做到吗?
修改cloud-api-commons
模块的PayFeignApi
@GetMapping(value = "/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Integer id);
@GetMapping(value = "/pay/gateway/info")
public ResultData<String> getGatewayInfo();
在cloud-consumer-feign-order80
服务中新增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.getById(id);
}
@GetMapping(value = "/feign/pay/gateway/info")
public ResultData<String> getGatewayInfo()
{
return payFeignApi.getGatewayInfo();
}
}
启动8001服务和feign80服务和9527网关。
访问http://localhost/feign/pay/gateway/get/1
和http://localhost/feign/pay/gateway/info
,正常!
此时,我们将网关9527停止,再访问上面两个接口,发现还是可以正常访问!说明,我们的请求并没有通过网关进行访问!
这是因为,在PayFeignApi
中,我们使用的是:
@FeignClient(value = "cloud-payment-service")
此时,我们还是直接访问的服务提供者,并没有通过网关进行访问。
如果要通过网关进行访问,则需要更改为:
@FeignClient(value = "cloud-gateway")
此时,再次访问接口,就是通过网关进行访问的。
正确做法:
- 同一家公司自己人,系统内环境,直接找微服务
- 不同家公司有外人,系统外访问,先找网关再服务
此时,还有一个问题,在9527的yaml配置文件中,我们将路由映射写死了!只要服务的端口或者地址一变化,那么网关就会出现问题了。所以,这是一个需要解决的问题。
8.6 Route以微服务名-动态获取服务URI
针对9527网关yaml配置文件中将路由映射写死的问题,我们需要更改为以服务名称懂带获取服务的URI进行调用。
解决路由映射写死的问题
修改9527网关yaml配置文件
gateway:
routes:
- id: pay_routh1 #pay_routh1 路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
重启9527网关,进行测试。
访问http://localhost/feign/pay/gateway/get/1
,可以成功访问!
此时,如果我们将8001服务的端口号更改为8007,再次访问http://localhost/feign/pay/gateway/get/1
也是可以成功访问的。因为此时是依据URI: lb://cloud-payment-service
进行查找服务的。
实验后,将8007改回8001。
8.7 Predicate断言
Spring Cloud Gateway将路由匹配作为Spring WebFlux HanlderMapping基础架构的一部分。
Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多个Route Predicate工厂可以进行组合。
Spring Cloud Gateway创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route。Spring Cloud Gateway包含许多内置的Route Predicate Factories。所有这些谓词都匹配HTTP请求的不同属性。多种谓词工厂可以组合,并通过逻辑and。
route(路由):解决要访问哪个服务的问题。
Predicate(断言):解决是否可以访问的问题。
启动微服务gatway9527
,查看日志输出。
整体架构
配置语法
共有两种配置方式,二选一:
- shortcuts(简洁)
- full expanded(全面)
Shortcuts配置样例
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Cookie=mycookie,mycookievalue
full expanded配置样例
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- name: Cookie
args:
name: mycookie
regexp: mycookievalue
我们一般使用shortcut方式
常用Predicate配置项
- After Route Predicate
- Before Route Predicate
- Between Route Predicate
- Cookie Route Predicate
- Header Route Predicate
- Host Route Predicate
- Path Route Predicate
- Query Route Predicate
- RemoteAddr route predicate
- Method Route Predicate
After Route Predicate
在指定的时间之后,才可以访问。使用ZonedDateTIme
时间。
如何获得ZoneDateTime
呢?
public class ZonedDateTimeDemo
{
public static void main(String[] args)
{
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
System.out.println(zbj);
}
}
yaml中配置
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- After=2024-05-26T17:20:13.586918800+08:00[Asia/Shanghai]
应用场景:比如京东18:00秒杀开始,那么就设置18:00后才可以访问该服务。
Before Route Predicate
在指定的时间之前,才可以访问。使用ZonedDateTIme
时间。
Between Route Predicate
在定义的两个时间之间,才可以访问。使用ZonedDateTIme
时间。
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Between=2024-05-26T17:20:13.586918800+08:00[Asia/Shanghai],2024-05-26T17:25:13.586918800+08:00[Asia/Shanghai]
Cookie Route Predicate
Cookie Route Predicate需要两个参数,一个是 Cookie name ,一个是正则表达式。
路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Cookie=username,codewei
- Cookie=age,xxx
如,上诉规则,我们定义的意思就是,当请求的cookie
为username=codewei
时才可以允许访问。
Header Route Predicate
和Cookie配置方式类似。两个参数:一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
- Header=xxxxx, xxxx
Host Route Predicate
接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。多个域名之间用英文逗号分隔。
它通过参数中的主机地址作为匹配规则。
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Host=**.codewei.cn,**.codewei.com
Path Route Predicate
进行访问地址的匹配,支持正则表达式。
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
Query Route Predicate
支持传入两个参数,一个是属性名,一个为属性值,属性值可以是正则表达式。根据请求参数进行匹配。
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Query=username, codewei # 要有参数名username并且值还要是code
- Query=age, \d+ # 要有参数名age并且值还要是整数才能路由
RemoteAddr route predicate
远程地址IP要符合。
很多项目中,如果涉及到ip的拦截和访问控制都会有这样的写法:192.168.31.1/24
,这种写法是一个网络标记的标准规范,这种写法称为无类别域间路由(CIDR)。
IPV4是有32位组成,意味着被分为4个八位字节。如00000000.00000000.00000000.00000000
,28 也就是256,也就是每个字节取值范围为0~255。
也就是,如:
192.168.31.1/24标识为192.168.31.0-255。解释就是前24位要保持一致,后面8位任由发挥。
192.168.31.2/30就是前30位要保持一致,后2位自由发挥,也就标识为:192.168.31.0-3。
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- RemoteAddr=192.168.124.1/24 # 外部访问我的IP限制,最大跨度不超过32,目前是1~24它们是 CIDR 表示法。
配置了该项后,不能再使用
localhost
进行访问,必须使用IP地址访问才可以。(注意,不是127.0.0.1,而是真实的IP)
Method Route Predicate
请求方法限制,如GET或者POST等。
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Method=GET,POST
8.8 自定义Predicate
如果原有的断言配置满足不了业务需求,就需要我们自定义断言Predicate。
在自定义时,我们要么继承AbstractRoutePredicateFactory
抽象类,要么实现RoutePredicateFactory
接口,开头任意取名,但是必须以RoutePredicateFactory
后缀结尾。
编写步骤
- 新建类名XXX需要以RoutePredicateFactory结尾并继承AbstractRoutePredicateFactory类。
- 重写apply方法
- 新建apply方法所需要的静态内部类MyRoutePredicateFactory.Config,这个Config类就是我们的路由断言规则,重要
- 空参构造方法,内部调用super
代码实现
@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config> {
public MyRoutePredicateFactory() {
super(MyRoutePredicateFactory.Config.class);
}
// 接收yaml中定义的配置项
@Validated
public static class Config{
@Getter
@Setter
@NotEmpty
private String userType; // 用户等级,青铜会员、银会员、黄金会员
}
// 判断是否符合定义的要求,符合则返回true,允许请求访问服务
@Override
public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");
if (userType==null) return false;
return userType.equals(config.getUserType());
}
};
}
}
在yaml中使用
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- My=gold
此时,启动项目,出现了问题!
但是,在项目启动时,我们定义的Predicate已经被加载了。
此时,我们改用Fully Expanded Arguments
方式进行尝试,修改yaml配置如下:
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- name: My
args:
userType: gold
此时,项目可以正常启动,且配置已经生效!
这就意味着,我们自定义的Predicate缺少shortcutFieldOrder方法的实现,所以不支持短格式。
对于MyRoutePredicateFactory
类进行修改,增加如下方法:
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("userType");
}
此时,我们在yaml中再使用shortcut方式进行配置,然后重启尝试,此时发现可以正常启动且配置生效!
8.9 Filter过滤
8.9.1 介绍
相当于SpringMVC里面的的拦截器Interceptor,Servlet的过滤器。
“pre”和 “post” 分别会在请求被执行前调用和被执行后调用,用来修改请求和响应信息。
注意:
如果定义了断言,比如
predicate: Header=name,codewei
。但是发起的请求中没有携带这个请求头。此时,我又定义了一个过滤器
AddRequesstHeader=name,codewei
。那这个请求会通过吗?答:
不会通过,断言是在过滤器之前评估的。
功能:
- 请求鉴权
- 异常处理
- 记录接口调用时常统计
类型
-
全局默认过滤器
Global Filters
gateway出厂默认已有的,直接用即可,主要作用于所有的路由。不需要在配置文件中配置,作用在所有的路由上,实现GlobalFilter接口即可。
-
单一内置过滤器
Gateway Filter
也可以称为网关过滤器,这种过滤器主要是作用于单一路由或者某个路由分组。
-
自定义过滤器
- Predicate决定是否将请求路由到某个服务。
- Filter对请求或响应进行处理和修改。
8.9.2 内置的过滤器
常见的内置过滤器
请求头(RequestHeader)相关组
-
The AddRequestHeader GatewayFilter Factory
用于添加请求头的内容。
在8001微服务PayGateWayController新增方法
@GetMapping(value = "/pay/gateway/filter") public ResultData<String> getGatewayFilter(HttpServletRequest request) { String result = ""; Enumeration<String> headers = request.getHeaderNames(); while(headers.hasMoreElements()) { String headName = headers.nextElement(); String headValue = request.getHeader(headName); System.out.println("请求头名: " + headName +"\t\t\t"+"请求头值: " + headValue); if(headName.equalsIgnoreCase("X-Request-codewei1") || headName.equalsIgnoreCase("X-Request-codewei2")) { result = result+headName + "\t " + headValue +" "; } } return ResultData.success("getGatewayFilter 过滤器 test: "+result+" \t "+ DateUtil.now()); }
9527网关YML添加过滤内容
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: - AddRequestHeader=X-Request-codewei1,codeweiValue1 - AddRequestHeader=X-Request-codewei2,codeweiValue2
启动9527和8001,访问
http://localhost:9527/pay/gateway/filter
。可以看到,请求头中存在了过滤器中添加的请求头。
-
The RemoveRequestHeader GatewayFilter Factory
根据请求头名称删除请求头。
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: - RemoveRequestHeader=sec-fetch-site
-
The SetRequestHeader GatewayFilter Factory
根据请求头名称修改请求头。
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: - SetRequestHeader=sec-fetch-mode, Blue-updatebyzzyy # 将请求头sec-fetch-mode对应的值修改为Blue-updatebyzzyy
请求参数(RequestParameter)相关组
-
The AddRequestParameter GatewayFilter Factory
新增请求参数
Parameter:k ,v
。gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: - AddRequestParameter=customerId,9527001 # 新增请求参数Parameter:k ,v
-
The RemoveRequestParameter GatewayFilter Factory
删除请求参数。
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: - RemoveRequestParameter=customerName # 删除url请求参数customerName,你传递过来也是null
响应头(ResponseHeader)相关组
-
The AddResponseHeader GatewayFilter Factory
新增响应头。
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: - AddResponseHeader=X-Response-atguigu, BlueResponse # 新增响应头参数X-Response-atguigu并设值为BlueResponse
-
The SetResponseHeader GatewayFilter Factory
修改响应头。
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: - SetResponseHeader=Date,2099-11-11 # 设置响应头Date值为2099-11-11
-
The RemoveResponseHeader GatewayFilter Factory
删除响应头。
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: - RemoveResponseHeader=Content-Type # 将默认自带Content-Type响应头属性删除
前缀和路径相关组
-
The PrefixPath GatewayFilter Factory
自动添加路径前缀。
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: # - Path=/pay/gateway/filter/** - Path=/gateway/filter/** # 断言,为配合PrefixPath测试过滤,暂时注释掉/pay filters: - PrefixPath=/pay # http://localhost:9527/pay/gateway/filter
-
The SetPath GatewayFilter Factory
修改访问路径。
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: # - Path=/pay/gateway/filter/** - Path=/XYZ/abc/{segment} # 断言,为配合SetPath测试,{segment}的内容最后被SetPath取代 filters: - SetPath=/pay/gateway/{segment} # {segment}表示占位符,你写abc也行但要上下一致
访问
http://localhost:9527/XYZ/abc/filter
会自动访问到http://localhost:8001/pay/gateway/filter
。 -
The RedirectTo GatewayFilter Factory
重定向到某个页面。
gateway: routes: - id: pay_routh3 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/filter/** filters: - RedirectTo=302, http://www.atguigu.com/ # 访问http://localhost:9527/pay/gateway/filter跳转到http://www.atguigu.com/
其它
-
Default Filters
配置在此处相当于全局通用,自定义秒变Global。
gateway: default-filters: - AddRequestHeader=X-Request-codewei1,codeweiValue1
8.9.3 自定义过滤器
自定义全局Filter
面试题:统计接口调用耗时情况,如何落地,谈谈设计思路
通过自定义全局过滤器搞定上述需求
在cloud-gateway9527
中,新建类MyGlobalFilter并实现GlobalFilter,Ordered两个接口。
@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered {
/**
* 数字越小优先级越高
*/
@Override
public int getOrder() {
return 0;
}
private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间
/**
* 各种统计
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//先记录下访问接口的开始时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
if (beginVisitTime != null) {
log.info("访问接口主机: " + exchange.getRequest().getURI().getHost());
log.info("访问接口端口: " + exchange.getRequest().getURI().getPort());
log.info("访问接口URL: " + exchange.getRequest().getURI().getPath());
log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery());
log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms");
log.info("我是美丽分割线: ###################################################");
System.out.println();
}
}));
}
}
YAML中的配置
gateway:
routes:
- id: pay_routh1
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/get/**
- id: pay_routh2
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/info/**
- id: pay_routh3
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/filter/**
此时,访问http://localhost:9527/pay/gateway/filter
或者http://localhost:9527/pay/gateway/info
,就可以看到接口请求时长了。
自定义条件Filter
-
新建类名XXX需要以GatewayFilterFactory结尾并继承AbstractGatewayFilterFactory类
-
新建
xxxGatewayFilterFactory.Config内部类
-
重写apply方法
-
重写shortcutFieldOrder
-
空参构造方法,内部调用super
@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config>
{
public MyGatewayFilterFactory()
{
super(MyGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(MyGatewayFilterFactory.Config config)
{
return new GatewayFilter()
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
System.out.println("进入了自定义网关过滤器MyGatewayFilterFactory,status:"+config.getStatus());
if(request.getQueryParams().containsKey("codewei")){
return chain.filter(exchange);
}else{
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("status");
}
public static class Config
{
@Getter@Setter
private String status;//设定一个状态值/标志位,它等于多少,匹配和才可以访问
}
}
yaml配置
gateway:
routes:
- id: pay_routh3
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/filter/**
filters:
- My=myfilter
9. SpringCloud Alibaba
2018.10.31,Spring Cloud Alibaba 正式入驻了 Spring Cloud 官方孵化器,并在 Maven 中央库发布了第一个版本。
Spring Cluod Alibaba致力于提供微服务开发的一站式解决方案。
中文文档:https://github.com/alibaba/spring-cloud-alibaba/blob/2022.x/README-zh.md
主要功能
- 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成对应 Spring Cloud 版本所支持的负载均衡组件的适配。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
组件
Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
10. Nacos服务注册和配置中心
10.1 介绍
Nacos: Dynamic Naming and Configuration Service
Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
Nacos就是注册中心 + 配置中心的组合。与Spring Cloud Consul功能类似。
作用:
- 替代Eureka/Consul做服务注册中心
- 替代(Config+Bus)/Consul 做服务配置中心和满足动态刷新广播通知
官网:https://nacos.io/
各种注册中心比较
C:一致性
A:可用性
P:分区容错性
服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
---|---|---|---|
Eureka | AP | 支持 | 低 |
Zookeeper | CP | 不支持 | 中 |
Consul | CP | 支持 | 高 |
Nacos | AP | 支持 | 高 |
据说 Nacos 在阿里巴巴内部有超过 10 万的实例运行,已经过了类似双十一等各种大型流量的考验,Nacos默认是AP模式,但也可以调整切换为CP,我们一般用默认AP即可。
10.2 下载安装
注意Nacos版本!这里我们下载Nacos 2.2.3
。
下载解压完成后,运行bin
目录下的startup
即可运行。standalone
表示单机模式运行,非集群模式。
sh startup.sh -m standalone
Nacos默认端口号为:8848
启动后,访问http://localhost:8848/nacos/index.html
默认用户名:nacos,密码:nacos。
通过执行如下命令,关闭nacos。
sh shutdown.sh
10.3 服务注册中心
nacos可以像consul一样,作为服务的注册中心。
新建Module,cloudalibaba-provider-payment9001
,作为服务提供者
修改Pom,引入依赖
<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>cn.codewei</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>
修改yaml配置
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class Main9001 {
public static void main(String[] args) {
SpringApplication.run(Main9001.class, args);
}
}
新建PayAlibabaController
@RestController
public class PayAlibabaController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/pay/nacos/{id}")
public String getPayInfo(@PathVariable("id") Integer id)
{
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
测试
访问http://localhost:9001/pay/nacos/1
,成功。
访问http://localhost:8848/nacos/index.html
,可以看到服务已经成功注册到nacos中。
新建cloudalibaba-consumer-nacos-order83
,作为服务消费者
修改Pom,引入依赖
<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>
修改yaml配置
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
#消费者将要去访问的微服务名称(nacos微服务提供者叫什么你写什么)
service-url:
nacos-user-service: http://nacos-payment-provider
主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class Main83 {
public static void main(String[] args) {
SpringApplication.run(Main83.class, args);
}
}
新建RestTemplateConfig
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
新建OrderNacosController
@RestController
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
// 绑定yaml中我们定义的service-url.nacos-user-service,也可以不在yaml中定义,直接在这里写死,但是不好维护
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping("/consumer/pay/nacos/{id}")
public String paymentInfo(@PathVariable("id") Integer id)
{
String result = restTemplate.getForObject(serverURL + "/pay/nacos/" + id, String.class);
return result+"\t"+" 我是OrderNacosController83调用者。。。。。。";
}
}
测试
启动nacos,启动9001和83服务,可以在nacos中看到,两个服务都已经注册。
访问http://localhost:83/consumer/pay/nacos/1
,可以正常访问!
负载均衡
为了方便,我们不再新建其他的服务提供,我们使用一个小技巧,直接拷贝虚拟端口映射。
右键要拷贝的微服务
点击Modify options
选择Add VM options
填入-DServer.port=9002
然后点击Apply
,OK
即可。
此时,启动nacos,9001,9002和83。
访问http://localhost:8848/nacos/index.html
,可以看到nacos-payment-provider
服务中有两个实例存在了。
此时,访问http://localhost:83/consumer/pay/nacos/1
。可以看到请求结果分别来自9001
和9002
,采用轮询方式。
10.4 服务配置中心
nacos可以像consul一样实现中心化全局配置的动态变更。
步骤
1.新建Module,cloudalibaba-config-nacos-client3377
2.修改POM,引入依赖
<dependencies>
<!--bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.修改yaml配置文件
有两个配置文件,application.yaml
和bootstrap.yaml
。
Nacos同Consul一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动,为了满足动态刷新和全局广播通知,springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application。
applicatin.yaml
server:
port: 3377
spring:
profiles:
active: dev # 表示开发环境
#active: prod # 表示生产环境
#active: test # 表示测试环境
bootstrap.yaml
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
# nacos端配置文件DataId的命名规则是:
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# 本案例的DataID是:nacos-config-client-dev.yaml
4.主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClient3377 {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClient3377.class,args);
}
}
5.业务类 NacosConfigClientController
@RestController
@RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
public class NacosConfigClientController
{
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
6.在Nacos中添加配置信息
Nacos中的匹配规则:
-
设置DataId理论:Nacos中的dataid的组成格式及与SpringBoot配置文件中的匹配规则。
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
-
配置DataId实操
prefix
默认为spring.application.name
的值。spring.profile.active
即为当前环境对应的profile
,可以通过配置项spring.profile.active
来配置。file-exetension
配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension
来配置。
实操:
在nacos管理页面进行配置。
点击创建配置。
填写相关信息。
填写完毕后,点击发布即可。
7.测试
此时启动3377。
访问localhost:3377/config/info
可以看到,成功获取了nacos中配置的信息。
8.自带动态刷新
此时,我们在nacos配置页面中将配置信息中的version=1
改为version=2
。刷新页面,可以看到,配置信息自适应的更改了。这是因为我们在主启动类上增加了@RefreshScope
注解。
9.历史配置
Nacos会记录配置文件的历史版本默认保留30天,此外还有一键回滚功能,回滚操作将会触发配置更新。
我们重启nacos,nacos的配置的数据也是存在的,具有持久化的功能。
10.5 数据模型之Namespace-Group-DataId
问题1:
实际开发中,通常一个系统会准备:
-
dev开发环境
-
test测试环境
-
prod生产环境
如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
问题2:
一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境等。那怎么对这些微服务配置进行分组和命名空间管理呢?
Namespace+Group+DataId三者关系?为什么这么设计?
问题 | 描述 |
---|---|
是什么 | 类似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:DataID方案
指定spring.profile.active
和配置文件的DataID
来使不同环境下读取不同的配置。
默认空间public+默认分组DEFAULT_GROUP+新建DataID
新建test配置DataID:nacos-config-client-test.yaml
修改yaml配置,通过spring.profile.active
属性就能进行多环境下配置文件的读取。
application.yaml
server:
port: 3377
spring:
profiles:
active: test # 表示测试环境
bootstrap.yaml
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
测试访问http://localhost:3377/config/info
方案2:Group方案
通过Group实现环境区分。
默认空间public+新建PROD_GROUP+新建DataID。
新建prod配置DataID:nacos-config-client-prod.yaml
。新建Group:PROD_GROUP
。
修改yaml配置,在config下增加一条group的配置即可。可配置为PROD_GROUP
。
application.yaml
server:
port: 3377
spring:
profiles:
active: prod
bootstrap.yaml
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
group: PROD_GROUP # 指定分组group
访问http://localhost:3377/config/info
测试
方案3:Namespace方案
通过Namespace实现命名空间环境区分。
新建Namespace:Prod_Namespace
。
新建Namespace但命名空间ID不填(系统自动生成):Prod2_Namespace
。
Prod_Namespace+PROD_GROUP+DataID(nacos-config-client-prod.yaml)。
修改yaml配置,在config下增加一条namespace:: Prod_Namespace
。
application.yaml
server:
port: 3377
spring:
profiles:
active: prod
bootstrap.yaml
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
group: PROD_GROUP
namespace: Prod_Namespace
访问http://localhost:3377/config/info
测试
11. Sentinel服务熔断与限流
11.1 介绍
Sentinel是面向分布式、多语言异构化服务架构的流量治理组件。
Sentinel的特征
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
经典面试题
服务雪崩、服务降级、服务熔断、服务限流、服务隔离、服务超时。
11.2 下载与安装
下载地址:https://github.com/alibaba/Sentinel/releases
sentinel组件由2部分构成:核心库(Java客户端)、控制台(Dashboard)。
后台端口默认为:8719。前台端口默认为:8080。
运行前提:Java环境配置成功,8080端口不能被占用。
使用如下命令运行启动:
java -jar sentinel-dashboard-1.8.8.jar
启动后,访问http://localhost:8080
进入前台页面。
默认用户名:sentinel,默认密码:sentinel
登陆成功后,进入首页。
11.3 整合Sentinel案例
启动Nacos和Sentinel。
1.新建微服务cloudalibaba-sentinel-service8401
2.修改POM文件,引入依赖
<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>cn.codewei</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.修改yaml配置
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel: # sentinel配置
transport:
dashboard: localhost:8080
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
4.主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class Main8401 {
public static void main(String[] args) {
SpringApplication.run(Main8401.class, args);
}
}
5.业务类FlowLimitController
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA()
{
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
return "------testB";
}
}
6.启动8401微服务访问测试
7.访问微服务后,查看sentinel后台控制页面
Sentinel采用懒加载的方式。
注意:想使用Sentinel对某个接口进行限流和降级等操作,一定要先访问下接口,使Sentinel检测出相应的接口。
11.4 流控规则
11.4.1 基本介绍
Sentinel能够对流量进行控制,主要是监控应用的QPS流量或者并发线程数等指标,如果达到指定的阈值时,就会被流量进行控制,以避免服务被瞬时的高并发流量击垮,保证服务的高可靠性。
参 数 | 描述 |
---|---|
资源名 | 资源的唯一名称,默认就是请求的接口路径,可以自行修改,但是要保证唯一。 |
针对来源 | 具体针对某个微服务进行限流,默认值为default,表示不区分来源,全部限流。 |
阈值类型 | QPS表示通过QPS进行限流,并发线程数表示通过并发线程数限流。 |
单机阈值 | 与阈值类型组合使用。如果阈值类型选择的是QPS,表示当调用接口的QPS达到阈值时,进行限流操作。 如果阈值类型选择的是并发线程数,则表示当调用接口的并发线程数达到阈值时,进行限流操作。 |
是否集群 | 选中则表示集群环境,不选中则表示非集群环境。 |
11.4.2 流控模式-直接
默认的流控模式,当接口达到限流条件时,直接开启限流功能。
配置及说明
表示1秒钟内查询1次就是OK,若超过次数1,就直接-快速失败,报默认错误。
测试
快速点击访问http://localhost:8401/testA。
提示:Blocked by Sentinel (flow limiting)
。也就是流量被限制。
直接返回错误信息不好,后面应该对这种进行处理,有fallback进行处理。
11.4.3 流控模式-关联
当关联的资源达到阈值时,就限流自己。当与A关联的资源B达到阀值后,就限流A自己。
配置及说明
访问/testB
接口时,流量达到超过每秒1次,那么/testA
接口就会挂掉。
使用Jmeter模拟并发密集访问testB
Jmeter是压力测试工具。
Jmeter下载地址:https://jmeter.apache.org/download_jmeter.cgi
下载后,进入bin
目录,执行如下命令即可启动。
sh jmeter
如果需要将jmeter设置为中文,则在/bin/jmeter.properties
文件中进行修改
language=zh_CN
修改后,重新启动即可。
首先添加线程组。
配置线程组属性。线程数为80,也就是模拟80个用户。Ramp-Up是指到达指定线程数所需要的时间为4秒。这也就意味着,将在4秒内,每秒启动80/4=20个线程,直至达到80个线程。循环次数是指每个线程发送请求的次数。设置为1,也就是每个线程发送1次请求。
设置完成后,再添加HTTP请求。
配置HTTP请求,填写请求路径即可。
注意:填写/testB请求的路径,因为是testB接口的访问每秒超过1次时,testA接口会挂掉。
然后,侧边栏中选中HTTP请求
,然后点击上方的保存按钮。
此时,我们访问http://localhost:8401/testA
一切正常。
这时,我们启动Jmeter发起对/testB
对大量请求。
然后,我们立刻再去访问http://localhost:8401/testA
,这时会发现,该请求已经挂掉了,当Jmeter执行结束后,该接口又自动恢复了。
11.4.4 流控模式-链路
来自不同链路的请求对同一个目标访问时,实施针对性的不同限流措施,比如C请求来访问就限流,D请求来访问就是OK。
修改微服务cloudalibaba-sentinel-service8401
yaml配置文件
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
web-context-unify: false #增加该配置。controller层的方法对service层调用不认为是同一个根链路
新建FlowLimitService
@Service
public class FlowLimitService
{
@SentinelResource(value = "common") // 后文对该注解进行解释
public void common()
{
System.out.println("------FlowLimitService come in");
}
}
修改FlowLimitController,增加如下内容
/**流控-链路演示demo
* 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";
}
sentinel配置及说明
说明:C和D两个请求都访问flowLimitService.common()
方法,对C限流,对D不管。
注意:资源名要与注解@SentinelResource
中的value
保持一致。
上诉配置,也就是,在通过/testC
接口,访问common
资源时,流量每秒超过1时,就会被限流。
但是通过/testD
接口访问common
时,不会受到影响。
注意,在配置之前,先访问一次/testC接口,这样sentinel才会检测到该接口。不然无法配置。
配置完成后,此时,我们连续访问http://localhost:8401/testC
。会出现如下错误。
此时,我们通过http://localhost:8401/testD
连续多次访问,也不会受到限流的影响。
11.4.5 流控效果-快速失败
直接失败,抛出异常。Blocked by Sentinel (flow limiting)
。
11.4.6 流控效果-Warm UP
限流,冷启动。
当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。
这个场景主要用于启动需要额外开销的场景,例如建立数据库连接等。
公式:阈值除以冷却因子coldFactor(默认值为3),经过预热时长后才会达到阈值
默认coldFactor
为3,即请求QPS从threshold/3
开始,经预热时长逐渐升至QPS的阀值。
配置与说明
如按上图配置,当我们选择Warm Up
流控效果时,预热时间设置为5秒,阀值设置为10,也就意味着,5秒才能达到设定的阀值10。
前5秒的阀值上限为threshold/3
,也就是10/3=3。在这5秒内,如果其中某一时刻对/testA
同时发起10个请求是会有请求被限流的。过了保护期5秒后QPS为10,阀值上限变为10。
测试
配置完成后,此时我们连续访问localhost:8401/testA
,每秒访问该接口超过3次就会出现限流,经过5秒后,我们连续每秒点击次数不超过10次就不会再被限流。
当我们停止访问后,过一段时间,又会回到初始阀值threshold/3
。
11.4.7 流控效果-排队等待
修改FlowLimitController,添加如下内容
@GetMapping("/testE")
public String testE()
{
System.out.println(System.currentTimeMillis()+" testE,排队等待");
return "------testE";
}
sentinel配置与说明
按照单机阈值,一秒钟通过一个请求,10秒后的请求作为超时处理,放弃。
比如,同时来了15个请求,那么/testE
每秒只能限制1个请求访问,那么剩下的14个请求排队等待,每秒处理其中一个请求,我们设置了超时时间为10秒,也就意味着,第10个请求往后的请求都会超时。
测试
使用Jmeter进行测试。
1秒发送20个请求到/testE
。
看后台的打印输出。
可以看到,只处理了11个请求,按道理来说,应该是处理10个请求,可能由于时间的误差,第11个请求也挤了进来。但是无伤大雅,可以看到后面超时的请求已经被丢弃掉了。
11.4.8 流控效果-并发线程数
平时建议使用QPS。
阀值类型选择并发线程数
,并设置单机阀值为1。
选择阀值类型为并发线程数
后,流控效果默认为快速失败
,没有其他效果。
使用Jmeter进行测试
模拟多个线程并发,并且循环请求。
设置线程数为100,勾选循环次数为永远。
此时,我们从浏览器手动访问localhost:5401/testE
,无法访问,被限制了。
11.5 熔断规则
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,
让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
Sentinel主要提供了三个熔断策略
- 慢调用比例
- 异常比例
- 异常数
11.5.1 熔断规则案例 - 慢调用比例
选择以慢速调用比例阈值,需要调用的比例是 RT(最大响应时间),请求的比例大于该值即为慢速调用比例。当请求比例statIntervalMs
大于设置的最小请求比例,并且比例大于阈值,则接下来的熔时长请求自动熔器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求比例小于设置的慢速调用比例,则熔时长请求自动熔器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求比例小于设置的慢速调用比例,则熔时长请求自动熔器会进入探测恢复状态(HALF-OPEN 状态)。
配置参数说明
最大RT:即最大的响应时间,指系统对请求作出响应的业务处理时间。
慢调用:处理业务逻辑的实际时间>设置的最大RT时间,这个调用叫做慢调用。
慢调用比例:在所以调用中,慢调用占有实际的比例=慢调用次数➗总调用次数
字段 | 说明 | 默认值 |
---|---|---|
资源名 | 资源名,即规则的作用对象 | |
策略 | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
最大RT | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
熔断时长 | 熔断时长,单位为 s | |
最小请求数 | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
统计时长 | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
比例阈值 | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
触发条件
进入熔断状态判断依据:在统计时长内,实际请求数目>设定的最小请求数且实际慢调用比例>比例阈值,进入熔断状态。
熔断状态
- 熔断状态(保险丝跳闸断电,不可访问):在接下来的熔断时长内请求会自动被熔断。
- 探测恢复状态(探路先锋):熔断时长结束后进入探测恢复状态。
- 结束熔断(保险丝闭合恢复,可以访问):在探测恢复状态,如果接下来的一个请求响应时间小于设置的慢调用 RT,则结束熔断,否则继续熔断。
修改FlowLimitController,新增如下内容
@GetMapping("/testF")
public String testF()
{
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("----测试:新增熔断规则-慢调用比例 ");
return "------testF 新增熔断规则-慢调用比例";
}
sentinel配置
对于上图配置解释,对于/testF
接口,当1000ms(1秒)内的请求超过10%为慢调用(调用时长超过200ms,即0.2秒),且请求数最少达到5次,就会进入熔断状态,熔断时间为5秒。经过熔断时长后,熔断器会进入探测恢复状态(半开状态),若接下来的一个请求响应时间小于设置的最大RT时间,则断路器闭合,熔断结束。若响应时间大于设置的最大RT时间,则会被再次熔断。
测试
使用Jmeter进行测试。
一秒内发送10次请求。因为再该接口中定义了睡眠1秒,所以每个请求都会被判定为慢调用。
jmeter执行后,我们再通过浏览器访问localhost:5401/testF
,就会发现已经无法访问,已经熔断了。
11.5.2 熔断规则案例 - 异常比例
当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%。
修改FlowLimitController,新增如下内容
@GetMapping("/testG")
public String testG()
{
System.out.println("----测试:新增熔断规则-异常比例 ");
int age = 10/0;
return "------testG,新增熔断规则-异常比例 ";
}
sentinel配置
按上图配置,当1秒内(统计时长),异常调用比例达到20%(比例阀值),且请求数达到5次,就会进入熔断,熔断时长为5秒。
测试
注意:前面的案例中,我们配置了全局异常处理,这里要将全局异常注释掉。
使用Jmeter,1秒内向/testG
发送10次请求。
此时,我们再通过浏览器访问localhost:8401/testG
,会发现已经无法访问,进入了熔断。
11.5.3 熔断规则案例 - 异常数
当单位统计时长异常数值超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
修改FlowLimitController,新增如下内容
@GetMapping("/testH")
public String testH()
{
System.out.println("----测试:新增熔断规则-异常数 ");
int age = 10/0;
return "------testH,新增熔断规则-异常数 ";
}
sentinel配置
按上图配置,当1秒内(统计时长),异常请求数达到3次,且总请求数达到10次,就会进入熔断,熔断时长为5秒。
测试
使用Jmeter,1秒内向/testH
发送20次请求。
此时,我们再通过浏览器访问localhost:8401/testH
,会发现已经无法访问,进入了熔断。
11.6 SentinelResource注解
前面已经遇到过这个注解。
@SentinelResource
是一个流量防卫防护组件注解,用于指定防护资源,对配置的资源进行流量控制、熔断降级等功能。
@SentinelResource注解说明
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {
//资源名称
String value() default "";
//entry类型,标记流量的方向,取值IN/OUT,默认是OUT
EntryType entryType() default EntryType.OUT;
//资源分类
int resourceType() default 0;
//处理BlockException的函数名称,函数要求:
//1.必须是 public
//2.返回类型 参数与原方法一致
//3.默认要原方法在同一个类。若希望使用其他类的函数,可配置blockHandlerClass ,并指定blockHandlerClass里面的方法。
String blockHandler() default "";
//存放blockHandler的类,对应的处理函数必须static修饰。
Class<?>[] blockHandlerClass() default {};
//用于在抛出异常的时候提供fallback处理逻辑。 fallback函数可以针对所
//有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求:
//1. 返回类型与原方法一致
//2. 参数类型需要和原方法相匹配
//3. 默认需和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定fallbackClass里面的方法。
String fallback() default "";
//存放fallback的类。对应的处理函数必须static修饰。
String defaultFallback() default "";
//用于通用的 fallback 逻辑。默认fallback函数可以针对所有类型的异常进
//行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:
//1. 返回类型与原方法一致
//2. 方法参数列表为空,或者有一个 Throwable 类型的参数。
//3. 默认需要和原方法在同一个类中。若希望使用其他类的函数,可配置fallbackClass ,并指定 fallbackClass 里面的方法。
Class<?>[] fallbackClass() default {};
//需要trace的异常
Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};
//指定排除忽略掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。
Class<? extends Throwable>[] exceptionsToIgnore() default {};
}
启动nacos和sentinel。
11.6.1 按照rest地址限流+默认限流返回
通过访问的rest地址来限流,会返回Sentinel自带默认的限流处理信息。
新建业务类RateLimitController
@RestController
@Slf4j
public class RateLimitController
{
@GetMapping("/rateLimit/byUrl")
public String byUrl()
{
return "按rest地址限流测试OK";
}
}
sentinel配置
测试
连续请求http://localhost:8401/rateLimit/byUrl
,就会看到被限流后的默认返回。
11.6.2 按SentinelResource资源名称限流+自定义限流返回
不想用默认的限流提示(Blocked by Sentinel (flow limiting)),想返回自定义限流的提示。
修改业务类RateLimitController,添加如下内容
@GetMapping("/rateLimit/byResource")
@SentinelResource(value = "byResourceSentinelResource",blockHandler = "handleException")
public String byResource()
{
return "按资源名称SentinelResource限流测试OK";
}
public String handleException(BlockException exception)
{
return "服务不可用@SentinelResource启动"+"\t"+"o(╥﹏╥)o";
}
sentinel配置
注意:资源名,要与@SentinelResource中定义的相同。
测试
连续请求http://localhost:8401/rateLimit/byResource
,就会看到定义的BlockHandler返回。
11.6.3 按SentinelResource资源名称限流+自定义限流返回+服务降级处理
按SentinelResource配置,点击超过限流配置返回自定义限流提示+程序异常返回fallback服务降级。
修改业务类RateLimitController,添加如下内容
@GetMapping("/rateLimit/doAction/{p1}")
@SentinelResource(value = "doActionSentinelResource",
blockHandler = "doActionBlockHandler", fallback = "doActionFallback")
public String doAction(@PathVariable("p1") Integer p1) {
if (p1 == 0){
throw new RuntimeException("p1等于零直接异常");
}
return "doAction";
}
public String doActionBlockHandler(@PathVariable("p1") Integer p1,BlockException e){
log.error("sentinel配置自定义限流了:{}", e);
return "sentinel配置自定义限流了";
}
public String doActionFallback(@PathVariable("p1") Integer p1,Throwable e){
log.error("程序逻辑异常了:{}", e);
return "程序逻辑异常了"+"\t"+e.getMessage();
}
sentinel配置
测试
连续请求http://localhost:8401/rateLimit/doAction/1
,就会看到定义的BlockHandler返回。
请求http://localhost:8401/rateLimit/doAction/0
,发生异常,所以触发了降级处理。
小总结
blockHandler,主要针对sentinel配置后出现的违规情况处理
fallback,程序异常了JVM抛出的异常服务降级
两者可以同时共存
11.7 热点规则
热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作。
热点参数会统计输入参数中的热点参数,并根据配置的流阈值与模式,对包含热点参数的资源调用进行流控制。热点参数可以做一种特殊的流量控制,对包含热点参数的资源调用生效。
修改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配置
限流模式只支持QPS模式,固定写死了。
@SentinelResource
注解的方法参数索引,0代表第一个参数,1代表第二个参数,以此类推。
单机阀值以及统计窗口时长表示在此窗口时间超过阀值就限流。
上面的抓图就是第一个参数有值的话,1秒的QPS为1,超过就限流,限流后调用dealHandler_testHotKey支持方法。
测试
连续访问http://localhost:8401/testHotKey?p1=1
,则会触发热点规则。
连续访问http://localhost:8401/testHotKey?p2=1
,因为我们指定了参数索引为0,所以不会触发热点规则。
注意:热点参数的注意点,参数必须是基本类型或者String。
参数例外项
普通正常限流:含有P1参数,超过1秒钟一个后,达到阈值1后马上被限流。
例外特殊限流:我们期望p1参数当它是某个特殊值时,到达某个约定值后【普通正常限流】规则突然例外、失效了,它的限流值和平时不一样。假如当p1的值等于5时,它的阈值可以达到200或其它值。
sentinel配置
测试
连续访问http://localhost:8401/testHotKey?p1=1
,则会触发热点规则。
连续访问http://localhost:8401/testHotKey?p1=5
,因为我们设定了参数例外项,所以不会触发限流。
11.8 授权规则
在某些场景下,需要根据调用接口的来源判断是否允许执行本次请求。此时就可以使用Sentinel提供的授权规则来实现,Sentinel的授权规则能够根据请求的来源判断是否允许本次请求通过。
在Sentinel的授权规则中,提供了 白名单与黑名单 两种授权类型。白放行、黑禁止。
新增EmpowerController
@RestController
@Slf4j
public class EmpowerController //Empower授权规则,用来处理请求的来源
{
@GetMapping(value = "/empower")
public String requestSentinel4(){
log.info("测试Sentinel授权规则empower");
return "Sentinel授权规则";
}
}
新增MyRequestOriginParser
@Component
public class MyRequestOriginParser implements RequestOriginParser
{
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
return httpServletRequest.getParameter("serverName");
}
}
Sentinel配置
测试
访问http://localhost:8401/empower?serverName=test
。
访问http://localhost:8401/empower?serverName=test1
正常访问。
11.9 规则持久化
一旦我们重启微服务应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效。
修改POM,增加如下内容
<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改yaml配置,增加如下内容
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow # com.alibaba.cloud.sentinel.datasource.RuleType
流量控制规则:FlowRule
熔断降级规则:DegradeRule
访问控制规则:AuthorityRule
系统保护规则:SystemRule
热点规则:ParamFlowRule
Nacos配置
在nacos中新建配置
[
{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
- resource:资源名称
- limitApp:来源应用
- grade:阈值类型,0表示线程数,1表示QPS
- count:单机阈值
- strategy:流控模式,0表示直接,1表示关联,2表示链路
- controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待
- clusterMode:是否集群。
测试
重启8401服务。
连续快速访问http://localhost:8401/rateLimit/byUrl
,发现限流已经生效。
访问http://localhost:8080/
,sentinel后台页面,可以看到配置的规则也存在sentinel管理页面中。
注意:这只是持久化的其中一种方案,可以将配置持久化道数据库、redis、file文件等。
参考:https://github.com/all4you/sentinel-tutorial/blob/master/sentinel-practice/sentinel-persistence-rules/sentinel-persistence-rules.md
11.10 集成OpenFeign
cloudalibaba-consumer-nacos-order83
通过OpenFeign调用cloudalibaba-provider-payment9001
。
访问者要有fallback服务降级的情况,不要持续访问9001加大微服务负担,但是通过feign接口调用的又方法各自不同,如果每个不同方法都加一个fallback配对方法,会导致代码膨胀不好管理。
通过fallback属性进行统一配置,feign接口里面定义的全部方法都走统一的服务降级,一个搞定即可。
blockHandler还是要和业务代码写一起,但是fallback交给openFeign处理。
11.10.1 修改cloudalibaba-provider-payment9001
pom,新增如下内容
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
yaml配置
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
sentinel:
transport:
dashboard: localhost:8080
port: 8719
主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class Main9001 {
public static void main(String[] args) {
SpringApplication.run(Main9001.class, args);
}
}
修改PayAlibabaController,新增如下内容
@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流控配置规则");
}
11.10.2 修改cloud-api-commons
pom,新增如下内容
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
新增PayFeignSentinelApi接口
@FeignClient(value = "nacos-payment-provider",fallback = PayFeignSentinelApiFallBack.class)
public interface PayFeignSentinelApi {
@GetMapping("/pay/nacos/get/{orderNo}")
public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo);
}
新建PayFeignSentinelApiFallBack
@Component
public class PayFeignSentinelApiFallBack implements PayFeignSentinelApi
{
@Override
public ResultData getPayByOrderNo(String orderNo)
{
return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"对方服务宕机或不可用,FallBack服务降级");
}
}
11.10.3 修改cloudalibaba-consumer-nacos-order83
pom,新增如下内容
<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>
<dependency>
<groupId>cn.codewei</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
yaml配置,新增如下内容
feign:
sentinel:
enabled: true
主启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Main83 {
public static void main(String[] args) {
SpringApplication.run(Main83.class, args);
}
}
修改OrderNacosController,新增如下内容
@Resource
private PayFeignSentinelApi payFeignSentinelApi;
@GetMapping(value = "/consumer/pay/nacos/get/{orderNo}")
public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo)
{
return payFeignSentinelApi.getPayByOrderNo(orderNo);
}
11.10.4 测试
启动nacos、sentinel。
启动9001与83服务。
出现错误!
错误原因:springboot+springcloud版本太高导致和阿里巴巴Sentinel不兼容。
解决办法:降低springboot与cloud的版本。
<spring.boot.version>3.0.9</spring.boot.version>
<spring.cloud.version>2022.0.2</spring.cloud.version>
该版本仅为该案例演示,后面继续使用如下版本
<spring.boot.version>3.2.0</spring.boot.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
再次启动,启动成功!
访问http://localhost:83/consumer/pay/nacos/get/1024
,成功。
sentinel流控为例,进行配置
连续快速访问http://localhost:83/consumer/pay/nacos/get/1024
,触发blockHandler。
模拟9001宕机,我们关闭9001服务。
再次访问http://localhost:83/consumer/pay/nacos/get/1024
。
可以看到,调用了openfeign配置的降级fallback处理。
11.11 集成GateWay
先将boot和cloud版本号恢复。
<spring.boot.version>3.2.0</spring.boot.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
cloudalibaba-sentinel-gateway9528
保护cloudalibaba-provider-payment9001
。
1. 新建Module,cloudalibaba-sentinel-gateway9528
2. 修改POM,引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3. 修改yaml配置
server:
port: 9528
spring:
application:
name: cloudalibaba-sentinel-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: pay_routh1
uri: http://localhost:9001
predicates:
- Path=/pay/**
4. 主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class Main9528 {
public static void main(String[] args) {
SpringApplication.run(Main9528.class, args);
}
}
5. 新建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").setCount(2).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
BlockRequestHandler handler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
Map<String,String> map = new HashMap<>();
map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
map.put("errorMessage", "请求太过频繁,系统忙不过来,触发限流(sentinel+gataway整合Case)");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(handler);
}
}
测试
访问http://localhost:9001/pay/nacos/333
,不会出现限流。
访问http://localhost:9528/pay/nacos/333
,出现限流。
12. Seata分布式事务
12.1 简介
官网:https://seata.apache.org/
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
但是关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。
分布式事务,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
用户购买商品的业务逻辑。整个业务逻辑由 3 个微服务提供支持:
- 仓储服务:对给定的商品扣除仓储数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
架构图:
12.2 seata工作流程
纵观整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知。
Seata对分布式事务的协调和控制就是1+3
1个XID,XID是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
Seata术语
-
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。就是seata本身。
-
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
标注全局
@GlobalTransactional
启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。 -
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
就是mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向TC注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚
TC和TM有且仅有一个,RM可以存在多个。
小总结
三个组件相互协作,TC以Seata 服务器(Server)形式独立部署,TM和RM则是以Seata Client的形式集成在微服务中运行。
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
- XID 在微服务调用链路的上下文中传播
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖
- TM 向 TC 发起针对 XID 的全局提交或回滚决议
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求
各事务模式
- Seata AT模式
- Seata TCC模式
- Seata Saga模式
- Seata XA模式
在该文章中,我们主要学习Seata AT模式。
AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。
12.3 seata下载安装
下载地址:https://github.com/apache/incubator-seata/releases
seata参数配置:https://seata.apache.org/zh-cn/docs/user/configurations
新人部署文档:https://seata.apache.org/zh-cn/docs/ops/deploy-guide-beginner
mysql8.0数据库里面建库+建表
新建数据库seata
create database seata;
在seata库中建表
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
seata更改配置
修改seata配置文件,/seata/conf/application.yaml
,注意备份原来的配置文件
server:
port: 7091
spring:
application:
name: seata-server
console:
user:
username: seata
password: seata
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
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: 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: jdbc:mysql://127.0.0.1:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
user: root
password: 密码
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
测试
先启动nacos。
再启动seata。
sh seata-server.sh
访问http://localhost:7091/
。
访问http://localhost:8848/
可以看到seata已经存在nacos中了。
12.4 案例实战
案例说明
这里我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单 → 减库存 → 扣余额 → 改(订单)状态
12.4.1 数据库准备
订单、库存、账户,3个业务数据库准备。
注意,要先启动nacos,再启动seata。
创建3个数据库
- seata_order:存储订单的数据库
- seata_storage:存储库存的数据库
- seata_account:存储账户信息的数据库
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
订单-库存-账户3个库下都需要建各自的undo_log回滚日志表
官网:https://github.com/seata/seata/blob/2.x/script/client/at/db/mysql.sql
注意:AT模式专用,其它模式不需要
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个数据库分别建立对应的数据库表
t_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;
t_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');
t_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');
12.4.2 编码实现
1.mybatis一键生成
config.properties
package.name=cn.codewei
# seata_order
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =密码
# seata_storage
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =密码
# seata_account
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =密码
generatorConfig.xml
<table tableName="t_order" domainObjectName="Order">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
<!--seata_storage-->
<!--<table tableName="t_storage" domainObjectName="Storage">-->
<!-- <generatedKey column="id" sqlStatement="JDBC"/>-->
<!--</table>-->
<!--seata_account-->
<!--<table tableName="t_account" domainObjectName="Account">-->
<!-- <generatedKey column="id" sqlStatement="JDBC"/>-->
<!--</table>-->
2.修改cloud-api-commons模块
新建StorageFeignApi
接口
@FeignClient("seata-storage-service")
public interface StorageFeignApi {
/**
* 扣减库存
*/
@PostMapping(value = "/storage/decrease")
ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
新建AccountFeignApi
接口
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
//扣减账户余额
@PostMapping("/account/decrease")
ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}
3.新建Order微服务
新建微服务seata-order-service2001
修改pom.xml
,引入依赖
修改yaml配置文件
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
# ==========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: 密码
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.codewei.entity
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
主启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("cn.codewei.mapper")
public class Main2001 {
public static void main(String[] args) {
SpringApplication.run(Main2001.class,args);
}
}
将mybatis生成的对应的文件移至该模块中。
新建OrderService
接口
public interface OrderService {
void create(Order order);
}
新建OrderServiceImpl
@Slf4j
@Service
public class OrderServiceImpl implements OrderService
{
@Resource
private OrderMapper orderMapper;
@Resource//订单微服务通过OpenFeign去调用库存微服务
private StorageFeignApi storageFeignApi;
@Resource//订单微服务通过OpenFeign去调用账户微服务
private AccountFeignApi accountFeignApi;
@Override
//@GlobalTransactional(name = "zzyy-create-order",rollbackFor = Exception.class) //AT
//@GlobalTransactional @Transactional(rollbackFor = Exception.class) //XA
public void create(Order order) {
//xid检查
String xid = RootContext.getXID();
//1. 新建订单
log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid);
//订单状态status:0:创建中;1:已完结
order.setStatus(0);
int result = orderMapper.insertSelective(order);
//插入订单成功后获得插入mysql的实体对象
Order orderFromDB = null;
if(result > 0)
{
orderFromDB = orderMapper.selectOne(order);
//orderFromDB = orderMapper.selectByPrimaryKey(order.getId());
log.info("-------> 新建订单成功,orderFromDB info: "+orderFromDB);
System.out.println();
//2. 扣减库存
log.info("-------> 订单微服务开始调用Storage库存,做扣减count");
storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
log.info("-------> 订单微服务结束调用Storage库存,做扣减完成");
System.out.println();
//3. 扣减账号余额
log.info("-------> 订单微服务开始调用Account账号,做扣减money");
accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
log.info("-------> 订单微服务结束调用Account账号,做扣减完成");
System.out.println();
//4. 修改订单状态
//订单状态status:0:创建中;1:已完结
log.info("-------> 修改订单状态");
orderFromDB.setStatus(1);
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);
log.info("-------> 修改订单状态完成"+"\t"+updateResult);
log.info("-------> orderFromDB info: "+orderFromDB);
}
System.out.println();
log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);
}
}
新建OrderController
@RestController
public class OrderController {
@Resource
private OrderService orderService;
/**
* 创建订单
*/
@GetMapping("/order/create")
public ResultData create(Order order)
{
orderService.create(order);
return ResultData.success(order);
}
}
4.新建Storage微服务
新建微服务seata-storage-service2002
修改pom.xml
,引入依赖
修改yaml配置文件
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: 密码
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.codewei.entity
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
主启动类
@SpringBootApplication
@MapperScan("cn.codewei.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@EnableFeignClients
public class SeataStorageMainApp2002
{
public static void main(String[] args)
{
SpringApplication.run(SeataStorageMainApp2002.class,args);
}
}
将mybatis生成的对应的文件移至该模块中。
StorageMapper
public interface StorageMapper extends Mapper<Storage> {
public interface StorageMapper extends Mapper<Storage>
{
/**
* 扣减库存
*/
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
}
<update id="decrease">
UPDATE
t_storage
SET
used = used + #{count},
residue = residue - #{count}
WHERE product_id = #{productId}
</update>
新建StorageService
接口
public interface StorageService {
/**
* 扣减库存
*/
void decrease(Long productId, Integer count);
}
新建StorageServiceImpl
@Service
@Slf4j
public class StorageServiceImpl implements StorageService
{
@Resource
private StorageMapper storageMapper;
/**
* 扣减库存
*/
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service中扣减库存开始");
storageMapper.decrease(productId,count);
log.info("------->storage-service中扣减库存结束");
}
}
新建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("扣减库存成功!");
}
}
5.新建Account微服务
新建微服务seata-Account-service2002
修改pom.xml
,引入依赖
修改yaml配置文件
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: 密码
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.codewei.entity
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
主启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("cn.codewei.mapper")
public class SeataAccountMainApp2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
将mybatis生成的对应的文件移至该模块中。
AccountMapper
public interface AccountMapper extends Mapper<Account>
{
void decrease(@Param("userId") Long userId, @Param("money") Long money);
}
<update id="decrease">
UPDATE
t_account
SET
residue = residue - #{money},used = used + #{money}
WHERE user_id = #{userId};
</update>
新建AccountService
接口
public interface AccountService {
/**
* 扣减账户余额
* @param userId 用户id
* @param money 本次消费金额
*/
void decrease(@Param("userId") Long userId, @Param("money") Long money);
}
新建AccountServiceImpl
@Service
@Slf4j
public class AccountServiceImpl implements AccountService
{
@Resource
AccountMapper accountMapper;
/**
* 扣减账户余额
*/
@Override
public void decrease(Long userId, Long money) {
log.info("------->account-service中扣减账户余额开始");
accountMapper.decrease(userId,money);
//myTimeOut();
//int age = 10/0;
log.info("------->account-service中扣减账户余额结束");
}
/**
* 模拟超时异常,全局事务回滚
*/
private static void myTimeOut()
{
try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
新建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("扣减账户余额成功!");
}
}
12.4.3 测试
启动nacos。
启动seata。
启动2001、2002、2003服务。全部成功启动。
数据库初始化情况。
t_order
t_account
t_storage
正常下单,没有@GlobalTransactional
此时没有在订单模块添加@GlobalTransactional
。
模拟1号用户花费100块钱买了10个1号产品。访问请求http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
。
请求成功。
此时数据库情况如下。
t_order
t_account
t_storage
超时异常出错,没有@GlobalTransactional
修改seata-account-service2003微服务,AccountServiceImpl添加超时。
OpenFeign默认60秒超时。
重启2003服务。
模拟1号用户花费100块钱买了10个1号产品。访问请求http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
。
测试发现,库存和账户金额都减少了,但是订单状态还是没0,并没有发生改变。
超时异常解决,添加@GlobalTransactional
AccountServiceImpl保留超时方法。
添加@GlobalTransactional
注解
@Override
@GlobalTransactional(name = "zzyy-create-order",rollbackFor = Exception.class) //AT
public void create(Order order)
{
...
}
重启2001服务。
模拟1号用户花费100块钱买了10个1号产品。访问请求http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
。
查看seata后台http://localhost:7091/
。
全局事务ID
全局锁
在业务执行过程中,订单数据会新增至数据库、库存以及金额也会发生改变,业务结束后,所有数据会发生会滚,任何数据都不发生改变。
12.5 面试题
AT模式如何做到对业务的无侵入?
AT模式整体机制:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
在一阶段,Seata 会拦截“业务 SQL”
- 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”
- 执行“业务 SQL”更新业务数据,在业务数据更新之后
- 其保存成“after image”,最后生成行锁
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段分两种情况
-
正常提交
二阶段如是顺利提交的话,因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
-
异常提交
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。