文章目录
- SpringCloud笔记
- SpringCloud介绍
- 进行基础的项目构建
- 引入微服务
- 对子目录进行改造(抽取公共部分)
- Consul服务中心
- LoadBalancer负载均衡
- OpenFeign
- **CircuitBreaker断路器【重点】**
- 隔离(resilience4j-bulkhead)
- 限流
- Micrometer + ZipKin 分布式链路追踪
- Gateway新一代网关
- SpringCloud alibaba Nacos
- Spring Cloud Alibaba Sentinel(实现熔断与限流)
- SpringCloud Alibaba Seata(处理分布式事务)
SpringCloud笔记
SpringCloud介绍
传统的单体架构足以满足中小型项目的需求,但是如果对于一个用户量庞大的系统就会出现各种问题。
例如:如果只有一个支付系统,那么系统崩溃了整个系统就运作不了了。
而微服务解决了这个问题,它允许系统以集群的形式部署,形成负载均衡,尽量减少系统崩溃带来的问题。
组件的介绍:
进行基础的项目构建
首先创建一个maven父工程,该项目只留下pom.xml文件,
在pom.xml文件中写入以下配置:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <hutool.version>5.8.22</hutool.version> <lombok.version>1.18.26</lombok.version> <druid.version>1.1.20</druid.version> <mybatis.springboot.version>3.0.2</mybatis.springboot.version> <mysql.version>8.0.11</mysql.version> <swagger3.version>2.2.0</swagger3.version> <mapper.version>4.2.3</mapper.version> <fastjson2.version>2.0.40</fastjson2.version> <persistence-api.version>1.0.2</persistence-api.version> <spring.boot.test.version>3.1.5</spring.boot.test.version> <spring.boot.version>3.2.0</spring.boot.version> <spring.cloud.version>2023.0.0</spring.cloud.version> <spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version> </properties> <dependencyManagement> <dependencies> <!--springboot 3.2.0--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--springcloud 2023.0.0--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--springcloud alibaba 2022.0.0.0-RC2--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring.cloud.alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--SpringBoot集成mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.springboot.version}</version> </dependency> <!--Mysql数据库驱动8 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--SpringBoot集成druid连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <!--通用Mapper4之tk.mybatis--> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>${mapper.version}</version> </dependency> <!--persistence--> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> <version>${persistence-api.version}</version> </dependency> <!-- fastjson2 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>${fastjson2.version}</version> </dependency> <!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>${swagger3.version}</version> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <optional>true</optional> </dependency> <!-- spring-boot-starter-test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring.boot.test.version}</version> <scope>test</scope> </dependency> </dependencies> </dependencyManagement>
在这过程中出现的问题是dependency爆红,解决方法:
这里说下父工程引入依赖爆红问题,本质上是应为dependcyMangerment标签只是声明依赖,如果在本地仓库中没有就会爆红。解决方法,去掉dependcyMangerment标签,刷新工程,让他依赖下载完毕,再加上dependcyMangerment。
复习一下maven
Maven 使用dependencyManagement 元素来提供了一种管理依赖版本号的方式。
通常会在一个组织或者项目的最顶层的父POM 中看到dependencyManagement 元素。
使用pom.xml 中的dependencyManagement 元素能让所有在子项目中引用一个依赖而不用显式的列出版本号。Maven会沿着父子层次向上走,直到找到一个拥有dependencyManagement 元素的项目,然后它就会使用这个dependencyManagement 元素中指定的版本号。
这样做的好处就是:如果有多个子项目都引用同一样依赖,则可以避免在每个使用的子项目里都声明一个版本号,优势:
- 这样当想升级或切换到另一个版本时,只需要在顶层父容器里更新,而不需要一个一个子项目的修改
- 另外如果某个子项目需要另一个版本,是需要声明version就可
dependencyManagement里只是声明依赖,*并不实现引入*,因此子项目需要显示的声明需要用的依赖。
如果不在子项目中声明依赖,是不会从父项目中继承下来的,只有在子项目中写了该依赖项并且没有指定具体版本,才会从父项目中继承该项 且version和scope都读取自父pom;
新建数据库,设置 t_pay表:
DROP TABLE IF EXISTS `t_pay`; CREATE TABLE `t_pay` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `pay_no` VARCHAR(50) NOT NULL COMMENT '支付流水号', `order_no` VARCHAR(50) NOT NULL COMMENT '订单流水号', `user_id` INT(10) DEFAULT '1' COMMENT '用户账号ID', `amount` DECIMAL(8,2) NOT NULL DEFAULT '9.9' COMMENT '交易金额', `deleted` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除标志,默认0不删除,1删除', `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='支付交易表'; INSERT INTO t_pay(pay_no,order_no) VALUES('pay17203699','6544bafb424a'); SELECT * FROM t_pay;
创建一个mybatis自动生成模块
在父工程下创建一个子模块,该模块的作用只用于根据数据库生成实体类
说明:这个工程只是为了暂时存储生成的代码,等到业务工程使用的时候,会将对应的类复制过去
该模块的pom.xml文件:
<dependencies> <!--Mybatis 通用mapper tk单独使用,自己独有+自带版本号--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <!-- Mybatis Generator 自己独有+自带版本号--> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.4.2</version> </dependency> <!--通用Mapper--> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> </dependency> <!--mysql8.0--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--persistence--> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <resources> <resource> <directory>${basedir}/src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>${basedir}/src/main/resources</directory> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.4.2</version> <configuration> <configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile> <overwrite>true</overwrite> <verbose>true</verbose> </configuration> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>4.2.3</version> </dependency> </dependencies> </plugin> </plugins> </build>
在子模块的resources下新建文件config.properties
,将内容改成自己的:
#t_pay表包名 package.name=com.atguigu.cloud # mysql8.0 jdbc.driverClass = com.mysql.cj.jdbc.Driver jdbc.url= jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true jdbc.user = root jdbc.password =123456
在子模块的resources下新建文件generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <properties resource="config.properties"/> <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/> <property name="caseSensitive" value="true"/> </plugin> <jdbcConnection driverClass="${jdbc.driverClass}" connectionURL="${jdbc.url}" userId="${jdbc.user}" password="${jdbc.password}"> </jdbcConnection> <javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/> <sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/> <javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/> <table tableName="t_pay" domainObjectName="Pay"> <generatedKey column="id" sqlStatement="JDBC"/> </table> </context> </generatorConfiguration>
创建业务逻辑模块(cloud-provider-payment8001)
微服务小口诀:
- 建module
- 改pom
- 写yml
- 主启动
- 业务类
在service包中编写基本业务逻辑的增删改查,需要注意的是用的是mybatis和Mapper4,所以在增删改查的时候需要注入payMapper
,调用payMapper
的方法对数据库进行增删改查操作。
@Service public class PayServiceImpl implements PayService { @Resource private PayMapper payMapper; @Override public int add(Pay pay) { return payMapper.insertSelective(pay); } @Override public int delete(Integer id) { return payMapper.deleteByPrimaryKey(id); } @Override public int update(Pay pay) { return payMapper.updateByPrimaryKey(pay); } @Override public PayDTO getById(Integer id) { Pay pay = payMapper.selectByPrimaryKey(id); PayDTO payDTO = new PayDTO(); BeanUtils.copyProperties(pay,payDTO); return payDTO; } @Override public List<PayDTO> getAll() { List<Pay> pays = payMapper.selectAll(); List<PayDTO> payDTOList = new ArrayList<>(); for (Pay pay : pays) { PayDTO payDTO = new PayDTO(); BeanUtils.copyProperties(pay,payDTO); payDTOList.add(payDTO); } return payDTOList; } }
编写全局异常处理GlobalException,编写全局返回类(类似于用户中心项目),完成一个基本项目的搭建。
引入微服务
构建一个cloud-consumer-order80
的子项目。
问题:订单微服务80如何才能调用到支付微服务8001。
首先一个子模块向另一个子模块通信,需要一个桥梁,这时候可以用到RestTemplate,通过其API可以将两个微服务之间进行通信。
RestTemplate介绍(了解即可,后面使用SpringCloud代替)
官网地址:https://docs.spring.io/spring-framework/docs/6.0.11/javadoc-api/org/springframework/web/client/RestTemplate.html
RestTemplate提供了多种便捷访问远程Http服务的方法, 是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集。
RestTemplate常用的API
使用restTemplate访问restful接口非常的简单粗暴无脑。
(url, requestMap, ResponseBean.class)这三个参数分别代表 :REST请求地址、请求参数、HTTP响应转换被转换成的对象类型。
在项目中使用RestTemplate首先是写一个配置类,将创建的对象交给Spring容器进行管理。
@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
向支付模块进行通信
- 首先要定义一个全局路径,该路径为8001端口的全路径
- 明确你要访问支付模块的接口类型(是post还是get),调用对应的RestTemplate方法进行通信
@RestController @RequestMapping("/consumer") public class OrderController { public static final String PAYMENT_URL = "http://localhost:8001"; @Resource private RestTemplate restTemplate; @GetMapping("/pay/add") public ResultData addOrder(PayDTO payDTO){ return restTemplate.postForObject(PAYMENT_URL + "/pay/add",payDTO,ResultData.class); } }
对子目录进行改造(抽取公共部分)
可以看出每个微服务之间存在着许多公共部分,所以我们可以将其抽取出来,形成一个公共的模块
- 首先创建一个模块,该模块存放着所有微服务相同的类或配置等。
- 将改模块进行mvn打包成jar包
- 再需要使用的模块的maven中进行引入
Consul服务中心
问题:为什么要引入Consul服务中心?它帮我们解决了哪些存在问题?
首先先明确之前存在的问题:
微服务所在的IP地址和端口号硬编码到订单微服务中,会存在非常多的问题
- 如果订单微服务和支付微服务的IP地址或者端口号发生了变化,则支付微服务将变得不可用,需要同步修改订单微服务中调用支付微服务的IP地址和端口号。
- 如果系统中提供了多个订单微服务和支付微服务,则无法实现微服务的负载均衡功能。
- 如果系统需要支持更高的并发,需要部署更多的订单微服务和支付微服务,硬编码订单微服务则后续的维护会变得异常复杂。
所以,在微服务开发的过程中,需要引入服务治理功能,实现微服务之间的动态注册与发现,从此刻开始我们正式进入SpringCloud实战
Consul简介:
Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发。
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。它具有很多优点。包括: 基于 raft 协议,比较简洁; 支持健康检查, 同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持 Linux、Mac、Windows
如何在本地运行Consul?
在Consul的目录文件中进入cmd控制台,并执行:consul agent -dev
命令在本地开启Consul。就像Tomcat一样,通过网页本地的8500端口能够访问到Consul
在项目中引入Consul
- 提供者支付模块
首先引入Consul依赖
<!--SpringCloud consul discovery --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency>
配置yml文件
spring: ####Spring Cloud Consul for Service Discovery cloud: consul: host: localhost port: 8500 discovery: service-name: ${spring.application.name}
在主启动类中添加
@EnableDiscoveryClient
标识为在SpringBoot中开启Consul
消费者订单模块
同样的跟支付模块一样第一步需要先引入Consul依赖
编写配置文件【要在yml中写入application name,这个是在consul服务中心对应的服务名称】
server: port: 80 spring: application: name: cloud-consumer-order ####Spring Cloud Consul for Service Discovery cloud: consul: host: localhost port: 8500 discovery: service-name: ${spring.application.name}
解决之前的路径写死的问题,因为每个模块都在Consul中注册了对应的服务名,那么我们就可以通过对应的服务名来进行微服务间的通信【cloud-payment-service:为yml中注册进Consul的服务名】
public static final String PAYMENT_URL = "http://cloud-payment-service";
需要注意:因为Consul默认通信时是一个集群,若是单个微服务不是集群则要在配置
RestTemplate
的配置类中加入@LoadBalanced
开启负载均衡配置
以上就是通过Consul服务中心解决了微服务通信间url地址写死的问题。
分布式系统面临的问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。比如某些配置文件中的内容大部分都是相同的,只有个别的配置项不同。就拿数据库配置来说吧,如果每个微服务使用的技术栈都是相同的,则每个微服务中关于数据库的配置几乎都是相同的,有时候主机迁移了,我希望一次修改,处处生效。
那如何形成一套集中式的、动态的配置管理设施呢?
引入一个概念:bootstrap.yml
applicaiton.yml是用户级的资源配置项
bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的Application Context
的父上下文。初始化的时候,Bootstrap Context
负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment
。
Bootstrap
属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap context
和Application Context
有着不同的约定,所以新增了一个bootstrap.yml
文件,保证Bootstrap Context
和Application Context
配置的分离。
- 提供者支付模块:
首先引入两个依赖:
<!--SpringCloud consul config--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
创建bootstrap.yml,将关于Consul的配置写到该bootstrap.yml文件中
spring: application: name: cloud-payment-service ####Spring Cloud Consul for Service Discovery cloud: consul: host: localhost port: 8500 discovery: service-name: ${spring.application.name} config: profile-separator: '-' # default value is ",",we update '-' format: YAML # config/cloud-payment-service/data # /cloud-payment-service-dev/data # /cloud-payment-service-prod/data
consul服务器key/value配置填写
问题:接上一步,我们在consul的dev的配置分支修改了内容马上访问,结果无效,如下图:
解决方法:
在主启动类中添加
@RefreshScop
注解标识为实时更新,默认就会等55秒后进行刷新在一个基础上将
bootstrap.yml
加上watch:wait-time: 1
表示1s后刷新,【注意实际项目不建议修改】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: 1
仍存在的问题:若我们这时候重启Consoul服务器,上面写的key/value的文件还会存在吗?—— > 答案是不会,需要进行Consul配置的持久化。
LoadBalancer负载均衡
概述:简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用),常见的负载均衡有软件Nginx,LVS,硬件 F5等
spring-cloud-starter-loadbalancer组件是什么
Spring Cloud LoadBalancer是由SpringCloud官方提供的一个开源的、简单易用的客户端负载均衡器,它包含在SpringCloud-commons中用它来替换了以前的Ribbon组件。相比较于Ribbon,SpringCloud LoadBalancer不仅能够支持RestTemplate,还支持WebClient(WeClient是Spring Web Flux中提供的功能,可以实现响应式异步请求)
loadbalance和nginx的区别(面试题)
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的。
loadbalancer本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
LoadBalancer负载均衡有两种算法:随机和轮询,LoadBalancer默认的负载均衡策略为:轮询
负载均衡LoadBalancer实例演示:
引入依赖
<!--loadbalancer--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
在RestConfig配置类中的方法中添加上
@LoadBalanced
注解开启负载均衡
负载均衡算法实现原理:
负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。
ist instances = discoveryClient.getInstances(“cloud-payment-service”);
如: List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001
8001+ 8002 组合成为集群,它们共计2台机器,集群总数为2, 按照轮询算法原理:
当总请求数为1时: 1 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001
当总请求数位2时: 2 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
当总请求数位3时: 3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001
当总请求数位4时: 4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
如此类推…
负载均衡切换随机模式
修改RestConfig配置类:
- 在RestConfig类上加上
@LoadBalanceClient
注解,并设置value为你所要通信的微服务名,蛇者configuration为当前类 - 复制官网代码,创建出randomLoadBalancer并标注为Bean
@Configuration @LoadBalancerClient(value = "cloud-payment-service",configuration = RestTemplateConfig.class) public class RestTemplateConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } @Bean ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new RandomLoadBalancer(loadBalancerClientFactory .getLazyProvider(name, ServiceInstanceListSupplier.class), name); } }
OpenFeign
介绍openFeign:
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
openFeign是一个声明式的Web服务客户端。
只需要创建一个Rest接口(即创建一个接口)并在该接口上添加注解@FeignClient
标注为一个openFeign接口即可使用。
openFeign的优点
- 可插拔的注解支持,包括Feign注解和 JAX-RS 注解
- 支持可插拔的HTTP编码器和解码器
- 支持Sentinel和它的Fallback
- 支持SpringCloudLoadBalancer的负载均衡
- 支持HTPP请求和响应的压缩
为什么使用openFeign而不使用LoadBalancer
前面在使用SpringCloud LoadBalancer+RestTemplate时,利用RestTemplate对http请求的封装处理形成了一套模版化的调用方法。但是在实际开发中:
由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,OpenFeign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在OpenFeign的实现下,我们只需创建一个接口并使用注解的方式来配置它(在一个微服务接口上面标注一个**@FeignClient**注解即可),即可完成对服务提供方的接口绑定,统一对外暴露可以被调用的接口方法,大大简化和降低了调用客户端的开发量,也即由服务提供者给出调用接口清单,消费者直接通过OpenFeign调用即可,
OpenFeign同时还集成SpringCloud LoadBalancer
可以在使用OpenFeign时提供Http客户端的负载均衡,也可以集成阿里巴巴Sentinel来提供熔断、降级等功能。而与SpringCloud LoadBalancer不同的是,通过OpenFeign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。
代码演示使用openFeign
创建新的模块cloud-consumer-feign-order80
,引入依赖,创建application.yml
,修改主启动类并且添加上@FeignClient
注解表示开启
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
因为openFeign的原理是定义接口,所以这里将接口都放到公共部分中,即openFeign
接口都写在cloud-api-commons
模块中,
所以在cloud-api-commons
中需要添加openFeign的依赖
编写openFeign接口调用支付模块
@FeignClient("cloud-payment-service")//这里所要填写的值是调用哪个微服务模块 public interface PayFeignApi { @PostMapping("/pay/add") public ResultData addPay(@RequestBody PayDTO payDTO); @GetMapping("/pay/get/{id}") public ResultData getPayInfo(@PathVariable Integer id); @GetMapping("/pay/get.info") public ResultData mylb(); }
OpenFeign超时控制
在Spring Cloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会有多大问题的,但是如果是业务比较复杂,服务要进行比较繁杂的业务计算,那后台很有可能会出现Read Timeout这个异常,因此定制化配置超时时间就有必要了。
默认OpenFeign客户端等待60秒钟,但是服务端处理超过规定时间会导致Feign客户端返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制,默认60秒太长或者业务时间太短都不好
yml文件中开启配置:
- connectTimeout 连接超时时间
- readTimeout 请求处理超时时间
开启全局超时控制的配置:
spring: cloud: openfeign: client: config: default: #连接超时时间 connectTimeout: 3000 #读取超时时间 readTimeout: 3000
开启指定配置:
spring: cloud: openfeign: client: config: #指定调用某个微服务模块的超时时间 cloud-payment-service: connectTimeout: 8000 #连接超时时间 readTimeout: 8000 #读取超时时间
OpenFeign重试机制
OpenFeign重试机制默认是关闭的。
开启OpenFeign重试机制需要编写一个配置类,自定义重试:
@Configuration public class FeignConfig { @Bean public Retryer myRetryer(){ //最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s return new Retryer.Default(100,1,3); } }
OpenFeign默认HttpClient修改
OpenFeign中 http client 如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求。
由于默认HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不是最牛B的,所以加到最大。
所以这里必须将HttpURLConnection
替换成 Apache HttpClient 5
首先先引入依赖:
<!-- httpclient5--> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.3</version> </dependency> <!-- feign-hc5--> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-hc5</artifactId> <version>13.1</version> </dependency>
配置yml配置文件,开启
Apache HttpClient 5
# Apache HttpClient5 配置开启 spring: cloud: openfeign: httpclient: hc5: enabled: true
OpenFeign高级特性
对请求和响应进行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 #最小触发压缩的大小
spring: cloud: openfeign: compression: request: enabled: true min-request-size: 2048 #最小触发压缩的大小 mime-types: text/xml,application/xml,application/json #触发压缩数据类型 response: enabled: true
OpenFeign日志打印功能
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节,说白了就是对Feign接口的调用情况进行监控和输出
开启日志打印的步骤:
- 编写配置类,配置Logger的级别
@Bean public Logger.Level feignLoggerLevel(){ return Logger.Level.BASIC; }
在yml配置文件中指定对哪个包下的哪个类进行日志监控,配置公式:
公式(三段):logging.level + 含有@FeignClient注解的完整带包名的接口名+debug#开启日志监控 logging: level: com.zsc.hines.api: com.zsc.hines.api.PayFeignApi: debug
CircuitBreaker断路器【重点】
分布式系统面临的问题:复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
如何解决服务雪崩
有问题的节点,快速熔断(快速返回失败处理或者返回默认兜底数据【服务降级】)。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),==向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,==这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
CircuitBreaker简介:
CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
当一个组件或服务出现故障时,==CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。==这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
注意:Circuit Breaker只是一套规范和接口,落地实现者式Resilience4J
Resilience4J的简介:
核心模块:
- resilience4j-Circuitbreaker:断路
- resilience4j-ratelimiter:速率限制
- resilience4j-bulkhead:舱壁
熔断(服务熔断+服务降级)
按计数的滑动窗口
常用的API:
配置yml文件:
spring: cloud: #全局配置 openfeign: # 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled circuitbreaker: enabled: true group: enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后 # Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子 # 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。 # 等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。 # 如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。 resilience4j: circuitbreaker: configs: default: failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。 slidingWindowType: COUNT_BASED # 滑动窗口的类型 slidingWindowSize: 6 #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒 minimumNumberOfCalls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。 automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常 waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间 permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。 recordExceptions: - java.lang.Exception instances: cloud-payment-service: baseConfig: default
按照时间的滑动窗口
修改配置文件yml:
spring: ####Spring Cloud Consul for Service Discovery cloud: #全局配置 openfeign: # 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled circuitbreaker: enabled: true group: enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后 # Resilience4j CircuitBreaker 按照时间:TIME_BASED 的例子 resilience4j: timelimiter: configs: default: timeout-duration: 10s #神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑 circuitbreaker: configs: default: failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。 slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例。 slowCallRateThreshold: 30 #慢调用百分比峰值,断路器把调用时间⼤于slowCallDurationThreshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级 slidingWindowType: TIME_BASED # 滑动窗口的类型 slidingWindowSize: 2 #滑动窗口的大小配置,配置TIME_BASED表示2秒 minimumNumberOfCalls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。 permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。 waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间 recordExceptions: - java.lang.Exception instances: cloud-payment-service: baseConfig: default
隔离(resilience4j-bulkhead)
什么是resilience4j-bulkhead
?
Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量。
SemaphoreBulkhead
使用了信号量FixedThreadPoolBulkhead
使用了有界队列和固定大小线程池
SemaphoreBulkhead
可以在各种线程和I/O模型上正常工作。与Hystrix不同,它不提供基于shadow的thread选项。由客户端来确保正确的线程池大小与隔离配置一致。
Bulkhead配置属性
实现SemaphoreBulkhead(信号量舱壁)
当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,
如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。
若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。
代码演示
在
cloud-provider-payment8001
模块中的PayCircuitController
类中添加一个方法:@GetMapping(value = "/pay/bulkhead/{id}") public String myBulkhead(@PathVariable("id") Integer id) { if(id == -4) throw new RuntimeException("----bulkhead id 不能-4"); if(id == 9999) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } return "Hello, bulkhead! inputId: "+id+" \t " + IdUtil.simpleUUID(); }
在
PayFeignApi
接口中新增舱壁api方法:@GetMapping(value = "/pay/bulkhead/{id}") public String myBulkhead(@PathVariable("id") Integer id);
在
cloud-consumer-feign-order80
模块中添加bulk依赖:<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-bulkhead</artifactId> </dependency>
在
cloud-consumer-feign-order80
模块中配置yml文件####resilience4j bulkhead 的例子 resilience4j: bulkhead: configs: default: maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量 maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback instances: cloud-payment-service: baseConfig: default timelimiter: configs: default: timeout-duration: 20s
在
OrderCircuitController
中添加接口使用@Bulkhead
启动舱壁隔离/** *(船的)舱壁,隔离 * @param id * @return */ @GetMapping(value = "/feign/pay/bulkhead/{id}") //第一个参数name表示你的舱壁隔离用在哪,fallbackMethod兜底策略将会执行自定义的兜底方法,type舱壁隔离的类型 @Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadFallback",type = Bulkhead.Type.SEMAPHORE) public String myBulkhead(@PathVariable("id") Integer id) { return payFeignApi.myBulkhead(id); } public String myBulkheadFallback(Throwable t) { return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~"; }
测试结果:
固定线程池舱壁(FixedThreadPoolBulkhead)
FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。
当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。
当线程池中无空闲时时,接下来的请求将进入等待队列,
若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,
在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法
FixedThreadPoolBulkhead配置
既然FixedThreadPoolBulkhead的原理是通过线程池来控制并发,该如何配置线程池呢,——> 通过yml文件可以配置线程池的最大线程数,核心线程数和阻塞队列,【切记最大线程池包含核心线程池】(下面的配置中当线程数达到6时将不再接收请求)
代码演示
添加依赖:【跟信号量舱壁的依赖一样】
<!--resilience4j-bulkhead--> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-bulkhead</artifactId> </dependency>
修改配置yml配置文件
thread-pool-bulkhead: configs: default: core-thread-pool-size: 1 max-thread-pool-size: 1 queue-capacity: 1 instances: cloud-payment-service: baseConfig: default
添加新的接口【跟信号量舱壁相比只是将类型改为固定线程池舱壁】
/** * (船的)舱壁,隔离,THREADPOOL * @param id * @return */ @GetMapping(value = "/feign/pay/bulkhead/{id}") @Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadPoolFallback",type = Bulkhead.Type.THREADPOOL) public CompletableFuture<String> myBulkheadTHREADPOOL(@PathVariable("id") Integer id) { System.out.println(Thread.currentThread().getName()+"\t"+"enter the method!!!"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t"+"exist the method!!!"); return CompletableFuture.supplyAsync(() -> payFeignApi.myBulkhead(id) + "\t" + " Bulkhead.Type.THREADPOOL"); } public CompletableFuture<String> myBulkheadPoolFallback(Integer id,Throwable t) { return CompletableFuture.supplyAsync(() -> "Bulkhead.Type.THREADPOOL,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~"); }
测试结果:
限流
限流的基本概述:限流 就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。
(如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。)
所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。
限流算法:
漏桶算法
一个固定容量的漏桶,按照设定常量固定速率流出水滴,类似医院打吊针,不管你源头流量多大,我设定匀速流出。 如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。
存在的缺陷:
这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
令牌桶算法(SpringCloud默认使用的限流算法)
概述:你可以设置令牌的数量,每一个请求都需要用令牌才能执行,执行完后,将令牌回收,将回收的令牌派发给正在排队的请求。
滚动窗口算法
允许固定数量的请求进入(比如1秒取4个数据相加,超过25值就over)超过数量就拒绝或者排队,等下一个时间段进入。
由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。下图统计了3次,
存在的缺陷:由于计数器算法存在时间临界点缺陷,因此在时间临界点左右的极短时间段内容易遭到攻击。
假如设定1分钟最多可以请求100次某个接口,如12:00:00-12:00:59时间段内没有数据请求但12:00:59-12:01:00时间段内突然并发100次请求,紧接着瞬间跨入下一个计数周期计数器清零;在12:01:00-12:01:01内又有100次请求。那么也就是说在时间临界点左右可能同时有2倍的峰值进行请求,从而造成后台处理请求加倍过载的bug,导致系统运营能力不足,甚至导致系统崩溃,/(ㄒoㄒ)/~~
滚动窗口算法会将时间划分为固定的窗口(例如每秒一个窗口),在每个窗口内计数请求。如果某个窗口内的请求数超过了设定的阈值,就会触发限流。这种方法在面对突发请求时,可能无法及时响应,因为每个窗口的阈值是固定的,无法动态调整。
(简单来说就是这种算法存在着一个时间间隔,当间隙时突然来了100各请求,就把服务器压垮了)
滑动时间窗口
顾名思义,该时间窗口是滑动的。所以,从概念上讲,这里有两个方面的概念需要理解:
- 窗口:需要定义窗口的大小
- 滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小
滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。下图统计了5次:
代码演示
在
cloud-provider-payment8001
模块中PayCircuitController
中新增方法进行测试://=========Resilience4j ratelimit 的例子 @GetMapping(value = "/pay/ratelimit/{id}") public String myRatelimit(@PathVariable("id") Integer id) { return "Hello, myRatelimit欢迎到来 inputId: "+id+" \t " + IdUtil.simpleUUID(); }
PayFeignApi
中新增限流方法进行测试:/** * Resilience4j Ratelimit 的例子 * @param id * @return */ @GetMapping(value = "/pay/ratelimit/{id}") public String myRatelimit(@PathVariable("id") Integer id);
修改
cloud-consumer-feign-order80
,引入resilience4j
限流依赖:<!--resilience4j-ratelimiter--> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-ratelimiter</artifactId> </dependency>
修改yml文件配置:
yml文件中的配置说明:
####resilience4j ratelimiter 限流的例子 resilience4j: ratelimiter: configs: default: limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数 # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod limitRefreshPeriod: 1s timeout-duration: 1 # 线程等待权限的默认等待时间 instances: cloud-payment-service: baseConfig: default
在
orderController
中新增方法:使用@RateLimiter
注解开启限流@GetMapping(value = "/feign/pay/ratelimit/{id}") @RateLimiter(name = "cloud-payment-service",fallbackMethod = "myRatelimitFallback") public String myBulkhead(@PathVariable("id") Integer id) { return payFeignApi.myRatelimit(id); } public String myRatelimitFallback(Integer id,Throwable t) { return "你被限流了,禁止访问/(ㄒoㄒ)/~~"; }
测试结果:
Micrometer + ZipKin 分布式链路追踪
Micronmeter简介:
Micrometer 为最流行的可观察性系统提供了一个简单的仪表客户端外观,让您可以对基于 JVM 的应用程序代码进行仪表化,而无需供应商锁定。想想 SLF4J,但用于应用程序可观察性!Micrometer 记录的数据旨在用于观察、警告和响应您环境的当前/近期运行状态
为什么会出现这个技术?
答: 在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
- 在分布式微服务场景下,我们需要解决如下问题:
- 在大规模分布式与微服务集群下,如何实时观测系统的整体调用链路情况。
- 在大规模分布式与微服务集群下,如何快速发现并定位到问题。
- 在大规模分布式与微服务集群下,如何尽可能精确的判断故障对系统的影响范围与影响程度。
- 在大规模分布式与微服务集群下,如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。
- 在大规模分布式与微服务集群下,如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。
- 在大规模分布式与微服务集群下,如何尽可能精确的分析系统的存储瓶颈与容量规划。
总结:分布式链路追踪技术要解决的问题,分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
分布式链路追踪原理
假定三个微服务调用的链路如下图所示:Service 1 调用 Service 2,Service 2 调用 Service 3 和 Service 4。
那么一条链路追踪会在每个服务调用的时候加上Trace ID 和 Span ID
链路通过TraceId唯一标识,
Span标识发起的请求信息,各span通过parent id 关联起来 (Span:表示调用链路来源,通俗的理解span就是一次请求信息)
一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来
1 | 第一个节点:Span ID = A,Parent ID = null,Service 1 接收到请求。 |
---|---|
2 | 第二个节点:Span ID = B,Parent ID= A,Service 1 发送请求到 Service 2 返回响应给Service 1 的过程。 |
3 | 第三个节点:Span ID = C,Parent ID= B,Service 2 的 中间解决过程。 |
4 | 第四个节点:Span ID = D,Parent ID= C,Service 2 发送请求到 Service 3 返回响应给Service 2 的过程。 |
5 | 第五个节点:Span ID = E,Parent ID= D,Service 3 的中间解决过程。 |
6 | 第六个节点:Span ID = F,Parent ID= C,Service 3 发送请求到 Service 4 返回响应给 Service 3 的过程。 |
7 | 第七个节点:Span ID = G,Parent ID= F,Service 4 的中间解决过程。 |
8 | 通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。 |
Zipkin概述
Zipkin是一种分布式链路跟踪系统图形化的工具,Zipkin 是 Twitter 开源的分布式跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调用链路信息展示到Web图形化界面上供开发人员分析,开发人员能够从ZipKin中分析出调用链路中的性能瓶颈,识别出存在问题的应用程序,进而定位问题和解决问题
代码演示(使用Zipkin和Micrometer)
在父工程中添加以下依赖:
<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>
在
cloud-provider-payment8001
模块中引入以下依赖:<!--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>
在
cloud-provider-payment8001
模块配置yml文件:# ========================zipkin=================== management: zipkin: tracing: endpoint: http://localhost:9411/api/v2/spans tracing: sampling: probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
新建控制器
PayMicrometerController
:/** * @auther zzyy * @create 2023-11-17 17:58 */ @RestController public class PayMicrometerController { /** * Micrometer(Sleuth)进行链路监控的例子 * @param id * @return */ @GetMapping(value = "/pay/micrometer/{id}") public String myMicrometer(@PathVariable("id") Integer id) { return "Hello, 欢迎到来myMicrometer inputId: "+id+" \t 服务返回:" + IdUtil.simpleUUID(); } }
在
cloud-api-commons
模块中的PayFeignApi
接口中添加以下接口:/** * Micrometer(Sleuth)进行链路监控的例子 * @param id * @return */ @GetMapping(value = "/pay/micrometer/{id}") public String myMicrometer(@PathVariable("id") Integer id)
在
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>
在
cloud-consumer-feign-order80
模块中修改yml配置文件:# zipkin图形展现地址和采样率设置 management: zipkin: tracing: endpoint: http://localhost:9411/api/v2/spans tracing: sampling: probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
在
cloud-consumer-feign-order80
模块中新增控制器,新建OrderMicrometerController
接口:@RestController @Slf4j public class OrderMicrometerController { @Resource private PayFeignApi payFeignApi; @GetMapping(value = "/feign/micrometer/{id}") public String myMicrometer(@PathVariable("id") Integer id) { return payFeignApi.myMicrometer(id); } }
Gateway新一代网关
Gateway简介:
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring6,Spring Boot 3和Project Reactor等技术。它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,并为它们提供跨领域的关注点,例如:安全性、监控/度量和恢复能力。
前端通过Nginx转发请求到后端,后端通过GetWay转发到对应的微服务模块。
Gateway的功能:
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
Gateway原理:
Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心。
Gateway三大核心概念:
- Route(路由):路由是构建网关的基本模块,它由ID,目标URL,一系列的断言和过滤器组成,如果断言为true则匹配该路由
- Predicate(断言):参考的是java8中
java.util.funtion.Predicate
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由 - Filter(过滤器):指的是Spring框架中GetwayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改
- web前端请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
- predicate 就是我们的匹配条件;
- filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
Gateway工作流程【核心:路由转发+断言判断+执行过滤链】
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。
在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;
在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
Gateway代码演示
首先说明演示的流程:Gateway也是微服务的一部分(SpringCloud Gateway)所以第一步需要将Gateway注册到注册中心,在 cloud-consumer-order80 模块 调用 cloud-provider-payment8001模块前加上Getway网关。
创建Gateway模块,命名为
cloud-getway9527
引入依赖:
<dependencies> <!--gateway--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
配置yml文件:
server: port: 9527 spring: application: name: cloud-gateway #以微服务注册进consul或nacos服务列表内 cloud: consul: #配置consul地址 host: localhost port: 8500 discovery: prefer-ip-address: true service-name: ${spring.application.name}
修改主启动类:
@SpringBootApplication @EnableDiscoveryClient //服务注册和发现 public class Main9527 { public static void main(String[] args) { SpringApplication.run(Main9527.class,args); } }
将Gateway到8001模块打通
配置完成网关微服务模块后,我们配置提供者colud-provider-payment8001
模块,我们不想暴露8001端口,希望在8001真正的支付微服务外面套一层3527网关。进行如下配置:
在提供者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()); } }
配置Gateway对提供者8001模块的路由判断与断言:【在Gateway模块中的yml文件中进行配置】
具体以下参数:
- 配置一个
id
,自定义一个标识 - 配置一个
uri
,本机ip+提供者模块端口 - 配置
predicates
断言,对访问的地址进行断言限制
server: port: 9527 spring: application: name: cloud-gateway #以微服务注册进consul或nacos服务列表内 cloud: consul: #配置consul地址 host: localhost port: 8500 discovery: prefer-ip-address: true service-name: ${spring.application.name} gateway: routes: - id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名 uri: http://localhost:8001 #匹配后提供服务的路由地址 predicates: - Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由 - id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名 uri: http://localhost:8001 #匹配后提供服务的路由地址 predicates: - Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
测试结果说明:
重启Gateway9527模块后,我们访问:http://localhost:8001/pay/gateway/get/1
,这个是肯定没问题的,访问http://localhost:9527/pay/gateway/get/1
,通过9527这个端口访问8001模块的的接口也同样通过,自此就打通了网关到提供者的路由配置
将订单模块通过feign访问到Gateway网关打通
在
cloud-api-commons
模块中的PayFeignApi
接口中添加以下两个方法:/** * GateWay进行网关测试案例01 * @param id * @return */ @GetMapping(value = "/pay/gateway/get/{id}") public ResultData getById(@PathVariable("id") Integer id); /** * GateWay进行网关测试案例02 * @return */ @GetMapping(value = "/pay/gateway/info") public ResultData<String> getGatewayInfo();
在
cloud-consumer-feign-order80
模块中创建一个新的控制器OrderGateController
:@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(); } }
修改
cloud-api-commons
的PayFeignApi接口,将@FeignClient
注解修改为@FeignClient("cloud-gateway")
,意思为:访问网关微服务
这样整条链路调用成功。
Gateway高级特性
特性一:Route以微服务名 - 动态获取服务URL
存在的问题:
使用动态获取服务URL来解决URL写死的问题:
修改Gateway模块的yml配置文件:【lb为负载均衡,后面是所访问的服务名】
routes: - id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名 #uri: http://localhost:8001 #匹配后提供服务的路由地址 uri: lb://cloud-payment-service predicates: - Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
特性二:Predicate断言(谓词)
Spring Cloud Gateway 包含许多内置的Route Predicate 工厂。所有这些 Predicate 都与 HTTP 请求的不同属性匹配。对各 Route Predicate 工厂进行组合
配置断言有两种语法:
Shortcut Configuration
使用等于号和逗号进行配置
Fully Expanded Arguments
这种方式类似于yml的语法,使用的key-value进行配置
After/Before/Between Route Predicate Factory(在某个时间点之后才开放)
路由After
谓词工厂采用一个参数 a datetime
(即 java ZonedDateTime
)。此谓词匹配在指定日期时间之后发生的请求。
那相对应的Before
和Between
就是在某个时间点之前才能访问,在某个时间点之间才能访问。
应用场景:预约活动要在某个时间之后才能访问…等等
格式:- After=2017-01-20T17:42:47.789-07:00[America/Denver]
实例配置:
spring: cloud: gateway: routes: - id: after_route uri: https://example.org predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver]
问题:这个时间点该怎么写? —— > 使用java的时区,随便定义一个普通的java类编写以下代码就能生成时间格式:
public class ZonedDateTimeDemo { public static void main(String[] args) { ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区 System.out.println(zbj); } }
- Before Route Predicate Factory示例代码:
spring: cloud: gateway: routes: - id: before_route uri: https://example.org predicates: - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
Between Route Predicate Factory示例代码:
spring: cloud: gateway: routes: - id: between_route uri: https://example.org predicates: - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
Cookie Route Predicate Factory
Cookie Route Predicate需要两个参数,一个是 Cookie name ,一个是正则表达式。路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行
其主要功能就是限制前端访问需要携带上我对应配置的cookie才能让网关放行。
设置cookie配置示例如下:
spring: cloud: gateway: routes: - id: cookie_route uri: https://example.org predicates: - Cookie=chocolate, ch.p
浏览器访问需要进入开发者模式进行cookie的设置进行测试:
Header Route Predicate
前端访问必须要有Getway所对应配置的请求头
示例:
两个参数:一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
spring: cloud: gateway: routes: - id: header_route uri: https://example.org predicates: - Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
测试结果:
Host Route Predicate
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。
示例:
spring: cloud: gateway: routes: - id: host_route uri: https://example.org predicates: - Host=**.atguigu.com
测试结果:
Path Route Predicate
必须是所配置的路由
示例:
spring: cloud: gateway: routes: - id: path_route uri: https://example.org predicates: - Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
Query Route Predicate
只能是对应的请求参数才能放行
spring: cloud: gateway: routes: - id: query_route uri: https://example.org predicates: - Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
测试结果:
RemoteAddr route predicate
只有指定的主机地址才能放行
配置示例:
spring: cloud: gateway: routes: - id: remoteaddr_route uri: https://example.org predicates: - RemoteAddr=192.168.124.1/24 # 外部访问我的IP限制,最大跨度不超过32,目前是1~24它们是 CIDR 表示法。
Method Route Predicate
只有指定的请求方法才能放行
spring: cloud: gateway: routes: - id: method_route uri: https://example.org predicates: - Method=GET,POST
自定义断言【重点】
观察AfterRouterPredicateFactory
类的源码,我们可以看到大致的步骤:
前置规则:自定义一个断言的类名一定要以xxxRouterPredicateFactory
命名,并且将该类用@component注解标识
代码演示(场景:自定义配置会员等级userTyp,按照钻/金/银等级,判断是否可以访问)
- 继承
AbstractRouterPredicateFactory
类并且该泛型为`xxxRouterPredicateFactory.Config - 编写一个内部类,其命名一定要为
Config
,该类定义一个userType(因为这里的场景是根据会员等级判断是否放行) - 编写空参的构造方法,并且使用super调用上一步定义的Config类
- 实现
apply
方法,该方法中return new Predicate,并在这里写判断逻辑 - 若想yml文件中支持 Shortcut Configuration 这种配置文件需要实现
shortcutFieldOrder
方法
//自定义配置会员等级userTyp,按照钻/金/银等级,判断是否可以访问 @Component public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config> { public MyRoutePredicateFactory(){ super(MyRoutePredicateFactory.Config.class); } //支持Shortcut Configuration格式 @Override public List<String> shortcutFieldOrder() { return Collections.singletonList("userType"); } @Override public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config) { return new Predicate<ServerWebExchange>() { @Override public boolean test(ServerWebExchange serverWebExchange) { //serverWebExchange类似于request,获取请求参数userType对应的值进行会员逻辑判断 String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType"); if(userType == null){ return false; } //判断前端传过来的userType是否和yml文件定义的相同 if(userType.equals(config.getUserType())){ return true; } return false; } }; } @Validated public static class Config{ @Getter @Setter @NotEmpty private String userType; } }
特性三:Filter(过滤器)
类似于 SpringMVC里面的拦截器 Interceptor,Servlet的过滤器,"pre"和"post"分别在请求被执行前调用和被执行后调用,用来修改请求和响应信息。
Filter的主要作用:
- 请求鉴权
- 异常处理
- 记录接口调用时长统计(大厂面试题)
Filter过滤器的类型:
- 全局默认过滤器 Global Filters:gateway出厂默认已有的,直接用即可,主要作用于所有的路由,不需要在配置文件中配置,作用在所有的路由上,实现GlobalFilter接口即可
- 单一内置过滤器 Getway Filter:也可以称为网关过滤器,这种过滤器主要是作用于单一路由或者某个路由分组
- 自定义过滤器
Gateway内置过滤器
请求头(RequestHeader)相关组
- The AddRequestHeader GatewayFilter Factory(指定请求头内容)
在8001微服务payGatewayController
新增方法:
@GetMapping(value = "/pay/gateway/filter") public ResultData<String> getGatewayFilter(HttpServletRequest request) { String result = ""; Enumeration<String> headers = request.getHeaderNames(); while(headers.hasMoreElements()) { String headName = headers.nextElement(); String headValue = request.getHeader(headName); System.out.println("请求头名: " + headName +"\t\t\t"+"请求头值: " + headValue); if(headName.equalsIgnoreCase("X-Request-atguigu1") || headName.equalsIgnoreCase("X-Request-atguigu2")) { result = result+headName + "\t " + headValue +" "; } } return ResultData.success("getGatewayFilter 过滤器 test: "+result+" \t "+ DateUtil.now()); }
这段代码获取前端发送请求的请求信息,并且进行对逻辑进行判断。
在9527网关yml配置文件中添加过滤内容:
- id: pay_routh3 #pay_routh3 uri: lb://cloud-payment-service #匹配后提供服务的路由地址 predicates: - Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由 filters: - AddRequestHeader=X-Request-atguigu1,atguiguValue1 # 请求头kv,若一头含有多参则重写一行设置 - AddRequestHeader=X-Request-atguigu2,atguiguValue2
访问地址:http://localhost:9527/pay/gateway/filter 后查看8001控制台如下图,说明请求信息成功添加了我们所配置的信息
- The RemoveRequestHeader GatewayFilter Factory(删除请求头)
删除所指定配置的请求头信息
编写yml配置:
- RemoveRequestHeader=sec-fetch-site # 删除请求头sec-fetch-site
重启服务,并访问:http://localhost:9527/pay/gateway/filter,结果如下两图
修改前:
修改后:
- The SetRequestHeader GatewayFilter Factory(修改请求头信息)
配置yml文件:
- SetRequestHeader=sec-fetch-mode, Blue-updatebyzzyy # 将请求头sec-fetch-mode对应的值修改为Blue-updatebyzzyy
重启服务,并访问:http://localhost:9527/pay/gateway/filter,结果如下两图
修改前:
修改后:
请求参数(RequestParameter)相关组
The AddRequestParameter GatewayFilter Factory:*新增请求参数Parameter:*k *,*v
The RemoveRequestParameter GatewayFilter Factory:*新增请求参数Parameter:*k *,*v
配置yml文件:
- AddRequestParameter=customerId,9527001 # 新增请求参数Parameter:k ,v - RemoveRequestParameter=customerName # 删除url请求参数customerName,你传递过来也是null
为了方便测试,修改8001微服务的PayGatewayController
中的getGatewayFilter
方法:
@GetMapping(value = "/pay/gateway/filter") public ResultData<String> getGatewayFilter(HttpServletRequest request) { String result = ""; Enumeration<String> headers = request.getHeaderNames(); while(headers.hasMoreElements()) { String headName = headers.nextElement(); String headValue = request.getHeader(headName); System.out.println("request headName:" + headName +"---"+"request headValue:" + headValue); if(headName.equalsIgnoreCase("X-Request-atguigu1") || headName.equalsIgnoreCase("X-Request-atguigu2")) { result = result+headName + "\t " + headValue +" "; } } System.out.println("============================================="); String customerId = request.getParameter("customerId"); System.out.println("request Parameter customerId: "+customerId); String customerName = request.getParameter("customerName"); System.out.println("request Parameter customerName: "+customerName); System.out.println("============================================="); return ResultData.success("getGatewayFilter 过滤器 test: "+result+" \t "+ DateUtil.now()); }
进行测试:
访问:http://localhost:9527/pay/gateway/filter,得出如下结果:
访问:http://localhost:9527/pay/gateway/filter?customerId=9999&customerName=z3 ,得出如下结果:
回应头(ResponseHeader)相关组
- The AddRepsonseHeader GatewayFilter Factory
- The SetRepsonseHeader GateFilter Factory
- The RemoveResonseHeader GateFilter Factory
修改yml配置文件:
- AddResponseHeader=X-Response-atguigu, BlueResponse # 新增请求参数X-Response-atguigu并设值为BlueResponse - SetResponseHeader=Date,2099-11-11 # 设置回应头Date值为2099-11-11 - RemoveResponseHeader=Content-Type # 将默认自带Content-Type回应属性删除
进行测试:访问:http://localhost:9527/pay/gateway/filter,测试结果如下:
原本的回应信息是这样的:
修改后的回应信息:
前缀和路径相关组
- The PrefixPath GatewayFilter Factory(自动添加路径前缀)
场景:为了保护地址,我们可以使用隐藏掉前缀的方法进行真实地址的隐藏,原正确地址:http://localhost:9527/pay/gateway/filter,这个地址也是我们控制层所映射的真实地址,通过以下yml文件的修改后,该地址访问不同,访问:http://localhost:9527/gateway/filter,才能通过,实际就是把前缀隐藏了。
predicates: #- Path=/pay/gateway/filter/** # 被分拆为: PrefixPath + Path - Path=/gateway/filter/** # 断言,为配合PrefixPath测试过滤,暂时注释掉/pay filters: - PrefixPath=/pay # http://localhost:9527/pay/gateway/filter
测试结果:
- The SetPath GatewayFilter Factory(访问路径修改)
对访问路径的修改,目的也是为了隐藏后端真实的地址。
predicates: - Path=/XYZ/abc/{segment} # 断言,为配合SetPath测试,{segment}的内容最后被SetPath取代 filters: #- PrefixPath=/pay # http://localhost:9527/pay/gateway/filter - SetPath=/pay/gateway/{segment} # {segment}表示占位符,你写abc也行但要上下一致
yml配置文件说明:【对于这个{segment},意思为前端传过来什么我就对应映射什么,比如:前端传来 /XYZ/abc/abc,后端就会映射成/page/gateway/abc,所以只要前端传来的路径前缀包含/XYZ/abc
能对应得上就会映射成/pay/gateway/
】
{segment}就是个占位符,等价于SetPath后面指定的{segment}内容
测试结果:
- The RedirectTo GatewayFilter Factory
重定向到某个页面
predicates: - Path=/pay/gateway/filter/** # 真实地址 filters: - RedirectTo=302, http://www.atguigu.com/ # 访问http://localhost:9527/pay/gateway/filter跳转到http://www.atguigu.com/
Gateway自定义过滤器
问题:如何统计每个接口调用耗时情况,——> 使用Gateway自定义过滤器。
如何自定义Gateway过滤器?
参考官网自定义过滤器的示例代码:
@Bean public GlobalFilter customFilter() { return new CustomGlobalFilter(); } public class CustomGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("custom global filter"); return chain.filter(exchange); } @Override public int getOrder() { return -1; } }
步骤:
- 新建一个类标注为
@Component
,并且该类必须实现GlobalFilter
和Ordered
两个接口并重写其接口方法 - 因为这里的场景是要开始的时间和结束的时间,在
ServerWebExchange
类中用一个getAttributes
的方法能够存放Map,所以我们可以定义一个常量key,value就是当前时间System.currentTimeMillis
- 参照官网示例,
return chain.filter(exchange)
,但这里我们需要写时间记录逻辑,所以写为return chain.filter(exchange).then(Mono.fromRunnable(() ->{}))
@Component @Slf4j public class MyGlobalFilter implements GlobalFilter, Ordered { public static final String BEGIN_VISIT_TIME = "begin_visit_time"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //记录下接口一开始执行的时间 exchange.getAttributes().put(BEGIN_VISIT_TIME,System.currentTimeMillis()); return chain.filter(exchange).then(Mono.fromRunnable(() ->{ //首先获取到接口一开始执行的时间 Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME); if(beginVisitTime != null){ log.info("访问接口主机: " + exchange.getRequest().getURI().getHost()); log.info("访问接口端口: " + exchange.getRequest().getURI().getPort()); log.info("访问接口URL: " + exchange.getRequest().getURI().getPath()); log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery()); log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms"); log.info("我是美丽分割线: ###################################################"); System.out.println(); } })); } @Override public int getOrder() { return 0; } }
自定义条件GatewayFilter
上面的这种方式是全局的网关Filter过滤器,下面要进行条件网关过滤的配置,步骤如下:
- 新建类名XXX需要以GatewayFilterFactory结尾并继承AbstractGatewayFilterFactory类,重写其抽象方法
- 新建XXXGatewayFilterFactory.Config内部类,这个类写的是对应业务逻辑的变量
- 在重写的
apply
方法中编写业务逻辑 - 重写
shortcutFieldOrder
,重写这个类是为了能够支持yml配置文件短促式格式 - 构造空参构造方法,调用super
@Component public class MyGateFilterFactory extends AbstractGatewayFilterFactory<MyGateFilterFactory.Config> { public MyGateFilterFactory(){ super(MyGateFilterFactory.Config.class); } public static class Config{ @Setter @Getter private String status; } @Override public GatewayFilter apply(MyGateFilterFactory.Config config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); if(request.getQueryParams().containsKey("atguigu")){ return chain.filter(exchange); }else{ exchange.getResponse().setStatusCode(HttpStatus.BAD_GATEWAY); return exchange.getResponse().setComplete(); } } }; } @Override public List<String> shortcutFieldOrder() { return Collections.singletonList("status"); } }
SpringCloud alibaba Nacos
一句话描述:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。(替代了Consul)
Nacos就是注册中心 + 配置中心的组合
Nacos = Eureka + Config + Bus
Nacos = Spring Cloud Consul
各种注册中心的比较:
(据说 Nacos 在阿里巴巴内部有超过 10 万的实例运行,已经过了类似双十一等各种大型流量的考验,Nacos默认是AP模式,但也可以调整切换为CP,我们一般用默认AP即可。)
如何启动Nacos?
进入到 E:\nacos-server2.2.3\bin 目录,打开cmd窗口,执行以下命令:(非集群模式)
startup.cmd -m standalone
Nacos代码实战
创建模块cloudalibaba-provider-payment8001
,添加依赖,添加yml配置文件,修改主启动类,新建控制层
<dependencies> <!--nacos-discovery--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 引入自己定义的api通用包 --> <dependency> <groupId>com.atguigu.cloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--SpringBoot通用依赖模块--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> <scope>provided</scope> </dependency> <!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
server: port: 9001 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 #配置Nacos地址
@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; } }
创建消费者模块cloudalibaba-consumer-order83
,添加依赖,添加yml配置文件,修改主启动类,新建控制层,配置restemplate
进行微服务之间负载均衡的调用
<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>
server: port: 83 spring: application: name: nacos-order-consumer cloud: nacos: discovery: server-addr: localhost:8848 #消费者将要去访问的微服务名称(nacos微服务提供者叫什么你写什么) service-url: nacos-user-service: http://nacos-payment-provider
@Configuration public class RestTemplateConfig { @Bean @LoadBalanced //赋予RestTemplate负载均衡的能力 public RestTemplate restTemplate() { return new RestTemplate(); } }
@RestController public class OrderNacosController { @Resource private RestTemplate restTemplate; @Value("${service-url.nacos-user-service}") private String serverURL; @GetMapping("/consumer/pay/nacos/{id}") public String paymentInfo(@PathVariable("id") Integer id) { String result = restTemplate.getForObject(serverURL + "/pay/nacos/" + id, String.class); return result+"\t"+" 我是OrderNacosController83调用者。。。。。。"; } }
使用快捷方式创建多一个提供者9002
Nacos Config服务配置中心
在之前的案例Consil8500服务配置动态变更功能可以被Nacos取代,通过Nacos和spring-cloud-starter-alibaba-nacos-config实现中心化全局配置的动态变更
基本的配置步骤:建Moudle,引入依赖,改主启动类,添加控制器编写业务逻辑。
<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>
@RestController @RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。 public class NacosConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/config/info") public String getConfigInfo() { return configInfo; } }
注意要写两个配置文件application.uml
,bootstrap.yml
,原因:Nacos同Consul一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动,为了满足动态刷新和全局广播通知springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
bootstrap.yml:
# nacos配置 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 config: server-addr: localhost:8848 #Nacos作为配置中心地址 file-extension: yaml #指定yaml格式的配置 # nacos端配置文件DataId的命名规则是: # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} # 本案例的DataID是:nacos-config-client-dev.yaml
application.yml:
server: port: 3377 spring: profiles: active: dev # 表示开发环境 #active: prod # 表示生产环境 #active: test # 表示测试环境
Nacos Config服务配置中心yml文件配置公式:【本案例的DataID是:nacos-config-client-dev.yaml】${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
创建Nacos配置管理,实现灰度发布:
注意:Nacos会记录配置文件的历史版本默认保留30天,此外还有一键回滚功能,回滚操作将会触发配置更新
Nacos数据模型之Namespace-Group-DataId【未完成】
问题1:实际开发中,通常一个系统会准备:dev开发环境test测试环境,prod生产环境。如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
问题2:一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境… 那怎么对这些微服务配置进行分组和命名空间管理呢?
是什么:类似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是对指定微服务的一个虚拟划分。见下一节:服务领域模型-补充说明
Spring Cloud Alibaba Sentinel(实现熔断与限流)
Sentinel是什么(基本概念):
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
Sentinel哨兵机制的原理(流量降级与容错标准):
Sentinel常见面试题(高并发解决的问题)
服务雪崩
(简单来说就是当一个微服务突然面对高并发的请求处理不过来的时候会影响该微服务调用的其他模块和调用他的模块,形成雪崩)
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务降级
服务降级,说白了就是一种服务托底方案,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。
例如,在商品详情页一般都会展示商品的介绍信息,一旦商品详情页系统出现故障无法调用时,会直接获取缓存中的商品介绍信息返回给前端页面。
服务熔断
在分布式与微服务系统中,**如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会暂时断开与下游服务的调用连接。**这种方式就是熔断。类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
服务熔断一般情况下会有三种状态:闭合、开启和半熔断;
- 闭合状态(保险丝闭合通电OK):服务一切正常,没有故障时,上游服务调用下游服务时,不会有任何限制。
- 开启状态(保险丝断开通电Error):上游服务不再调用下游服务的接口,会直接返回上游服务中预定的方法。
- 半熔断状态:处于开启状态时,上游服务会根据一定的规则,尝试恢复对下游服务的调用。此时,上游服务会以有限的流量来调用下游服务,同时,会监控调用的成功率。如果成功率达到预期,则进入关闭状态。如果未达到预期,会重新进入开启状态。
服务限流
服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如令牌桶算法和漏牌桶算法就是时间窗口的限流算法。
服务隔离
有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。
互联网行业常用的服务隔离方式有:线程池隔离和信号量隔离。
服务超时
整个系统采用分布式和微服务架构后,系统被拆分成一个个小服务,就会存在服务与服务之间互相调用的现象,从而形成一个个调用链。
形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。服务超时就是在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源。
Sentinel入门案例
创建一个新的模块,模块名为:cloudalibaba-sentinel-service8401
,这个模块的作用是将哨兵纳入管控的8401微服务提供者
添加依赖:
<dependencies> <!--SpringCloud alibaba sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!--nacos-discovery--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 引入自己定义的api通用包 --> <dependency> <groupId>com.atguigu.cloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!--SpringBoot通用依赖模块--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> <scope>provided</scope> </dependency> <!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
创建yml配置文件:
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 sentinel: transport: dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址 port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
修改主启动类
创建控制器编写业务逻辑:
@RestController public class FlowLimitController { @GetMapping("/testA") public String testA() { return "------testA"; } @GetMapping("/testB") public String testB() { return "------testB"; } }
启动8401服务,启动Nacos和Sentinel观察该服务是否已经入住Nacos,并访问 localhost:8401/testA与 localhost:8401/testB 观察Sentinel流量监控:
Sentinel之流控模式
在Sentinel
控制台中能制定流控规则,如下图:
对下表字段的一些解释:
- 资源名:资源的唯一名称,默认就是请求的接口路径,可以自行修改,但是要保证唯一。
- 针对来源:具体针对某个微服务进行限流,默认值为default,表示不区分来源,全部限流。
- 阈值类型:QPS表示通过QPS进行限流,并发线程数表示通过并发线程数限流。
- 单机阈值:与阈值类型组合使用。如果阈值类型选择的是QPS,表示当调用接口的QPS达到阈值时,进行限流操作。如果阈值类型选择的是并发线程数,则表示当调用接口的并发线程数达到阈值时,进行限流操作。
- 是否集群:选中则表示集群环境,不选中则表示非集群环境。
Sentinel能够对流量进行控制,主要是监控应用的QPS流量或者并发线程数等指标,如果达到指定的阈值时,就会被流量进行控制,以避免服务被瞬时的高并发流量击垮,保证服务的高可靠性。
流控模式分为三种:
- 直接
- 关联
- 链路
流控模式之直接
直接模式即为默认的控流模式,当接口达到限流条件时,直接开启限流功能。
示例:【对 localhost:8401/testA该路径的接口进行流控】
测试结果:当我们快速频繁刷新该地址时,就会出现默认的错误提示(Blocked by Sentinel (flow limiting)
)来进行流量的限制。
同时也支持自定义的兜底方法 fallback
。
流控模式之关联(实用)
当关联的资源达到阈值时,就限流自己,假设A接口与B接口关联,当B接口访问达到阈值时,A就会被限流(B惹事了,A挂了)
演示:
测试说明:使用jmeter
对testB接口进行压力测试,会发现A进行了限流的操作。
流控模式之链路
来自不同链路的请求对同一个目标访问时,实施针对性的不同限流措施,比如C请求来访问就限流,D请求来访问就是ok
演示:
修改微服务cloudalibaba-sentinel-service8401
,对yml文件进行修改,创建一个FlowLimitService
,并且在控制层新创建两个接口
sentinel: web-context-unify: false # controller层的方法对service层调用不认为是同一个根链路
@Service public class FlowLimitService { @SentinelResource(value = "common") public void common() { System.out.println("------FlowLimitService come in"); } }
控制层新增的方法:
/**流控-链路演示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不管】
测试说明:当快速频繁的访问testC
接口的时候后端就会报错,但是testD
则不会受到影响。
Sentinel之流控效果
sentinel的流控效果分为三种:
- 快速失败
- warm up
- 排队等待
Sentinel默认的流控效果为快速失败,这种方式是直接失败抛出异常
预热WarmUp
什么是限流 冷启动
当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。
公式:阈值除以冷却因子coldFactory
(默认值为3),经过预热时长后才会达到阈值
案例演示:【在Sentinel控制台进行如下配置】
案例说明:单机阈值为10,预热时长设置5秒。
系统初始化的阈值为10 / 3 约等于3,即单机阈值刚开始为3(我们人工设定单机阈值是10,sentinel计算后QPS判定为3开始);然后过了5秒后阀值才慢慢升高恢复到设置的单机阈值10,也就是说5秒钟内QPS为3,过了保护期5秒后QPS为10
测试结果:刚开始进行频繁快速访问会进行限流,5s后就不会再限流(原因就是设置了5秒的预热时长)
该配置的应用场景:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
排队等待
这种场景就非常常见,例如一个时间点同时并发大量的请求,使用这种方法能够让这种并发无序的请求进行排队以免造成服务崩溃。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象以下这样的场景,在某一秒有大量请求到来,而接下来的几秒处于空闲状态,我们希望系统能够在接下来的空闲期间处理这些请求,而不是在第一秒直接拒绝多余的请求。
演示实战:
在FlowLimitController
中添加新方法,该方法将时间输出到控制台中
@GetMapping("/testE") public String testE() { System.out.println(System.currentTimeMillis()+" testE,排队等待"); return "------testE"; }
在Sentinel控制台中进行如下配置:【解释下图中的配置:按照单机阈值,一秒钟通过一个请求,10秒后的请求作为超时处理,放弃】
测试:
使用 jmeter 进行压力测试,1s中发送20个请求并观察后端控制台打印如下图,【说明:因为设置了一秒钟通过一个请求10秒后的请求作为超时处理,所以后台只能接收10个请求,并且是一秒接收一个,剩余的丢弃】
Sentinel之熔断降级
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,
让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
熔断降级的策略分为三种:
- 慢调用比例:选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(
statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 - 异常比列:当==单位统计时长(
statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。==经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0, 1.0]
,代表 0% - 100%。 - 异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
熔断策略之慢调用比例
首先明确在什么样的情况下服务会进入熔断:在统计时长内,实际请求数目>设定的最小请求数 且 实际**慢调用比例(下面有解释)**>比例阈值 ,进入熔断状态。
熔断流程:
- 熔断状态(保险丝跳闸断电,不可访问):在接下来的熔断时长内请求会自动被熔断
- 探测恢复状态(探路先锋):熔断时长结束后进入探测恢复状态
- 结束熔断(保险丝闭合恢复,可以访问):在探测恢复状态,如果接下来的一个请求响应时间小于设置的慢调用 RT,则结束熔断,否则继续熔断。
在Sentinel控制台中设置熔断策略会看到以下选项设置:
先对上面的选项进行说明:
- 调用:一个请求发送到服务器,服务器给与响应,一个响应就是一个调用。
- 最大RT:即最大的响应时间,指系统对请求作出响应的业务处理时间。
- 慢调用:处理业务逻辑的实际时间>设置的最大RT时间,这个调用叫做慢调用。
- 慢调用比例:在所以调用中,慢调用占有实际的比例=慢调用次数➗总调用次数
- 比例阈值:自己设定的 , 比例阈值=慢调用次数➗调用次数
- 统计时长:时间的判断依据
- 最小请求数:设置的调用最小请求数,上图比如1秒钟打进来10个线程(大于我们配置的5个了)调用被触发
实际演示:
在8401模块添加新的接口:
@GetMapping("/testF") public String testF() { //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("----测试:新增熔断规则-慢调用比例 "); return "------testF 新增熔断规则-慢调用比例"; }
在Sentinel控制台中设置上图配置的熔断规则并使用jmeter
进行压力测试,循环发送每1s种发10次请求。测试得出,访问http://localhost:8401/testF
,会立即报错,只要一值由1s10个的请求发送就会一致无法访问,知道关闭请求发送后,并过5s后(上图设置的熔断时长)才会慢慢恢复访问。
异常比例
与满调用比例相似,当访问超出了比例阈值时就会触发熔断
实战演示:
更改8401模块,新增一个方法:
@GetMapping("/testG") public String testG() { System.out.println("----测试:新增熔断规则-异常比例 "); int age = 10/0; return "------testG,新增熔断规则-异常比例 "; }
在Sentinel控制台中进行以下配置:
使用jmeter继续压力测试,1s钟发送20个请求,最终测试的结论为:
按照上述配置,单独访问一次,必然来一次报错一次(int age = 10/0)达到100%,调一次错一次报错error;
测试结论:
开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件了。断路器开启(保险丝跳闸),微服务不可用了,不再报错error而是服务熔断+服务降级,出提示:Blocked by Sentinel (flow limiting)。
异常数
与异常比例相似,顾名思义,异常比例是根据比例阈值来判断是否进行熔断,而异常数则是通过异常数来判断是否进行熔断
实战演示:
更改8401模块,新增一个方法:
/** * 新增熔断规则-异常数 * @return */ @GetMapping("/testH") public String testH() { System.out.println("----测试:新增熔断规则-异常数 "); int age = 10/0; return "------testH,新增熔断规则-异常数 "; }
在Sentinel控制台中进行以下配置:
测试结论:
http://localhost:8401/testH,第一次访问绝对报错,因为除数不能为零,我们看到error窗口;
开启jmeter后,直接高并发干爆他发送请求,多次调用达到我们的配置条件了。但是jmeter开工,上述配置表示,在1秒钟内最少请求2次,当异常数大于1时,会触发熔断操作断路器开启(保险丝跳闸),微服务不可用了,熔断的时长为5秒,不再报错error而是服务降级了出提示Blocked by Sentinel (flow limiting)
@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 {}; }
@SentinelResource
作用一:
不想用默认的限流提示(Blocked by Sentinel (flow limiting)),想返回自定义限流的提示
步骤:只需要在控制层中加上@SentinelResource(value = "控制层方法名",blockHandler = "自定义限流方法名")
即可。
演示:
在业务类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"; }
测试效果:
@SentinelResource
作用二:
SentinelResource配置,点击超过限流配置返回自定义限流提示+程序异常返回fallback服务降级
@SentinelResource
注解中存在一个fallback
参数,该参数是用来异常的自定义服务降级
代码示例如下:
编写控制层业务:
@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(); }
测试说明:
访问地址:http://localhost:8401/rateLimit/doAction/2,并进行连续的快速访问超过所配置的最大请求则返回自己定义的限流处理信息,限流发生,配合了sentinel设定的规则。
访问地址:http://localhost:8401/rateLimit/doAction/0,p1参数为0,异常发生,返回自己定义的服务降级处理
总结:
- blockHandler,主要针对sentinel配置后出现的违规情况处理
- fallback,程序异常了JVM抛出的异常服务降级
- 两者可以同时共存
Sentinel之热点规则
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
案例演示:
添加新的业务方法:【使用自定义的限流方法】
@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支持方法。
测试说明:
含有参数P1,当每秒访问的频率超过1次时,回触发Sentinel的限流操作
没有热点参数P1,当每秒访问的频率超过1次时,不回触发Sentinel的限流操作
Sentinel之授权规则
在某些场景下,需要根据调用接口的来源判断是否允许执行本次请求。此时就可以使用Sentinel提供的授权规则来实现,Sentinel的授权规则能够根据请求的来源判断是否允许本次请求通过。
在Sentinel的授权规则中,提供了 白名单与黑名单 两种授权类型。白放行、黑禁止
设置黑白名单分为两步:【详细演示看代码演示】
- 写一个业务类实现
RequestOriginParser
,接口并重写其方法,在该方法中获取到路径的参数名并返回 - 在Sentinel控制台中进行黑名单配置
代码演示:
编写一个控制层接口:
@RestController @Slf4j public class EmpowerController //Empower授权规则,用来处理请求的来源 { @GetMapping(value = "/empower") public String requestSentinel4(){ log.info("测试Sentinel授权规则empower"); return "Sentinel授权规则"; } }
编写一个组件类并实现
RequestOriginParser
,接口并重写其方法,在该方法中获取到路径的参数名并返回@Component public class MyRequestOriginParser implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest httpServletRequest) { return httpServletRequest.getParameter("serverName"); } }
控制台进行配置:
测试说明:
访问 http://localhost:8401/empower?serverName=test 和 http://localhost:8401/empower?serverName=test2 会被限流,原因是配置黑名单,但你看控制层中的路径并没有接收传入serverName,原因是因为在组件MyRequestOriginParser
类中进行了请求参数的配置。
Sentinel规则持久化
问题:一旦我们重启微服务应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。
解决方法:将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效
具体做法:
在所要进行持久化的模块中添加依赖:
<!--SpringCloud ailibaba sentinel-datasource-nacos --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
在所要进行持久化的模块中配置yml文件:
spring: cloud: sentinel: datasource: ds1: #自定义key nacos: server-addr: localhost:8848 dataId: ${spring.application.name} groupId: DEFAULT_GROUP data-type: json rule-type: flow # com.alibaba.cloud.sentinel.datasource.RuleType
解释
rule-type
这段配置:在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:是否集群。
Sentinel整合Gateway网关
参考官网的文档进行修改:https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81#spring-cloud-gateway
代码实战演示:
创建一个新的模块,该模块是用于设置网关的,命名为:cloudalibaba-sentinel-getway9528
添加依赖:
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>
写yml,进行对某个微服务的网关设置
server: port: 9528 spring: application: name: cloudalibaba-sentinel-gateway # sentinel+gataway整合Case cloud: nacos: discovery: server-addr: localhost:8848 gateway: routes: - id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名 uri: http://localhost:9001 #匹配后提供服务的路由地址 predicates: - Path=/pay/** # 断言,路径相匹配的进行路由
配置类,设置配置规则,设置限流规则,参考官网给出的demo进行修改,修改成如下代码:
@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); } }
测试:
原生的url:http://localhost:9001/pay/nacos/4444,能够访问成功。
网关配置后的url:http://localhost:9528/pay/nacos/4444,能够访问成功
当频繁点击访问就会触发所配置的限流:
SpringCloud Alibaba Seata(处理分布式事务)
一些面试题:
- 你简历上写用微服务boot/cloud做过项目,你不可能只有一个数据库吧?请你谈谈多个数据库之间你如何处理分布式事务?
- 阿里巴巴的Seata-AT模式如何做到对业务的无侵入?
- 对于分布式事务问题,你知道的解决方案有哪些?请你谈谈?
- 对于分布式事务问题,你知道的解决方案有哪些?请你谈谈?
分布式事务如何产生
- 在微服务的模式下,正常情况每个模块对应一个数据库,如何保证数据的一致性
- 一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
- 关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,
业务操作需要调用三个服务来完成。
此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
Seata简介
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Seata工作流程简介
想要清楚Seata的工作流程,就要清楚以下三个概念:
- TC(Transaction Coordinator)事务协调器【可理解为班主任】:就是Seata,负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚
- TM(Transaction Manager)事务管理器【可理解为班长】:标注全局
@GlobalTransactional
启动入口动作的微服务模块(比如订单模块),他是事务的发起者,负责定义全局事务的范围,并根据TC维护全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议 - RM(Rescorce Manager)资源管理器【可理解为学生】:就是Mysql数据库本身,可以是多个RM,负责管理分支事务上的资源,向TC注册分支事务,汇报分支事务状态,驱动分支事务的调教或回滚
工作流程图:
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
- XID在微服务调用链路的上下文中传播
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
- TM向TC发起针对XID的全局提交或回滚决议
- TC调度XID下管辖的全部分支事务完成提交或回滚请求
代码实战:【详细看源码或者脑图】
创建订单数据库,库存数据库,账号数据库,【注意每个数据库中必须创建一张 undo_log 表】创建订单微服务模块,创建库存微服务模块,创建账号微服务模块,整体的流程是用户访问接口,创建订单,然后调用库存微服务减少对应的库存,调用账号微服务减少对应的余额。但在创建订单的时候出现了异常(比如 10/0 异常,接口访问超过了openFeign接口的超时时间),若不作事务的处理,就会在这三个模块中出现脏数据,所以必须进行全局的事务处理。
具体做法:在你所访问接口的业务实现类中添加上@GlobalTransactional
注解进行全局事务的回滚。
@Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderMapper orderMapper; @Resource private AccountFeignApi accountFeignApi; @Resource private StorageFeignApi storageFeignApi; @GlobalTransactional(name = "hines-create-order",rollbackFor = Exception.class) @Override 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); } }
@GlobalTransactional注解原理
官网地址:https://seata.apache.org/zh-cn/docs/user/mode/at
开启该注解后,无论是在插入过程中出现异常,都会将数据插入到数据库中,并且插入一行记录在undo_log
表中,若中途出现了异常就会触发事务的回滚,将当前插入的数据全部清空,undo_log
表插入的数据也会删除
机制:【分为两个阶段】
- 第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
- 第二阶段:
- 提交异步化,非常快速的完成
- 回滚通过一阶段的回滚日志进行反向补偿【undo_log的作用就是进行反向补偿】
细说两大阶段:
在第一阶段,Seata会拦截 ”业务SQL“
- 解析SQL语义,找到 "业务SQL"更新业务数据,在业务数据被更新前,将其保存成 “before image”
- 执行 “业务SQL” 更新业务数据,在业务数据更新之后
- 其保存成 “after image”,最后生成行锁
以上 操作全部在一个数据库事务内完成,这样保证了一阶段的原子性。
二阶段分为两种情况:
- 正常提交
- 出现异常,触发事务回滚
如果是顺利正常提交的话:
因为 “业务SQL” 在一阶段已经提交到数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可
如果是异常回滚的话:
Seata就需要回滚一阶段已经执行的 ”业务SQL“,还原业务数据
回滚方式便是 “before image” 还原业务数据;但是还原业务前先要校验脏写,对比 “数据库当前业务数据” 和 “after image”
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就要进行人工处理。