一、引言
1.1 什么是SpringCloud?(Dubbo, Dubbox,SpringCloud ***flix(Eureka/Hystrix/SpringCloud Config/zuul/Feign),SpringCloud Alibaba)
SpringCloud是基于SpringBoot的一整套实现微服务的框架。他提供了微服务开发所需的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等组件。最重要的是,跟spring boot框架一起使用的话,会让你开发微服务架构的云服务非常好的方便。
1.2 什么是微服务?
微服务本质上就是一个独立的SpringBoot工程,但是整个项目又是由多个SpringBoot工程组成,而且这些服务之间又存在一定的联系,需要进行一些统一的管理。
单体架构中,所有的代码集中在同一个项目中。虽然便于管理,但是当项目足够庞大时,所有的业务模块都集中在一个JVM进程中,会面临很多问题:
1、项目过于臃肿,难以维护
2、资源无法隔离,某个资源出现问题,整个系统崩溃
3、拓展性差,通常只能水平拓展,缺乏灵活性
什么是微服务?
简单来说,微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常用HTTP资源API)。这些服务围绕业务能力构建并且可通过全自动部署机制独立部署。这些服务共用一个最小型的集中式的管理,服务可用不同的语言开发,使用不同的数据存储技术。
微服务的特点:
1、根据业务模块划分服务
2、每个服务可以独立部署并且互相隔离
3、通过轻量的 API 调用服务
4、服务需要保证良好的高可用性
三高(高并发、高性能、高可用)
1.3 SpringCloud Alibaba简介
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
SpringCloudAlibaba - 官网
Spring Cloud Alibaba
SpringCloud Alibaba - GitHub地址
GitHub - alibaba/spring-cloud-alibaba: Spring Cloud Alibaba provides a one-stop solution for application development for the distributed solutions of Alibaba middleware.
https://github.***/alibaba/spring-cloud-alibaba/blob/master/README-zh.md - 中文文档
SpringBoot、SpringCloud ***flix、SpringCloud Alibaba兼容版本
Pom父工程统一管理SpringBoot/SpringCloud/SpringCloud Alibaba的依赖
<spring-boot.version>2.3.0.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<spring-cloud-ailbaba.version>2.2.5.RELEASE</spring-cloud-ailbaba.version>
<!-- 依赖管理 -->
<dependencyManagement>
<dependencies>
<!-- 管理SpringBoot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<!-- 管理SpringCloud ***flix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<!-- 管理SpringCloud alibaba -->
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-ailbaba.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
二、Nacos - 服务注册中心/统一配置中心 (Zookeeper、Eureka、Nacos、Consul+Apollo)
2.1 什么是nacos?
Nacos官网 - https://nacos.io/zh-***/index.html
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
Nacos与多注册中心对比
Nacos - AP和CP之间的切换命令
curl -X PUT '192.168.195.135:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
什么是注册中心?为什么需要注册中心?
核心作用:
1)服务和服务间调用的解耦,每个服务只需要和注册中心发生耦合,而服务之间没有直接联系,降低维护成本
2)轻松实现服务之间的负载均衡调用
2.2 nacos安装 - 原生方式
2.2.1 下载Nacos服务端
Release 2.0.1 (Apr 29th, 2021) · alibaba/nacos · GitHub
注意:可以根据自己的需要选择相应的版本
2.2.2 上传到Linux服务器并且解压
tar -zxf nacos-server-2.0.1.tar.gz
2.2.3 进入并且运行Nacos服务
cd nacos/bin
./startup.sh -m standalone
注意:-m standalone表示单机模式
2.2.4 访问Nacos服务
http://ip:8848/nacos
2.2.5 登录Nacos服务端
账号/密码默认都是:nacos
2.3 nacos安装 - docker方式
2.3.1 准备docker-***pose.yml文件
java">version: "3.1"
services:
mysql:
image: mysql:5.7
container_name: mysql
restart: always
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
- ./mysql/conf:/etc/mysql/conf.d
- ./mysql/data:/var/lib/mysql
nacos:
image: nacos/nacos-server
container_name: nacos
restart: always
environment:
SPRING_DATASOURCE_PLATFORM: mysql #数据源平台 仅支持mysql或不保存empty
MODE: standalone
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_DB_NAME: nacos
MYSQL_SERVICE_PORT: 3306
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: root
NACOS_APPLICATION_PORT: 8848
JVM_XMS: 256m
JVM_XMX: 512m
volumes:
- ./nacos/logs/:/home/nacos/logs
- ./nacos/plugins/:/home/nacos/plugins
ports:
- 8848:8848
2.3.2 启动docker容器
docker-***pose up -d
2.4 nacos的服务注册步骤
2.4.1 创建springboot pom工程,引入springcloud alibaba依赖管理
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.5.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 引入SpringCloud Alibaba的相关依赖管理 -->
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.4.2 微服务添加nacos注册相关依赖
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2.4.3 微服务配置application.yml
spring:
application:
name: server-test1
cloud:
nacos:
server-addr: ip:8848
server:
port: 8081
注意:如果不想使用Nacos进行服务注册和发现,则可以设置spring.cloud.nacos.discovery为false。
2.4.4 启动类添加注解
@EnableDiscoveryClient
2.4.5 查看nacos后台服务注册情况
ps nacos服务注册的相关配置
2.5 nacos的配置中心步骤
2.5.1 微服务添加依赖
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2.5.2 创建bootstrap.yml配置文件
spring:
cloud:
nacos:
config:
#配置中心的地址
server-addr: ip:8848
#配置中心文件的格式
file-extension: yaml
profiles:
#当前的运行环境
active: dev
application:
#微服务的名称
name: server-test2
2.5.3 nacos服务新增配置
2.5.4 在相应的类上添加自动刷新配置的注解
@RefreshScope
PS、Nacos新增配置时 dataId的完整格式
prefix-{spring.profiles.active}.${file-extension}
- prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置。
- spring.profiles.active 即为当前环境对应的 profile,详情可以参考 Spring Boot文档。
- file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。
注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 prefix.{file-extension}
PS、Nacos多配置文件的读取
spring:
cloud:
nacos:
config:
#配置中心的地址
server-addr: 192.168.195.135:8848
#配置中心文件的格式
file-extension: yaml
#多配置文件读取
shared-configs:
- dataId: new-config-${spring.profiles.active}.yaml
group: NEW_GROUP
refresh: true
profiles:
#当前的运行环境
active: dev
application:
name: server-test2
2.6 nacos集群与持久化
2.6.1 nacos集群的模式
1)模式一:直连模式
http://ip1:port/openAPI 直连ip模式,机器挂则需要修改ip才可以使用。
2)模式二:直连负载均衡模式
http://SLB:port/openAPI 挂载SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),
直连SLB即可,下面挂server真实ip,可读性不好。
3)模式三:域名 + 负载均衡模式(官方推荐)
http://nacos.***:port/openAPI 域名 + SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),
可读性好,而且换ip方便,推荐模式
2.6.2 nacos集群的搭建
2.6.2.1 准备3个nacos服务(伪集群)
2.6.2.2 分别修改端口:nacos/conf/application.properties
2.6.2.3 修改每个Nacos服务目录nacos/conf下的文件cluster.conf文件
#it is ip
#example
192.168.195.135:8848
192.168.195.135:8849
192.168.195.135:8850
2.6.2.4 配置外部数据源(持久化Nacos配置数据)
a) 导入数据源到外部mysql
#安装mysql数据库
#创建mysql数据库create database nacos_config;
#导入nacos/conf/nacos-mysql.sql文件
b) 配置nacos连接mysql(nacos/conf/**application.properties配置文件**)<br />![](https://cdn.nlark.***/yuque/0/2022/png/26763702/1647525435427-***741602-7486-43c1-bae7-c6acd7***0e21.png#crop=0&crop=0&crop=1&crop=1&id=mfTwk&originHeight=254&originWidth=1009&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />2.6.2.5 依次启动nacos服务
#内置数据源启动方式
startup.sh -p embedded
#使用外置数据源启动方式
nacos/bin/startup.sh
2.6.2.6 查看集群状态
2.6.2.7 代码端配置连接到nacos集群 - 直连模式
spring:
application:
name: server-test2
cloud:
nacos:
server-addr: 192.168.195.135:8848,192.168.195.135:8849,192.168.195.135:8850
2.6.3 nacos集群(Docker版)
version: "3.1"
services:
mysql:
image: mysql:5.7
container_name: mysql
restart: always
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
- ./mysql/conf:/etc/mysql/conf.d
- ./mysql/data:/var/lib/mysql
nacos1:
***work_mode: host #配置网络模式为host,表示容器直接使用宿主机网络
image: nacos/nacos-server
container_name: nacos1
restart: always
environment:
MODE: cluster #配置集群模式
NACOS_SERVERS: "192.168.195.188:8848 192.168.195.188:8850 192.168.195.188:8852" #配置集群地址
NACOS_APPLICATION_PORT: 8848 #配置nacos服务的启动端口
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: 192.168.195.188
MYSQL_SERVICE_PORT: 3306
MYSQL_SERVICE_DB_NAME: nacos_config
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: root
JVM_XMS: 256m
JVM_XMX: 512m
volumes:
- ./nacos1/logs/:/home/nacos/logs
- ./nacos1/plugins/:/home/nacos/plugins
nacos2:
***work_mode: host
image: nacos/nacos-server
container_name: nacos2
restart: always
environment:
MODE: cluster
NACOS_SERVERS: "192.168.195.188:8848 192.168.195.188:8850 192.168.195.188:8852"
NACOS_APPLICATION_PORT: 8850
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: 192.168.195.188
MYSQL_SERVICE_PORT: 3306
MYSQL_SERVICE_DB_NAME: nacos_config
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: root
JVM_XMS: 256m
JVM_XMX: 512m
volumes:
- ./nacos2/logs/:/home/nacos/logs
- ./nacos2/plugins/:/home/nacos/plugins
nacos3:
***work_mode: host
image: nacos/nacos-server
container_name: nacos3
restart: always
environment:
MODE: cluster
NACOS_SERVERS: "192.168.195.188:8848 192.168.195.188:8850 192.168.195.188:8852"
NACOS_APPLICATION_PORT: 8852
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: 192.168.195.188
MYSQL_SERVICE_PORT: 3306
MYSQL_SERVICE_DB_NAME: nacos_config
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: root
JVM_XMS: 256m
JVM_XMX: 512m
volumes:
- ./nacos3/logs/:/home/nacos/logs
- ./nacos3/plugins/:/home/nacos/plugins
注册中心,配置中心功能实现图解
三、微服务的调用 - Ribbon + RestTemplate
3.1 什么是Ribbon?
Ribbon本质上是一个客户端负载均衡器,在SpringCloud中主要的作用包含两个:
1、去Nacos上发现服务
2、负载均衡调用指定微服务
什么是服务端负载均衡?
什么是客户端负载均衡?
注意:客户端负载均衡往往会配合注册中心一起使用
3.2 Ribbon的服务调用
1)创建一个班级服务,注册到注册中心之上,实现学生服务对班级服务的调用
2)调用方添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-***flix-ribbon</artifactId>
</dependency>
3)注册RestTemplate对象
/**
* 注册RestTemplate对象
* @return
*/
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
4)在调用方使用RestTemplate发送请求
@Autowired
private RestTemplate restTemplate;
...
String clsName = restTemplate
.getForObject("http://micro-cls/cls/queryName/" + sid, String.class);
注意:
1、RestTemplate需要添加@LoadBalanced 才会配合Ribbon进行负载均衡和服务发现
2、RestTemplate请求的地址必须填写微服务的名称
3.3 Ribbon负载均衡的策略
Ribbon实现负载均衡策略的核心接口 - IRule
设置负载均衡策略
/**
* 设置负载均衡策略
* @return
*/
@Bean
public IRule getRule(){
return new RandomRule();
}
四、微服务的调用 - Feign
Feign其实本质上就是Ribbon + Hystrix,提供了更加面向对象的服务调用方式
4.1 Feign的服务调用
1)创建教师微服务,注册到注册中心上
2)调用方(班级服务)添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3)编写Feign的调用接口
@FeignClient(name = "micro-tea")
public interface TeaFeign {
@RequestMapping("/tea/queryName/{tid}")
String queryTeaName(@PathVariable Integer tid);
}
4)启动类添加注解,配置Feign接口的扫描
@EnableFeignClients(basePackages = "***.qf.feign")
5)使用Feign调用微服务
4.2 Feign的超时时间和重试
Feign本身拥有超时和重试的设定,但是默认是关闭的。Feign默认采用的是底层Ribbon的重试和超时的设定,一旦Feign的超时和重试开启,那么就会覆盖Ribbon的设置。
Ribbon的读超时默认是1s,连接超时默认也是1s,跨服务重试的次数默认1次,同服务重试的次数默认1次
ribbon的超时和重试的设置
ribbon:
ConnectTimeout: 1000 #毫秒 连接超时时间
ReadTimeout: 1000 #毫秒 读超时时间
MaxAutoRetries: 0 # 对当前实例的最大重试次数
MaxAutoRetriesNextServer: 1 # 切换实例的最大重试次数(如果还失败就切换下
feign的超时和重试的设置
#配置Feign的超时-会覆盖ribbon设置
feign:
client:
config:
default:
connectTimeout: 1000
readTimeout: 1000
//配置Feign的重试
@Bean
public Retryer getRetryer(){
//参数1:重试的间隔时间 默认100毫秒
//参数2:重试的最大间隔时间 默认1秒 重试间隔时间按照一定的规则逐渐增大,但不能超过最大间隔时间
//参数3:最大重试次数 默认5次 (包含第一次请求)
return new Retryer.Default(100, 500, 2);
}
注意:
1、实际开发过程中,可以根据业务适当的调整Read超时的大小
2、实际开发过程中,根据需要可以开启或者关闭重试,但是需要注意,如果开启了重试,就有重复请求的风险,尽可能的需要保证服务的被调用方的接口幂等
如何构造一个幂等的接口?
1、给数据库的某些字段设置唯一性
2、结合缓存服务器Redis实现接口幂等
五、路由网关 - Gateway
官网地址
Spring Cloud Gateway
为什么需要路由网关?
路由网关会作为微服务架构体系的入口组件,所有的外部请求,都必须经过路由网关分发(内部互相访问没有影响),这样可以屏蔽微服务体系结构复杂性,并且可以提供请求路由、负载访问、请求过滤整形等功能。
5.1 搭建Gateway
1)创建一个SpringBoot工程(依赖Eureka-Client、Gateway,千万不要依赖web)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2)启动类注解
@EnableDiscoveryClient
3)配置application.yml
server:
port: 80
spring:
application:
name: gateway
cloud:
nacos:
server-addr: ken:8848
4)配置getaway的路由功能(静态路由)
spring:
application:
name: gateway
cloud:
gateway:
#配置静态路由
routes:
#----路由规则1---------
- id: guize1 #当前的路由规则标识,不重复的合法标识即可
predicates: #配置路由断言,路由的条件
- Path=/stu/**
uri: lb://micro-stu
#----路由规则2---------
- id: guize2 #当前的路由规则标识,不重复的合法标识即可
predicates: #配置路由断言,路由的条件
- Path=/cls/**
- Query=token
uri: lb://micro-cls
5.2 动态路由
5.3 过滤器链
5.3.1 局部过滤器
package ***.qf.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.***ponent;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 局部过滤器
*/
@***ponent
public class MyGatewayFilter extends AbstractGatewayFilterFactory implements Ordered {
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("局部过滤器触发!!!" + exchange.getRequest().getURI().toString());
return chain.filter(exchange);
}
};
}
public String name() {
return "HelloGateway";
}
public int getOrder() {
return 0;
}
}
局部过滤器的使用
server:
port: 80
spring:
application:
name: gateway
cloud:
nacos:
server-addr: ken:8848
gateway:
#配置gateway的路由规则 - 请求转发给谁?
#uri - 当前路由转发的微服务名称
#predicates - 断言,怎么样的请求会匹配到当前的路由
#filters - 指定局部过滤器
routes:
- uri: lb://classes
predicates:
- Path=/cls/**
- Query=token
- uri: lb://student
predicates:
- Path=/stu/**
filters:
- name: HelloGateway
5.3.2 全局过滤器
对所有请求都进行过滤处理
全局过滤器的配置
@***ponent
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("全局过滤器生效!!!");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 50;
}
}
5.3.3 基于过滤器的请求限流
限流的实现方案
请求计数限流
漏桶算法限流
令牌桶算法限流
令牌桶算法的Lua脚本
--令牌桶的key
local key = 'tokentong_'..KEYS[1]
--需要多少令牌
local getTokens = tonumber(ARGV[1])
--获得令牌桶中的参数
--令牌桶中拥有的令牌
local hasToken = tonumber(redis.call('hget', key, 'hasToken'))
--令牌桶的最大令牌数
local maxToken = tonumber(redis.call('hget', key, 'maxToken'))
--每秒产生的令牌数
local tokensSec = tonumber(redis.call('hget', key, 'tokensSec'))
--下一次可以计算生成令牌的时间(微秒)
local nextTimes = tonumber(redis.call('hget', key, 'nextTimes'))
--进行一些参数计算
--计算多久产生一个令牌(微秒)
local o***okenTimes = 1000000/tokensSec
--获取当前时间
local now = redis.call('time')
--计算当前微秒的时间戳
local nowTimes = tonumber(now[1]) * 1000000 + tonumber(now[2])
--生成令牌
if nowTimes > nextTimes then
--计算生成的令牌数
local createTokens = (nowTimes - nextTimes) / o***okenTimes
--计算拥有的令牌数
hasToken = math.min(createTokens + hasToken, maxToken)
--更新下一次可以计算令牌的时间
nextTimes = nowTimes
end
--获取令牌
--当前能够拿到的令牌数量
local canTokens = math.min(getTokens, hasToken)
--需要预支的令牌数量
local reserveTokens = getTokens - canTokens
--根据预支的令牌数,计算需要预支多少时间(微秒)
local reserveTimes = reserveTokens * o***okenTimes
--更新下一次可以计算令牌的时间
nextTimes = nextTimes + reserveTimes
--更新当前剩余的令牌
hasToken = hasToken - canTokens
--更新redis
redis.call('hmset', key, 'hasToken', hasToken, 'nextTimes', nextTimes)
--返回本次获取令牌需要等待的时间
return math.max(nextTimes - nowTimes, 0)
有超时时间的版本
--令牌桶的key
local key = 'tokentong_'..KEYS[1]
--需要多少令牌
local getTokens = tonumber(ARGV[1])
--允许等待的最大时间(超时时间) 微秒
local timeouts = tonumber(ARGV[2] or -1)
--获得令牌桶中的参数
--令牌桶中拥有的令牌
local hasToken = tonumber(redis.call('hget', key, 'hasToken'))
--令牌桶的最大令牌数
local maxToken = tonumber(redis.call('hget', key, 'maxToken'))
--每秒产生的令牌数
local tokensSec = tonumber(redis.call('hget', key, 'tokensSec'))
--下一次可以计算生成令牌的时间(微秒)
local nextTimes = tonumber(redis.call('hget', key, 'nextTimes'))
--进行一些参数计算
--计算多久产生一个令牌(微秒)
local o***okenTimes = 1000000/tokensSec
--获取当前时间
local now = redis.call('time')
--计算当前微秒的时间戳
local nowTimes = tonumber(now[1]) * 1000000 + tonumber(now[2])
--判断超时时间范围内时候能够成功预支到令牌,如果不行,直接返回-1
--如果下一次计算令牌的时间,比当前时间要大,就需要等待,就有可能超过过期时间
if timeouts ~= -1 then
--在过期时间内,无法获得令牌
if nextTimes - nowTimes > timeouts then
return -1
end
end
--生成令牌
if nowTimes > nextTimes then
--计算生成的令牌数
local createTokens = (nowTimes - nextTimes) / o***okenTimes
--计算拥有的令牌数
hasToken = math.min(createTokens + hasToken, maxToken)
--更新下一次可以计算令牌的时间
nextTimes = nowTimes
end
--获取令牌
--当前能够拿到的令牌数量
local canTokens = math.min(getTokens, hasToken)
--需要预支的令牌数量
local reserveTokens = getTokens - canTokens
--根据预支的令牌数,计算需要预支多少时间(微秒)
local reserveTimes = reserveTokens * o***okenTimes
--更新下一次可以计算令牌的时间
nextTimes = nextTimes + reserveTimes
--更新当前剩余的令牌
hasToken = hasToken - canTokens
--更新redis
redis.call('hmset', key, 'hasToken', hasToken, 'nextTimes', nextTimes)
--返回本次获取令牌需要等待的时间
return math.max(nextTimes - nowTimes, 0)
六、链路追踪 - sleuth
什么是链路追踪?
微服务会有很多的服务互相调用,我们的一个业务到底用了多少微服务,每个微服务耗时多少,中间出现了异常,到底是谁出现了异常,所以我需要对请求的链路进行追踪
6.1 使用Docker搭建链路追踪的监控平台 - zipkin
1)拉取zipkin的镜像
docker pull openzipkin/zipkin
2)配置zipkin的docker-***pose模板
version: "3.1"
services:
zipkin:
image: openzipkin/zipkin
restart: always
volumes:
- ./zipkin/localtime:/etc/localtime
ports:
- 9411:9411
container_name: zipkin
3)执行命令,创建容器
docker-***pose up -d
4)开放端口,访问zipkin图形化页面
6.2 给所有微服务配置链路追踪
1
1)微服务添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
2)配置application.yml
spring:
zipkin:
#配置zipkin服务的地址
base-url: http://192.168.195.188:9411
sleuth:
sampler:
#链路采样率,默认0.1
probability: 1
6.3 整合MySQL持久化zipkin链路数据
1)创建mysql容器
mysql:
image: mysql:5.7
ports:
- 3306:3306
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
- ./mysql/conf:/etc/mysql/conf.d
- ./mysql/logs:/logs
- ./mysql/data:/var/lib/mysql
restart: always
2)连接mysql创建zipkin的表结构
create database `zipkin` /*!40100 DEFAULT CHARACTER SET utf8 */;
use zipkin;
create table if not exists zipkin_spans (
`trace_id_high` bigint not null default 0 ***ment 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` bigint not null,
`id` bigint not null,
`name` varchar(255) not null,
`remote_service_name` varchar(255),
`parent_id` bigint,
`debug` bit(1),
`start_ts` bigint ***ment 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` bigint ***ment 'Span.duration(): micros used for minDuration and maxDuration query',
primary key (`trace_id_high`, `trace_id`, `id`)
) engine=innodb row_format=***pressed character set=utf8 collate utf8_general_ci;
alter table zipkin_spans add index(`trace_id_high`, `trace_id`) ***ment 'for getTracesByIds';
alter table zipkin_spans add index(`name`) ***ment 'for getTraces and getSpanNames';
alter table zipkin_spans add index(`remote_service_name`) ***ment 'for getTraces and getRemoteServiceNames';
alter table zipkin_spans add index(`start_ts`) ***ment 'for getTraces ordering and range';
create table if not exists zipkin_annotations (
`trace_id_high` bigint not null default 0 ***ment 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` bigint not null ***ment 'coincides with zipkin_spans.trace_id',
`span_id` bigint not null ***ment 'coincides with zipkin_spans.id',
`a_key` varchar(255) not null ***ment 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` blob ***ment 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` int not null ***ment 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` bigint ***ment 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` int ***ment 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` binary(16) ***ment 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` smallint ***ment 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` varchar(255) ***ment 'Null when Binary/Annotation.endpoint is null'
) engine=innodb row_format=***pressed character set=utf8 collate utf8_general_ci;
alter table zipkin_annotations add unique key(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) ***ment 'Ignore insert on duplicate';
alter table zipkin_annotations add index(`trace_id_high`, `trace_id`, `span_id`) ***ment 'for joining with zipkin_spans';
alter table zipkin_annotations add index(`trace_id_high`, `trace_id`) ***ment 'for getTraces/ByIds';
alter table zipkin_annotations add index(`endpoint_service_name`) ***ment 'for getTraces and getServiceNames';
alter table zipkin_annotations add index(`a_type`) ***ment 'for getTraces and auto***plete values';
alter table zipkin_annotations add index(`a_key`) ***ment 'for getTraces and auto***plete values';
alter table zipkin_annotations add index(`trace_id`, `span_id`, `a_key`) ***ment 'for dependencies job';
create table if not exists zipkin_dependencies (
`day` date not null,
`parent` varchar(255) not null,
`child` varchar(255) not null,
`call_count` bigint,
`error_count` bigint,
primary key (`day`, `parent`, `child`)
) engine=innodb row_format=***pressed character set=utf8 collate utf8_general_ci;
3)重新创建zipkin的容器,需要连接数据库
zipkin:
image: openzipkin/zipkin
restart: always
volumes:
- ./zipkin/localtime:/etc/localtime
ports:
- 9411:9411
container_name: zipkin
environment:
MYSQL_USER: root
MYSQL_PASS: root
MYSQL_HOST: 192.168.195.148
STORAGE_TYPE: mysql
MYSQL_DB: zipkin
MYSQL_TCP_PORT: 3306
4)重新构建zipkin的容器
docker-***pose up -d --build zipkin
七、Sentinel熔断限流
7.1 Sentinel简介
sentinel - github地址
https://github.***/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
sentinel是什么?
sentinel:分布式系统的流量防卫兵。随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
sentinel的核心部分
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
7.2 Sentinel控制台的安装
1、下载Sentinel控制台程序(jar文件)
https://github.***/alibaba/Sentinel/releases/tag/1.8.1
2、将控制台程序上传的Linux服务器, 并且通过命令启动
java -jar sentinel-dashboard-1.8.1.jar
命令: nohup java -jar sentinel-dashboard-1.8.1.jar > sentinel.log 2>&1 &
该命令可以让jar程序在后台运行
注意:需要服务器预留8080端口
3、访问sentinel控制台程序
http://192.168.195.135:8080
注意:账号密码都为sentinel
7.3 使用docker安装
docker-***pose配置
sentinel:
image: bladex/sentinel-dashboard
container_name: sentinel
restart: always
ports:
- 8858:8858
7.4 微服务使用Sentinel配合仪表盘进行访问监控
1、调用方微服务添加相关依赖
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2、配置微服务访问sentinel仪表盘的地址
spring:
application:
name: server-test2
cloud:
nacos:
server-addr: 192.168.195.135:8848
sentinel:
transport:
#配置sentinel仪表盘的访问地址
dashboard: 192.168.195.135:8080
#在启动当前服务时,sentinel还会启动一个服务接收仪表盘的请求,该服务的默认端口为8719
port: 8719
3、启动微服务,并且访问仪表盘查看监控情况
注意:
1、sentinel默认为懒加载形式,仪表盘的记录需要在访问过微服务接口后,才会显示
2、sentinel-dashboard必须搭建在内网(sentinel控制台程序会主动访问微服务,也就是8719端口,如果是外网,sentinel控制台因为网段的原因会访问不到微服务)
7.5 Sentinel的流量控制
什么是流量控制?
流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
流量控制规则的元素组成
- resource:资源名,即限流规则的作用对象
- count: 限流阈值
- grade: 限流阈值类型(QPS 或并发线程数)
- limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
- strategy: 调用关系限流策略
- controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)
流控类型 - 怎样的条件下会触发流控
- 基于并发线程数控制流量并发数控制用于保护业务线程池不被慢调用耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的开销比较大,特别是对低延时的调用有比较大的影响。Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。
- 基于QPS(每秒请求数)控制流量当 QPS 超过某个阈值的时候,则采取措施进行流量控制
流控效果 - 流控触发后会导致的结果
- 直接拒绝 直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
- Warm Up Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
- 匀速排队匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
注意:匀速排队模式暂时不支持 QPS > 1000 的场景。
注意:流控类型如果为并发线程数,则流控效果只支持直接拒绝。
流控模式 - 流控中服务与服务之间的调用关系
- 直接资源达到限流条件时,直接限流
- 关联流量控制 当关联的资源达到阈值时,就会触发当前资源的限流
- 链路限流 - 新版本默认已经无效,需要做某些相关配置当从某个接口过来的资源达到限流条件时,开启限流;它的功能有点类似于针对 来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度 更细;
流控来源 - 针对调用者进行限流
流控规则中的 limitApp 字段用于根据调用来源进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:
- default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。
- {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者caller1的规则,那么当且仅当来自 caller1 对 NodeA 的请求才会触发流量控制。
- other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值。
注意:同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default
通过代码的方式配置流控
private void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule(resourceName);
// set limit qps to 20
rule.setCount(20);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp("default");
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
7.5.1 Sentinel整合SpringCloud GateWay实现网关流控
1)gateway添加相关依赖
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2)配置yml
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.195.135:8848
gateway:
routes:
- id: server-test1
uri: lb://server-test1
predicates:
- Path=/test1/**
- id: server-test2
uri: lb://server-test2
predicates:
- Path=/test2/**
sentinel:
transport:
#配置sentinel控制台地址
dashboard: 192.168.195.135:8080
3)自定义限流降级后的处理方法
@Bean
public void init(){
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
System.out.println("触发了限流!!!!" + throwable);
return ServerResponse.status(HttpStatus.BAD_REQUEST)
.header("content-type", "text/html;charset=utf-8")
.bodyValue("已经触发了限流!!!!");
}
});
}
4)登录sentinel控制台配置流控规则
7.6 Sentinel熔断降级
什么是熔断降级?
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。
由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
降级策略
平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。
注意:Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。
注意:异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。
使用代码的方式配置熔断降级
@Bean
public void init(){
System.out.println("代码设置熔断降级!");
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
//设置资源名称
rule.setResource("/test1");
//设置最大RT
rule.setCount(500);
//设置熔断类型 -
//RuleConstant.DEGRADE_GRADE_RT - 慢调用比例
//RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO - 异常比例
//RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT - 异常数
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
//设置阈值 0.0 ~ 1.0
rule.setSlowRatioThreshold(0.5);
//熔断时长
rule.setTimeWindow(12);
//最小请求数
rule.setMinRequestAmount(3);
//统计时长 - 滑动窗口时长
rule.setStatIntervalMs(10000);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
7.6.1 Sentinel整合Feign实现调用方的降级
1)添加配置
feign:
sentinel:
enabled: true
2)编写Feign接口的默认降级实现类
package ***.qf.stu.application.feign;
import org.springframework.stereotype.***ponent;
@***ponent
public class ClsServiceFallback implements ClsService {
@Override
public String getCls(Integer cid) {
return "Feign的降级方法!!!";
}
}
3)配置Feign接口的注解
package ***.qf.stu.application.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "cls-server", fallback = ClsServiceFallback.class)
public interface ClsService {
@RequestMapping("/cls/get")
String getCls(@RequestParam("cid") Integer cid);
}
7.7 Sentinel热点参数限流
什么是热点参数限流?
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制,热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
配置热点参数限流
1)定义热点限流资源
2)控制台设置资源限流参数
3)发送请求测试
注意:只有相同参数值达到阈值时才会触发限流,比如阈值为3,当单位时间内p1=100,请求3次就会触发限流,而p1=100/101/102,请求3次不会触发限流
设置参数的额外项 - 如果参数值匹配上指定的项,就会根据额外设定的阈值触发限流
1)修改热点限流设置
2)选择高级选项,设置额外项
7.8 Sentinel注解支持 - @SentinelResource
@SentinelResource的作用
Sentinel 提供了 @SentinelResource 注解用于定义资源,并提供了 AspectJ 的扩展用于自动定义资源、处理 BlockException 等。
@SentinelResource注解的属性介绍
- value:资源名称,必需项(不能为空)
- entryType:entry 类型,可选项(默认为 EntryType.OUT)
- blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- fallback / fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
- fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
- defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
注意:
1、blockHandler主要用来指定Sentinel限流或者熔断后(也就是报BlockException异常)的处理方法
2、fallback主要用来指定方法抛出的所有异常(非BlockException、非exceptionsToIgnore指定的异常)处理方法
3、blockHandler指定的方法最后一个参数必须为BlockException
4、@SentinelResource注解、blockHandler、fallback指定的方法必须为公共方法(public)
7.9 Nacos持久化管理控制参数
1)微服务添加依赖
<dependency>
<groupId>***.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2)微服务添加相应代码(启动微服务时触发)
package ***.qf.classes.application;
import ***.alibaba.csp.sentinel.datasource.ReadableDataSource;
import ***.alibaba.csp.sentinel.datasource.nacos.NacosDataSource;
import ***.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import ***.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import ***.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import ***.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import ***.alibaba.fastjson.JSON;
import ***.alibaba.fastjson.TypeReference;
import org.springframework.boot.***mandLineRunner;
import org.springframework.stereotype.***ponent;
import java.util.List;
@***ponent
public class NacosSentinelInit implements ***mandLineRunner {
@Override
public void run(String... args) throws Exception {
//配置Nacos配置文件地址,获取流控配置
ReadableDataSource<String, List<FlowRule>> flowDataSource =
new NacosDataSource<>("nacos地址:端口",
"配置的group",
"配置的dataId",
json -> JSON.parseObject(json, new TypeReference<List<FlowRule>>() {}));
//将nacos中的流控信息设置到微服务的Sentinel组件中
FlowRuleManager.register2Property(flowDataSource.getProperty());
//配置Nacos配置文件地址,获取降级配置
ReadableDataSource<String, List<DegradeRule>> degradeDataSource =
new NacosDataSource<>("nacos地址:端口",
"配置的group",
"配置的dataId",
json -> JSON.parseObject(json, new TypeReference<List<DegradeRule>>() {}));
//将nacos中的降级信息设置到微服务的Sentinel组件中
DegradeRuleManager.register2Property(degradeDataSource.getProperty());
}
}
3)在nacos中添加对应的配置文件
//流控的json内容
[
{
"resource": "/resource",//资源名称
"count": 1000,//限流阈值
"grade": 1,//限流类型 0-线程数 1-QPS
"limitApp": "default",//流控针对的调用来源,若为 default 则不区分调用来源
"strategy": 0,//调用关系限流策略 0-直接 1-关联 2-链路
"controlBehavior": 0//流量控制效果 0-直接拒绝 1-Warm Up 2-匀速排队
},
...
]
//降级的json内容
[
{
"resource": "/resource",//资源名称
"grade": 0,//熔断策略,0-慢调用比例 1-异常比例 2-异常数策略
"count": 500, //慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
"timeWindow": 10,//熔断时长,单位为 s
"slowRatioThreshold": 0.5,//慢调用比例阈值,仅慢调用比例模式有效
"minRequestAmount": 5,//熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断
"statIntervalMs": 1000//滑动窗口时长(统计单位时间,ms)
},
....
]
网关流控
ReadableDataSource<String, Set<GatewayFlowRule>> flowDataSource =
new NacosDataSource<>("192.168.195.188:8848",
"DEFAULT_GROUP",
"gateway-flow-rule",
json -> JSON.parseObject(json, new TypeReference<Set<GatewayFlowRule>>() {}));
GatewayRuleManager.register2Property(flowDataSource.getProperty());
[
{
"resource": "xxxx",//资源名称
"resourceMode": 0, //规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME)"
"grade": 1,//限流类型 0-线程数 1-QPS
"count": 10, //限流阈值
"intervalSec": 1 //统计时间窗口,单位是秒,默认是 1 秒。
}
]
7.10 集群流控
什么是集群流控?
假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很多(比如有 100 台)。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例都与这台 server 通信来判断是否可以调用。这就是最基础的集群流控的方式。
另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机维度去限制的话会无法精确地限制总体流量。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。
集群限流的模式
集群流控中共有两种身份:
- Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
- Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。
7.10.1 集群流控服务端的配置
1)初始化服务端配置
<!-- sentinel 集群流控 服务端的依赖 -->
<dependency>
<groupId>***.alibaba.csp</groupId>
<artifactId>sentinel-cluster-server-default</artifactId>
</dependency>
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>***.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
package ***.ken.ability.sentinel.application;
import ***.alibaba.csp.sentinel.cluster.server.ClusterTokenServer;
import ***.alibaba.csp.sentinel.cluster.server.SentinelDefaultTokenServer;
import ***.alibaba.csp.sentinel.cluster.server.config.ClusterServerConfigManager;
import ***.alibaba.csp.sentinel.cluster.server.config.ServerTransportConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.***mandLineRunner;
import org.springframework.stereotype.***ponent;
/**
* Sentinel集群流控Server端 初始化配置
*/
@***ponent
public class SentinelClusterInit implements ***mandLineRunner {
@Value("${server.port}")
private Integer port;
@Override
public void run(String... args) throws Exception {
// 加载服务配置
ClusterServerConfigManager.loadGlobalTransportConfig(new ServerTransportConfig()
.setIdleSeconds(600)
.setPort(port));
//启动服务
ClusterTokenServer tokenServer = new SentinelDefaultTokenServer();
tokenServer.start();
//配置为服务端
ClusterStateManager.applyState(ClusterStateManager.CLUSTER_SERVER);
//根据namespace 加载对应的集群限流规则
ClusterFlowRuleManager.setPropertySupplier(namespace -> {
ReadableDataSource<String, List<FlowRule>> ds = new NacosDataSource<>(
"nacos地址:端口",
"group组名称",
namespace + "流控配置文件的dataid",
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
SentinelProperty<List<FlowRule>> property = ds.getProperty();
return property;
});
//配置动态的命名空间数据源 -- 必须放在最后
ReadableDataSource<String, Set<String>> namespaceDs = new NacosDataSource<>(
"nacos地址:端口",
"group组名称",
"namespace配置的dataid",
source -> JSON.parseObject(source, new TypeReference<Set<String>>() {}));
//注册Sentinel集群流控服务端的namespace列表(每个namespace就代表一个微服务)
ClusterServerConfigManager.registerNamespaceSetProperty(namespaceDs.getProperty());
}
}
2)nacos上提供对应的配置信息
//namespace的配置,每个namespace对应一个微服务
[
"appA",
"appB",
....
]
appA-flow-rule
//appA - 对应的流控规则
[
{
"resource":"/resources",
"grade":1,
"count":1000,
"clusterMode":true, //是否开启集群流控 true表示开启
"clusterConfig":{
"flowId":1231241, // (必需)全局唯一的规则 ID,由集群限流管控端分配
"thresholdType":1, //阈值模式,默认(0)为单机均摊,1 为全局阈值.
"strategy": 0, //集群策略
"fallbackToLocalWhenFail":true //在 client 连接失败或通信失败时,是否退化到本地的限流模式
}
}
]
7.10.2 集群流控客户端的配置
1)配置服务端地址,连接到服务端
<dependency>
<groupId>***.alibaba.csp</groupId>
<artifactId>sentinel-cluster-client-default</artifactId>
</dependency>
/**
* 配置当前模式为客户端
*/
ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);
//配置服务端地址
ReadableDataSource<String, ClusterClientAssignConfig> ds =
new NacosDataSource<>(
"nacos地址:端口",
"group组名称",
"流控服务端地址配置文件的dataid",
source -> JSON.parseObject(source,
new TypeReference<ClusterClientAssignConfig>() {}));
ClusterClientConfigManager.registerServerAssignProperty(ds.getProperty());
//配置客户端相关配置
ReadableDataSource<String, ClusterClientConfig> ds2 =
new NacosDataSource<>(
"nacos地址:端口",
"group组名称",
"流控服务端地址配置文件的dataid",
source -> JSON.parseObject(source,
new TypeReference<ClusterClientConfig>() {}));
ClusterClientConfigManager.registerClientConfigProperty(ds2.getProperty());
2)配置从nacos上读取流控规则
//配置从Nacos上获取流控规则
ReadableDataSource<String, List<FlowRule>> ds3 =
new NacosDataSource<>(
"nacos地址:端口",
"group组名称",
appName + "流控规则配置文件的dataid",
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(ds3.getProperty());
3)Nacos上提供对应的配置信息
//流控服务端的地址
{
"serverHost":"ip",
"serverPort":端口,
"requestTimeout": 1000//超时时间
}
八、Seata分布式事务
分布式事务的常规解决方案
1、2PC - 两段提交协议(3PC)
2、T*** - 柔性事务(2PC)
3、基于MQ的最终一致性
8.1 Seata简介
Seata官网
Seata
Seata是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、T***、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata相关概念
TC (Transaction */) - 事务协调者维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
8.2 Seata服务端安装(Transaction Coordinator)
1)下载seata-server端的程序
2)上传至linux服务,并且解压
unzip seata-server-1.4.2.zip
cd seata/seata-server-1.4.2
3)配置seata-server注册到nacos上并且使用nacos管理seata-server配置(conf/register.conf文件)
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos" #修改注册中心类型为nacos
nacos { #配置nacos的相关地址
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
......
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos" #修改配置中心的类型为nacos
nacos { #配置nacos的相关地址
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
.....
}
4)基于seata官方脚本,将seata-server相关配置上传到nacos,并做相关修改
上传命令:
nacos/nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 5a3c7d6c-f497-4d68-a71a-2e5e3340b3ca -u username -w password
-h nacos的服务器地址
-p nacos端口
-g 配置分组名称
-t 配置命名空间
-u nacos认证用户名
-p nacos认证密码
修改nacos上的配置数据,改为数据库的存储方式
5)mysql数据创建seata数据库,并且导入相关表(基于官网给出的数据库文件)
6)启动seata-server,并查看nacos注册服务
bin/seata-server.sh [-h 指定注册到注册中心的ip地址]
8.3 Seata服务端安装(Docker版)
1)准备docker-***pose.yml
version: "3.1"
services:
seata-server:
image: seataio/seata-server:1.4.2
hostname: seata-server
container_name: seata-server
restart: always
ports:
- "8091:8091"
environment:
- SEATA_PORT=8091 #指定seata-server启动的端口, 默认为 8091
- SEATA_IP=xx.xx.xx.xx #指定seata-server启动的IP, 该IP用于向注册中心注册时使用,
- SEATA_CONFIG_NAME=file:/root/seata-config/registry #指定配置文件位置, 如 file:/root/registry, 将会加载 /root/registry.conf 作为配置文件
volumes:
- ./seata/config:/root/seata-config
8.4 微服务配置Seata客户端,实现分布式事务
1)微服务添加相关依赖
<dependency>
<groupId>***.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2)application.yml添加相关配置
.......
seata:
#配置seata注册到nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 192.168.195.135:8848
group : "SEATA_GROUP"
namespace: "public"
username: "nacos"
password: "nacos"
#配置seata读取nacos配置中心
config:
type: nacos
nacos:
server-addr: 192.168.195.135:8848
group: "SEATA_GROUP"
namespace: "seata-config"
username: "nacos"
password: "nacos"
#配置分布式事务组的名称
tx-service-group: my_test_tx_group
3)在微服务业务链中所有的业务库依次添加回滚记录表
-- 注意此处0.7.0+ 增加字段 context
create table `undo_log` (
`id` bigint(20) not null auto_increment,
`branch_id` bigint(20) not null,
`xid` varchar(100) not null,
`context` varchar(128) not null,
`rollback_info` longblob not null,
`log_status` int(11) not null,
`log_created` datetime not null,
`log_modified` datetime not null,
primary key (`id`),
unique key `ux_undo_log` (`xid`,`branch_id`)
) engine=innodb auto_increment=1 default charset=utf8;
4)在业务主方法上添加指定注解,并且测试
@Override
@GlobalTransactional
public int insertUser(User user) {
//业务方法体
return result;
}
8.5 Seata AT模式原理与使用
前置条件
- 基于本地ACID事务的关系型数据库
- Java应用,通过JDBC访问数据库
机制原理
两段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
- 二阶段: 提交异步化,快速完成 回滚通过一阶段的回滚日志进行反向补偿
工作流程
业务表:product
Field |
Type |
Key |
id |
bigint(20) |
PRI |
name |
varchar(100) |
|
since |
varchar(100) |
AT 分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
**一阶段**
过程:
- 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息。
- 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';
得到前镜像:
1 |
TXC |
2014 |
id |
name |
since |
- 执行业务 SQL:更新这条记录的 name 为 'GTS'。
- 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, since from product where id = 1;
id |
name |
since |
1 |
GTS |
2014 |
- 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
- 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
- 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
- 将本地事务提交的结果上报给 TC。
**二阶段-回滚**
- 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
- 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
**二阶段-提交**
- 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
注意:Seata默认的分布式事务就是基于AT模式的实现
8.6 Seata T***模式的原理与实现
T***机制
T*** 模式,是指支持把自定义的分支事务纳入到全局事务的管理中,并不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 ***mit 行为:调用 自定义 的 ***mit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
Seata实现T***模式
1)创建T***接口
package ***.qf.t***;
import ***.qf.entity.User;
import io.seata.rm.t***.api.BusinessActionContext;
import io.seata.rm.t***.api.BusinessActionContextParameter;
import io.seata.rm.t***.api.LocalT***;
import io.seata.rm.t***.api.TwoPhaseBusinessAction;
@LocalT***
public interface IUserT*** {
@TwoPhaseBusinessAction(name = "userService", ***mitMethod = "insertUser***mit", rollbackMethod = "insertUserRollback")
int insertUser(@BusinessActionContextParameter(paramName = "userKey") User user);
boolean insertUser***mit(BusinessActionContext context);
boolean insertUserRollback(BusinessActionContext context);
}
**2)实现T***接口,并在业务层调用该T***方法**
package ***.qf.service.impl;
import ***.qf.entity.User;
import ***.qf.feign.Test1Feign;
import ***.qf.service.IUserService;
import ***.qf.t***.IUserT***;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private Test1Feign test1Feign;
@Autowired
private IUserT*** userT***;
@Override
@GlobalTransactional
public int insertUser(User user) {
user.setId(1);
int result = userT***.insertUser(user);
System.out.println("业务层添加用户:" + user);
String result2 = test1Feign.test1(user.getId());
System.out.println("远程调用添加用户详细信息:" + result2);
return result;
}
}
T***注解详解
- @LocalT*** (必要)该注解需要添加到上面描述的接口上,表示实现该接口的类被 seata 来管理,seata 根据事务的状态,自动调用我们定义的方法,如果没问题则调用 ***mit 方法,否则调用 Rollback 方法。
- @TwoPhaseBusinessAction (必要)该注解用在接口的 Try 方法上
- name 为 t*** 方法的 bean 名称,需要全局唯一,一般写方法名即可;
- ***mitMethod 自然地写 ***mit 方法的方法名;
- rollbackMethod 写 Rollback 方法的方法名;
- @BusinessActionContextParameter该注解用来修饰 Try 方法的入参,被修饰的入参可以在 ***mit 方法和 Rollback 方法中通过 BusinessActionContext 获取。
注意:Seata允许多种模式混合使用管理分布式事务,比如微服务A使用AT模式,微服务B使用T***模式