微服务
是一种软件架构风格,以专注于单一职责的很多小型项目为基础,组合出复杂的大型应用
mybatisPlus
是通过扫描实体类,并基于反射获取信息作为数据库表信息
第一步:引入mybatisPlus依赖
第二步:进行配置
第三步:自定义Mapper基础BaseMapper
第四步:在实体类上添加注解声明表信息
在application.yml中根据需要添加配置信息
常见注解
类名驼峰转下划线作为表名
名为id的字段作为主键
@TableName:用来指定表名
@TableId:用来指定表中的主键字段信息
AUTO:数据库自增长
INPUT:通过set方法自行输入
ASSIGN_ID:分配ID.接口identifierGenerator的方法nextid来生成id
默认实现类雪花算法
@TableField:用来指定表中的普通字段信息
1.成员变量名于数据库字段不一致
2.成员变量名是以is开头,且是布尔值
例:private Boolean isMarried
在底层通过反射机制获取名称,通过反射处理会去掉is剩下的作为变量名,会于数据库变量名不一致,因此需要加上@TableField(“is_married ”)
3.成员变量名于数据库关键字冲突
例:order和数据库中的变量完全一致,但是于数据库中的orderby关键字冲突
@TableField(“ `order`”)(转义字符)
4.成员变量不是数据库中的字段
@TableField(exist = false) 标记不是数据库字段
常见配置
配置继承MyBatis原生配置和自己独有配置
mybatis-plus:
type-aliases-package: com.包名.mp.xx.po #别名扫描包
mapper-locations:“classpath*:/mapper/**/*.xml” #Mapper.xml 文件地址,默认值
configuration:
map-underscore-to-camel-case:true # 是否开启下划线和驼峰的映射
cache-enabled:false # 是否开启二级缓存
global-config:
db-type:
id-type:assign_id# id为雪花算法生成
update-strategy:not_null# 更新策略:只更新非空字段
核心内容
条件构造器:MyBatisPlus支持各种复杂的where条件,可满足日常开发的所有需求
QueryWrapper和LambdaQueryWrapper通常用来构建select,delete,update的where条件部分
UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用
尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码
自定义SQL:利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分
1.基于Wrapper构建where条件
2.在mapper方法参数中用Param注解声明wrapper变量名称,必须ew
3.自定义SQL,并使用Wrapper条件
${ ew.customSqlSegment }是一个占位符,where条件进行拼接
什么时候使用自定义SQL:除了where条件以外的部分不方便用wrapper生成
Service接口:
(JDBC批处理删除方案通常比IN删除在性能上更高效,尤其是在处理大量数据时。
JDBC批处理删除方案和IN删除的区别主要体现在以下几个方面:
-
性能差异:
- JDBC批处理删除:通过使用
PreparedStatement
的addBatch()
和executeBatch()
方法,可以将多个删除操作批量发送到数据库,从而减少网络往返次数和提高整体执行效率。 - IN删除:通常是在SQL语句中使用
IN
子句一次性指定多个值进行删除,虽然也较为高效,但当处理的数据量非常大时,可能会导致SQL语句的长度限制问题或者执行计划的优化不足。
- JDBC批处理删除:通过使用
-
适用场景:
- JDBC批处理删除:适用于需要从数据库中删除大量数据的情况,特别是当这些数据分布在不同的行中,且数量庞大时。
- IN删除:适用于需要根据特定的一组值来删除数据,且这些值的数量相对较少,不会导致SQL语句过长或性能问题。
-
实现方式:
- JDBC批处理删除:通过循环设置参数并调用
addBatch()
方法将每个删除操作添加到批处理中,然后执行executeBatch()
一次性执行所有操作。 - IN删除:直接在SQL语句中构造
IN
子句,如DELETE FROM table WHERE id IN (1, 2, 3)
。
- JDBC批处理删除:通过循环设置参数并调用
综上所述,JDBC批处理删除方案在处理大量数据删除时通常更为高效,因为它可以减少网络传输的开销和提高数据库的并发处理能力。而IN删除则适用于数据量不大时的快速删除操作。在实际应用中,应根据具体需求和数据量大小选择合适的删除方法。)
MP的Service接口使用流程:
自定义Service 接口继承IService<实体>
自定义Service实现类,实现自定义接口并继承ServiceImpl类
继承的实现类需要给使用的mapper和实体
IService批量新增:
普通for循环插入:插入速度极差
IService的批量插入:MP的批量新增,基于预编译批处理,性能不错,但还是一条一条sl进行插入
开启&rewriteBatchedStatements=ture参数:能多条sql进行插入
拓展功能
代码生成: 安装MybatisPlus插件
静态工具:当多个service之间相互调用,采用传统注入方式,会形成循环依赖,
因此可以使用Db静态工具进行调用
逻辑删除 :基于代码逻辑模拟删除效果,但并不会真正删除数据
1.在表中添加一个字段把标记置为1
2.当删除数据时把标记置为1
3.查询时只查询标记为0的数据
缺点:会导致数据库表垃圾数据越来越多,影响查询效率
SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此不太推荐采用逻辑删除功能,如果数据不能删除,可以采用数据迁移到 其他表的办法
使用:在a.yaml进行配置
mybatis-puls:
global-config:
db-config:
logic-delete-field: flag #全局逻辑删除的实体字段名,字段类型可以是 boolean,integer
logic-delete-value:1#逻辑已删除值
logic-not-delete-value: 0#逻辑为删除值
枚举处理器:在a.yml中配置
1.给枚举中的与数据库对应value值添加@EnumValue注解
2.在配置文件中配置统一的枚举处理器,实现类型转换
JSON处理器:1.字段上定义处理器 2.开启自动的autoResultMap映射
可以实现json和java对象的转换
插件功能
分页插件
在配置类中注册MyBatispuls的核心插件,同时添加分页插件
新建工具包config.MyBatisConfig MybatisPlusInterceptor核心插件,添加分页 插件
通用分页实体:创建一个专门用于分页的实体(详细黑马2024springcloud 20集
Docker
1.卸载旧版
首先如果系统中已经存在旧的Docker,则先卸载:
yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine
2.配置Docker的yum库
首先要安装一个yum工具
yum install -y yum-utils
安装成功后,执行命令,配置Docker的yum源:
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
3.安装Docker
最后,执行命令,安装Docker
yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
4.启动和校验
# 启动Docker systemctl start docker # 停止Docker systemctl stop docker # 重启 systemctl restart docker # 设置开机自启 systemctl enable docker # 执行docker ps命令,如果不报错,说明安装启动成功 docker ps
5.配置镜像加速
这里以阿里云镜像加速为例。
5.1.注册阿里云账号
首先访问阿里云网站:
注册一个账号。
5.2.开通镜像服务
在首页的产品中,找到阿里云的容器镜像服务:
点击后进入控制台:
首次可能需要选择立刻开通,然后进入控制台。
5.3.配置镜像加速
找到镜像工具下的镜像加速器:
sudo mkdir -p /etc/docker
https://xxx.mirror.aliyuncs.com
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [""]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
1.1.部署MySQL
使用Docker安装,仅仅需要一步即可,在命令行输入下面的命令(建议采用CV大 法):
docker run -d \
--name mysql \
-p 3307:3306 \
-e TZ=Asia/Shanghai \
-e MYSQL_ROOT_PASSWORD=123 \
mysql
-
docker run -d
:创建并运行一个容器,-d
则是让容器以后台进程运行 -
--name
mysql
: 给容器起个名字叫mysql
,你可以叫别的 -
-p 3306:3306
: 设置端口映射。-
容器是隔离环境,外界不可访问。但是可以将宿主机端口映射容器内到端口,当访问宿主机指定端口时,就是在访问容器内的端口了。
-
容器内端口往往是由容器内的进程决定,例如MySQL进程默认端口是3306,因此容器内端口一定是3306;而宿主机端口则可以任意指定,一般与容器内保持一致。
-
格式:
-p 宿主机端口:容器内端口
,示例中就是将宿主机的3306映射到容器内的3306端口
-
-
-
e
TZ=Asia/Shanghai
: 配置容器内进程运行时的一些参数-
格式:
-e KEY=VALUE
,KEY和VALUE都由容器内进程决定 -
案例中,
TZ=Asia/Shanghai
是设置时区;MYSQL_ROOT_PASSWORD=123
是设置MySQL默认密码
-
-
mysql
: 设置镜像名称,Docker会根据这个名字搜索并下载镜像-
格式:
REPOSITORY:TAG
,例如mysql:8.0
,其中REPOSITORY
可以理解为镜像名,TAG
是版本号 -
在未指定
TAG
的情况下,默认是最新版本,也就是mysql:latest
-
Docker本身包含一个后台服务,我们可以利用Docker命令告诉Docker服务,帮助我们快速部署指定的应用。Docker服务部署应用时,首先要去搜索并下载应用对应的镜像,然后根据镜像创建并允许容器,应用就部署完成了。
2.Docker基础
接下来,我们一起来学习Docker使用的一些基础知识,为将来部署项目打下基础。具体用法可以参考Docker官方文档:
https://docs.docker.com/
2.1.常见命令
首先我们来学习Docker中的常见命令,可以参考官方文档:
https://docs.docker.com/engine/reference/commandline/cli/
命令介绍
其中,比较常见的命令有:
命令 | 说明 | 文档地址 |
---|---|---|
docker pull | 拉取镜像 | |
docker push | 推送镜像到DockerRegistry | |
docker images | 查看本地镜像 | |
docker rmi | 删除本地镜像 | |
docker run | 创建并运行容器(不能重复创建) | |
docker stop | 停止指定容器 | |
docker start | 启动指定容器 | |
docker restart | 重新启动容器 | |
docker rm | 删除指定容器 | |
docker ps | 查看容器 | |
docker logs | 查看容器运行日志 | |
docker exec | 进入容器 | |
docker save | 保存镜像到本地压缩文件 | |
docker load | 加载本地压缩文件到镜像 | |
docker inspect | 查看容器详细信息 |
docker命名别名
设置 :vi ~/.bashrc
例:alias dps='docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}\t{{.Names}}"'
alias dis='docker images'
生效 :source ~/.bashrc
2.2.数据卷
容器是隔离环境,容器内程序的文件、配置、运行时产生的容器都在容器内部,我们要读写容器内的文件非常不方便。大家思考几个问题:
-
如果要升级MySQL版本,需要销毁旧容器,那么数据岂不是跟着被销毁了?
-
MySQL、Nginx容器运行后,如果我要修改其中的某些配置该怎么办?
-
我想要让Nginx代理我的静态资源怎么办?
因此,容器提供程序的运行环境,但是程序运行产生的数据、程序运行依赖的配置都应该与容器解耦。
什么是数据卷
数据卷(volume)是一个虚拟目录,是容器内目录与宿主机目录之间映射的桥梁。
以Nginx为例,我们知道Nginx中有两个关键的目录:
-
html
:放置一些静态资源 -
conf
:放置配置文件
如果我们要让Nginx代理我们的静态资源,最好是放到html
目录;如果我们要修改Nginx的配置,最好是找到conf
下的nginx.conf
文件。
案例:mysql容器的数据挂载
需求:查看mysql容器,判断是否有数据卷挂载
基于宿主目录实现MySQL数据目录,配置文件,初始化脚本的挂载
1.挂载/root/mysql/data到容器内的/var/lib/mysql目录
2.挂载/root/mysql/init到容器内的/docker-entrypoint-initdb.d目录,携带SQL脚本
3.挂载/root/mysql/conf到容器内的/etc/mysql/conf.d目录,携带配置文件
在执行docker run命令时使用-v本地目录:容器内目录 可以完成本地目录挂载
本地目录必须以“/” 或“。/”开头,如果直接以名称开头,会被识别为一个数据卷而非本地 目录
自定义镜像
镜像是包含了应用程序,程序运行的系统函数库,运行配置等文件包。构建镜像的过程其实就是把上述文件打包的过程
镜像结构:
入口(Entrypoint)镜像运行入口,一般是程序启动的脚本和参数
层(layer)添加安装包,依赖,配置等,每次操作都形成新的一层
基础镜像(Baselmage)应用依赖的系统函数库,环境,配置,文件等
Dockerfile是一个文件,其中包含一个个指令,用来说明要执行什么操作来构建镜像。将来Docker可以根据Dockerfile帮我们构建镜像,常见命令
指令 | 说明 | 示例 |
FROM | 指定基础镜像 | FROM centos:6 |
ENV | 设置环境变量,可在后面指令使用 | ENV key value |
COPY | 拷贝本地文件到镜像的指定目录 | COPY ./jrell.tar.gz /tmp |
RUN | 执行Linux的shell命令,一般是安装过程的命令 | RUN tar -zxvf /tmp/jrell.tar.gz&& EXPORTS path=/tmp/jrell:$path |
EXPOSE | 指定容器运行时监听的端口,是给镜像使用者看的 | EXPOSE 8080 |
ENTRYPOINT | 镜像中应用的启动命令,容器运行时调用 | ENTRYPOINT java -jar xx.jar |
官方文档:https://docs.docker.com/engine/reference/builder
当编写好Dockerfile,用下面命令构建镜像
docker build -t myImage:1.0_.
网络
默认情况下,所有容器都是以brdge方式连接到Docker的一个虚拟网桥上
但当创建容器顺序不同时ip地址就会改变,因此应该自定义网络
加入自定义网络的容器才可以通过容器名相互访问
命令 | 说明 |
docker network create | 创建一个网络 |
docker network ls | 查看所有网络 |
docker network rm | 删除指定网络 |
docker network prune | 清除未使用网络 |
docker network connect | 使指定容器连接加入某网络 |
docker network disconnect | 使指定容器连接离开某网络 |
docker network inspect | 查看网络详细信息 |
DockerCompose
类型 | 参数或指令 | 说明 |
Options | -f | 指定compose文件的路径和名称 |
-p | 指定project名称 | |
Commands | up | 创建并启动所有service容器 |
down | 停止并移除所有容器,网络 | |
ps | 列出所有启动的容器 | |
logs | 查看指定容器的日志 | |
stop | 停止容器 | |
start | 启动容器 | |
restart | 重启容器 | |
top | 查看运行的进程 | |
exec | 在指定的运行中容器执行命令 |
单体架构
将业务所有功能集中在一个项目中开发,打成一个包部署
优点:架构简单,部署成本低
缺点:团队协作成本高,系统发布效率低,系统可用性差
适合开发功能相对简单规模较小的项目
微服务架构
是服务化思想指导下的一套最佳实践架构方案。服务化就是把单体架构中的功能模块拆分为多个独立项目
SpringCloud
集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配
服务拆分原则
创业型项目:先采用单体架构,快速开发,快速试错,随着规模扩大,逐渐拆分
确定大型项目:资金充足,目标明确,直接采用微服务架构,避免后续拆分麻烦
拆分目标:
高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高,完整度高
低耦合:每个微服务的功能要相对独立,尽量减少对其他微服务的依赖
纵向拆分:按照业务模块拆分
横向拆分:抽取公共服务,提高复用性
拆分服务
独立Project Maven聚合
远程调用
Spring给我们提供的RestTemplate工具,可以方便的实现Http请求的发送
1.注入RestTemplate到Spring容器
2.发起远程调用
RestTemplate存在许多缺点,url地址是写死的,当要访问的模块发生异常端口无法访问,就会出错,或端口发生改变同理
注册中心原理
服务治理中的三个角色分别是:
服务提供者:暴露服务接口,供其他服务调用
服务消费者:调用其他服务提供的接口
注册中心:记录并监控微服务各实例状态,推送服务变更信息
消费者如何知道提供者的地址?
服务提供者会在启动时注册自己信息到注册中心,消费者可以从注册中心订阅和拉取服务信息
消费者如何知道服务状态变更?
服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者
当提供者有多个实例时,消费者该选择哪一个?
消费者通过负载均衡算法从多个实例中选择一个
Nacos注册中心
Nacos是国内产品,中文文档比较丰富,而且同时具备配置管理功能
服务注册
添加依赖
<!--nacos 服务注册发现-->
<dependency> <groupId>com.alibaba.cloud</groupId>
<artifactId>
spring-cloud-starter-alibaba-nacos-discovery
</artifactId>
</dependency>
配置Nacos
application.yml
中添加nacos地址配置:
spring: application: name: item-service # 服务名称 cloud: nacos: server-addr: 192.168.150.101:8848 # nacos地址
服务发现
消费者需要连接nacos以拉取和订阅服务,因此服务发现的前两步与服务注册是一样,后面再加上服务调用即可:
1.引入nacos discovery依赖
2.配置nacos地址
3.服务发现
OpenFeign
是一个声明式的http客户端,是springCloud在Feign基础是改造,作用是基于SpringMVC的常见注解,帮我们优雅的实现http请求的发送
1.引入依赖包括OpenFeign和负载均衡组件SpringCloudLoadBalancer
2.通过@EnableFeignClients注解,启动OpenFeign功能
3.编写FeignClient
@FeignClient(value= “ ”)//通过项目名称获取服务的实例列表
public interface ItemClient {
@GetMapping(“/items”)//请求方式和url
List<ItemDOT> queryItemByIds(@RequestParam("ids") Collection<Long> ids);//返回值类型和请求参数
}
4.使用FeignClient,实现远程调用
连接池
连接池通过预先创建并维护一定数量的数据库连接,避免应用程序每次访问数据库时都要重新建立连接,从而提高数据库操作的性能和速度。在Java应用程序开发中,常见的连接池有DBCP、C3P0、Proxool等。Druid连接池是一个常用的选择,特别是在与Spring Boot和Nacos集成的环境中。Druid不仅提供高效的连接池管理,还包括强大的监控和扩展功能。
OpenFeign对Http请求做了优雅的伪装,不过其底层发起的http请求,依赖于其他的框架这些框架可以自己选择包括以下3种:
HttpURLConnection:默认实现,不支持连接池
Apacyhe HttpClient:支持连接池
OKHttp:支持连接池
OpenFeign整合OKHttp步骤:
1.引入依赖
2.开启连接池功能
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。
两种解决方法:
1.指定FeignClient所在包
@EnableFeignClients(basePackages = "com.hmall.api.clients")
2.指定FeignClient字节码
@EnableFeignClient (clients = {UserClient.class})
日志
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
1.NONE:不记录任何日志信息,这是默认值
2.BASIC:仅记录请求的方法,url以及响应状态码和执行时间
3.HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
4.FULL:记录所有请求和响应的明细,包括头信息,请求体,元数据
由于Feign默认的日志级别是NONE,所以默认我们看不到请求日志
自定义日志级别:
需要声明一个类型为Logger.Level的Bean
但这个Bean并未生效,想要配置某个FeignClient的日志,可以在@FeignClient注解中声明:
@FeignClient(value = “item-service”,configuration = DefaultFeignConfig.class)//局部配置
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)//全局配置
网关
网络的关口,负责请求的路由,转发,身份校验
SpringCloud中网关的实现包括两种:
1.SpringCloud Gateway 基于WebFlux响应式编程
2.Netfilx Zuul 基于Servlet的阻塞式编程
步骤:
1.创建新模块 2.引入网关依赖 3.编写启动类 4.配置路由规则
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 192.168.x.xx:8848
gateway:
routes:
- id: item-service
uri: lb://item-service
predicates:
- Path=/items/**,/search/**
路由属性:
网关路由对应的Java类型是RouteDefinition,其中常见的属性:
id:路由唯一标示
uri:路由目标地址
predicates:路由断言,判断请求是否符合当前路由
filters:路由过滤器,对请求或响应做特殊处理
SpringCloudGateway中支持的断言类型有很多:
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=**.somehost.org,**.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
weight | 权重处理 |
路由过滤器:
网关中提供了33中路由过滤器,每种过滤器都有独特作用
网关登录校验
1.如何在网关转发之前做登录校验?
通过自定义过滤器pre进行校验成功后传入web(控制顺序)
把网关中的用户通过保存用户请求头的方式进行传递
微服务之间通过保存用户请求头进行传递
2.网关如何将用户信息传递给微服务?
3.如何在微服务之间传递用户信息?
自定义过滤器:
GlobalFilter:全局过滤器
实现GloballFilter接口,通过exchange.getRequest获取请求
然后获取请求头,通过chain.filter放行并传给下一个过滤器
之后实现Ordered接口进行控制顺序提高优先级
GatewayFilter:路由过滤器,作用任意指定的路由;默认不生效要配置到路由后生效
继承过滤器工厂AbstractGatewayFilterFactory<参数>,之后想要实现控制顺序有两种方法,返回一个装饰的匿名内部类OrderedGatewayFilter(new GatewayFilter(){},order),另一种在外部单独定义一个类,并实现ordered接口
自定义配置属性,成员变量名称
将变量名称依次返回,顺序很重要,读参数时需要按顺序获取
将Config字节码传递给父类,父类负责读取yaml配置
实现登录校验:
网关传递用户
在网关过滤器中拿到保存用户信息的请求头,拦截器(每个微服务都可能获取登录用户的需求,所以把拦截器定义在公共模块上)获取用户保存到ThreadLocal,每个业务通过ThreadLocal获取用户信息
定义一个不需要拦截的拦截器实现WebMvcConfigurer接口add一个拦截器,由于处于不同包下,通过springboot的spring.factors扫瞄,由于存在被别的微服务引用,但不属于Mvc出现错误所以需要增加启动条件,限定只有存在Mvc的微服务可以使用(@ConditionalOnClass(DispatcherServlet.class))
OpenFeign传递用户
用户之间传递必须携带请求头,才能获取用户信息
使用OpenFeign中提供的拦截器接口RequestInterceptor,所有OpenFeign发起的请求都会先调用拦截器处理请求
配置管理
配置共享
添加一些配置到Nacos,包括:jdbc,MybatisPuls,日志,Swagger,OpenFeign等配置
拉取共享配置:
基于NacosConfig拉取共享配置代替微服务的本地配置
引入依赖:
<!--nacos配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--读取bootstrap文件--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
配置热更新
当修改配置文件中的配置时,微服务无需重启即可使配置生效
微服务中要以特定的方式读取需要热更新的配置属性
动态路由
实现动态路由首先要将路由配置保存到nacos,当nacos中的配置变更时,推送最新配置到网关,实时更新网关中的路由信息
1.监听nacos配置变更的消息
2.当配置变更时,将最新的路由信息更新到网关路由表
步骤:
1.首先引入依赖到网关中
<!--nacos配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--读取bootstrap文件--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
2.配置bootstrap.yaml文件
微服务名字,配置环境,nacos服务地址,文件后缀yaml,共享日志
3.创建动态路由加载器
需要在项目启动时加载,加入Bean中,使用@postConstruct,让路由监听器在bean初始化之后执行
然后需要拿到configService需要注入NacosConfigManager,拉取配置添加监听器,通过dataId,group监听配置变更之后,需要更新路由表,当第一次读取到配置,也需要更新到路由表
路由表更新:
监听到路由信息后,可以利用RouteDefinitionWriter来更新路由表
由于获取的路由信息存在yaml中,所以需要解析yaml文件,但不方便,所以使用json格式的路由配置
解析配置信息,转为RouteDefinittion
private void updateConfigInfo(String configInfo) {
//解析配置信息,转为RouteDefinition
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 删除之前的路由表
for (String routeId : routeIds){
routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
// 重新加载路由表
for (RouteDefinition routeDefinition : routeDefinitions){
//更新路由表
routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
//记录路由id,方便下一次更新时删除
routeIds.add(routeDefinition.getId());
}
}
服务保护和分布式事务
雪崩问题
微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用
产生原因:
1.微服务相互调用,服务提供者出现故障或阻塞
2.服务调用者没有做好异常处理,导致自身故障
3.调用链中的所有服务级联失败,导致整个集群故障
解决思路:
1.尽量避免服务出现故障或阻塞
保证代码健壮性,保证网络通畅,能应对较高的并发请求
2.服务调用者做好远程调用异常的后备方案,避免故障扩散
服务保护方案
服务保护方案-请求限量
限制访问微服务的请求的并发量,避免服务因流量激增出现故障
服务保护方案-线程隔离
也叫舱壁模式,模拟船舱隔板的防水原理,通过限定每个业务能使用的线程数量而将故障业务隔离,避免故障扩散
服务保护方案-服务熔断
由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,拦截该接口的请求
失败处理
熔断期间,所有请求快速失败,全都走fallback逻辑
服务保护技术
微服务保护的技术有很多,但在目前国内使用较多的还是Sentinel
Sentinel
是阿里巴巴开源的一款流量控制组件
簇点链路:单机调用链路,是一次请求进入服务后经过的每一个被Sentinel监控的资源链,默认Sentinel会监控SpringMVC的每一个Endpoint(http接口)。限流,熔断等都是针对簇点链路中的资源设置的。而资源名默认就是接口的请求路径
Restful风格的API请求路径一般都相同,这会导致簇点资源名称重复。因此需要修改配置,把请求方式+请求路径作为簇点资源名称
请求限流
线程隔离
Fallback
1.将FeignClient作为Sentinel的簇点资源
feign:
sentinel:
enabled:true
2.FeignClient的Fallback有两种配置方式:
1.FallbackClass,无法对远程调用的异常做处理
2.FallbackFactory,可以对远程调用的异常做处理,通常都会选择这种
步骤:
自定义类,实现FallbackFactory,编写对某个FeignClient的fallback逻辑
服务熔断
熔断是解决雪崩问题的重要手段。由断路器统计服务调用的异常比例,慢请求比例,如果超出阈值则会熔断该服务。拦截一切访问服务请求;当服务恢复时,熔断器会放行该服务请求
断路器有三种状态:closed,open,half-open。默认为closed没有拦截,同时监控进入请求异常比例,慢请求比例,如果超出阈值就会进入open状态,拦截所有请求,但是有一定时间限制,到期之后会进入half-open状态,会放行一次请求检查是否还是有异常,存在异常回到open状态,不存在进入closed状态,关闭断路器
分布式事务
在分布式系统中,如果一个业务需要多个服务合作完成,而且每一个服务都有事务,多个事务必须同时成功或失败,这样的事务就是分布式事务。其中每个服务的事务技术一个分支事务。整个业务成为全局事务
Seata
解决分布式事务的方案有很多,但实现起来都比较复杂,因此一般会使用开源的框架来解决分布式事务问题。在众多的开源分布式事务框架中,功能最完善、使用最多的就是阿里巴巴在2019年开源的Seata。
Seata的事务管理中有三个重要的角色:
-
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
-
TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
-
RM (Resource Manager) - 资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
-
其中,TM和RM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。将来TM和RM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。
而TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。
部署TC服务
1.准备数据库表
2.准备配置文件
3.Docker部署
微服务集成Seata
1.引入依赖
<!--统一配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--读取bootstrap文件--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> <!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
2.在application.yml中添加配置,让微服务找到TC服务地址
XA模式
XA
规范 是 X/Open
组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM
与局部的RM
之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
一阶段工作:
1.RM注册分支事务到TC
2.RM执行分支业务sql但不提交
3.RM报告执行状态到TC
二阶段工作:
TC检测各分支事务执行状态
a.如果都成功,通知所有RM提交事务
b.如果都失败,通知所有RM回滚事务
RM接收TC指令,提交或回滚事务
优:事务强一致性,满足ACID原则。 常用数据库都支持,实现简单,并且没有代码入侵
缺:一阶段锁数据库资源,等待二阶段结束才释放,性能较差。依赖关系型数据库实现事务
Seata的starter已经完成了XA模式的自动装配,实现简单,步骤:
1.修改application.yaml文件(每个参与事务的微服务),开启XA模式
seata:
data-source-proxy-mode:XA
2.给全局事务的入口方法添加@GlobalTransactional注解
3.重启服务
AT模式
AT
模式同样是分阶段提交的事务模型,不过缺弥补了XA
模型中资源锁定周期过长的缺陷。
一阶段工作:
1.RM注册分支事务到TC
2.记录undo-log(数据快照)
3.RM执行分支业务sql提交
4.RM报告执行状态到TC
二阶段工作:
提交时RM删除undi-log
回滚时RM根据undo-log恢复数据到更新前
AT和XT最大区别:
XA模式一阶段不提交事务锁定了资源;AT模式一阶段模式直接提交
XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚
XA模式强一致,AT模式最终一致
RAbbitMQ
同步调用
时效性强,等待到结果后才返回,拓展性差,性能下降,级联失败问题
异步调用
通常是基于消息通知的方式,包含三个角色:
消息发送者:投递消息的人
消息接收者:接收和处理消息的人
消息代理:管理,暂存,转发消息
优势:解除耦合,拓展性好,无需等待,性能好,故障隔离,缓存消息,流量削峰填谷
缺点:不能立即得到调用结果,时效性差。不确定下游业务执行是否成功。业务安全依赖于Broker的可靠性
数据隔离
不同用户之间存在数据隔离
Java客户端
RabbitMQ官方提供的Java客户端编码相对复杂,一般生产环境下我们更多会结合Spring来使用。而Spring的官方刚好基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。
1.引入依赖
<!--AMQP依赖,包含RabbitMQ--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
2.配置服务端信息
3.利用RabbitTemplate
实现消息发送
4.利用@RabbitListener注解声明监听队列,监听消息
WorkQueue
让多个消费者绑定到一个队列,共同消费队列中的消息
消费者消息推送限制:
当出现多个消费者时,获取的消息采用轮询方式平均分配,当消费者处理速度不同时会降低效率
通过配置:
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
Fanout交换机
1) 可以有多个队列
2) 每个队列都要绑定到Exchange(交换机)
3) 生产者发送的消息,只能发送到交换机
4) 交换机把消息发送给绑定过的所有队列
5) 订阅队列的消费者都能拿到消息
Direct交换机
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
-
队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) -
消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 -
Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息
Topic交换机
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。
只不过Topic
类型Exchange
可以让队列在绑定BindingKey
的时候使用通配符!
BindingKey
一般都是有一个或多个单词组成,多个单词之间以.
分割,例如: item.insert
通配符规则:
-
#
:匹配一个或多个词 -
*
:匹配不多不少恰好1个词
声明队列交换机
SpringAMQP提供了几类,用来声明队列,交换机及其绑定关系:
Queue:声明队列,用工厂类QueueBuilder构建
Exchange:声明交换机,用工厂类ExchaneBuilder构建
Binding:声明队列和交换机的绑定关系,用工厂类BingBuilder构建
消息转换器
在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。
只不过,默认情况下Spring采用的序列化方式是JDK序列化。JDK序列化存在下列问题:
数据体积过大,有安全漏洞,可读性差
显然,JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。
在publisher
和consumer
两个服务中都引入依赖:
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.9.10</version> </dependency>
注意,如果项目中引入了spring-boot-starter-web
依赖,则无需再次引入Jackson
依赖。
MQ高级功能
发送者的可靠性
发送者重连
由于网络波动,可能会出现 发送者连接MQ失败的情况。通过配置可以开启连接失败后的重连机制:
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
注意:当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的。
如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。
发送者确认
SpringAMQP提供Publisher Confirm和Publisher Return两种确认机制。开启确认机制后,当发送者发送消息给MQ后,MQ会返回确认结果给发送者。返回的结果有以下几种情况:
1.消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功。
2.临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
3.持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功
4.其他情况都会返回NACK,告知投递失败
实现发送者确认:
1.在发送服务添加配置
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制并设置confirm类型 publisher-returns: true # 开启publisher return机制
publisher-confirm-type有三种模式:
1.none:关闭confirm机制
2.simple:同步阻塞等待MQ的回执消息
3.correlated:MQ异步回调返回方式返回回执消息
MQ的可靠性
在默认情况下RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。这样会导致两个问题。
1.一旦MQ宕机,内存中的消息会丢失
2.内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞
数据持久化
交换机持久化
队列持久化
消息持久化*
Lazy Queue
消息持久化会使并发能力下降,而LazyQueue能解决
1.接收消息后直接存入磁盘,不在存储到内存
2.消费者要消费消息时才会从磁盘中读取并加载到内存(可以提前缓存到内存,最多2048条)
消费者的可靠性
消费者确认机制是为了确认消费者是否成功处理消息。当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己的消息处理状态
SpringAMQP已经实现了消息确认功能。并允许通过配置文件选择ACK处理方式:
1.none:不处理。消息投递后立刻ack,消息会立刻从MQ删除
2.manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵
3.auto:自动模式。SpringAMQP利用AOP对消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack
当业务出现异常,根据异常判断返回不同的结果:
如果业务异常返回nack
如果消息处理或校验异常返回reject
失败重试机制是在消费者出现异常时利用本地重试,而不是无限的requeue传递给MQ。
但开启此功能会降低可靠性,消息失败3次后不在重试,因此还需要MessageRecoverer接口处理
它有3个不同实现:
-
RejectAndDontRequeueRecoverer
:重试耗尽后,直接reject
,丢弃消息。默认就是这种方式 -
ImmediateRequeueMessageRecoverer
:重试耗尽后,返回nack
,消息重新入队 -
RepublishMessageRecoverer
:重试耗尽后,将失败消息投递到指定的交换机
RepublishMessageRecoverer:
1.定义接收失败消息的交换机,队列及其绑定关系
2.定义RepublishMessageRecoverer
@Bean public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error"); }
业务幂等性:在程序开发中,同一个业务,执行一次或多次对业务状态的影响是一致的
方法一:
唯一消息ID
这个思路非常简单:
-
每一条消息都生成一个唯一的id,与消息一起投递给消费者。
-
消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
-
如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
SpringAMQP的MessageConverter自带了MessageID的功能,我们只要开启这个功能即可。
@Bean
public MessageConverter messageConverter(){ // 1.定义消息转换器 Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter(); // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息 jjmc.setCreateMessageIds(true); return jjmc; }
方法二:
结合业务逻辑,基于业务本身做判断
以支付业务为例:支付服务核心业务完成后进行边缘业务MQ通知,交易服务状态变为已支付,然后消费者进行退款,支付状态变为退款中,网络恢复,重新投递交易服务状态,会覆盖已退款变为已支付。
解决:当MQ通知前先查询支付状态,当处于未支付时可以变更已支付状态,当处于已退款或已支付状态,无法改变状态
如何保证支付服务与交易服务之间的订单状态一致性?
首先,支付服务会在用户支付成功后通过MQ消息通知交易服务,完成订单状态的同步。
其次,为例保证MQ消息的可靠性,需要采用了生产者确认机制,消费者确认,消费者失败重试等策略,确保消息投递和处理的可靠性。同时还开启了MQ的持久化机制,防止因服务器宕机导致消息丢失。
最后,在交易服务更新订单状态时做了业务幂等判断,避免因消息重复导致订单状态异常
如果交易服务消息处理失败,有没有声明兜底方案?
延迟消息
发送者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息
延迟任务:设置在一定时间之后才执行的任务
死信交换机
当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):
-
消费者使用
basic.reject
或basic.nack
声明消费失败,并且消息的requeue
参数设置为false -
消息是一个过期消息,超时无人消费
-
要投递的队列消息满了,无法投递
如果一个队列中的消息已经成为死信,并且这个队列通过dead-letter-exchange
属性指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机就称为死信交换机(Dead Letter Exchange)。而此时加入有队列与死信交换机绑定,则最终死信就会被投递到这个队列中。
延迟消息插件
这个插件可以将普通交换机改造为支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列
DelayExchange插件
基于死信队列虽然可以实现延迟消息,但是太麻烦了。因此RabbitMQ社区提供了一个延迟消息插件来实现相同的效果。
官方文档说明:Scheduling Messages with RabbitMQ | RabbitMQ
部署步骤:、
1.放到RabbitMQ的插件目录对应的数据卷。
2.安装插件
docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange
使用:
1.exchange = @Exchange(name = "delay.direct", delayed = "true"),
2.return ExchangeBuilder .directExchange("delay.direct") // 指定交换机类型和名称
.delayed() // 设置delay的属性为true
.durable(true) // 持久化
.build();
取消超时订单
当用户下单完成后,发送15分钟延迟消息,在15分钟后接收消息,检查支付状态:
已支付:更新订单状态为已支付
未支付:更新订单状态为关闭订单,恢复商品库存
Elasticsearch
高性能分布式搜索引擎
正向索引:基于文档id创建索引。根据id查询快,但是查询词条时必须先找到文档,而后判断是否包含词条
倒排索引:对文档内容分词,对词条创建索引,并记录词条所在文档id。查询时先根据词条查询到文档id,而后根据文档id查询文档
文档:每条数据就是一个文档
词条:文档按照语义分成的词语
部署Elasticsearch出现问题:
(在Linux中,当我们使用rm在linux上删除了大文件,但是如果有进程打开了这个大文件,却没有关闭这个文件的句柄,那么linux内核还是不会释放这个文件的磁盘空间,最后造成磁盘空间占用100%,整个系统无法正常运行。这种情况下,通过df和du命令查找的磁盘空间)
IK分词器
中文分词往往需要根据语义分析,比较复杂,这就需要用到中文分词器,例如IK分词器。Ik分词器采用正向迭代最细粒度切分算法
IK分词器有两种模式
1.ik_smart: 智能切分,粗粒度
2.ik_max_word: 最细切分,细粒度ik分词器
通过config目录的IkAnalyzer.cfg.xml文件添加拓展词典
在词典中添加拓展词条
基础概念:Elasticsearch中的文档数据会被序列化为json格式后存储在Elasticsearch中
Mapping映射属性
Mapping是对索引库中文档的约束,常见的Mapping属性包括:
-
type
:字段数据类型,常见的简单类型有:-
字符串:
text
(可分词的文本)、keyword
(精确值,例如:品牌、国家、ip地址) -
数值:
long
、integer
、short
、byte
、double
、float
、 -
布尔:
boolean
-
日期:
date
-
对象:
object
-
-
index
:是否创建索引,默认为true
-
analyzer
:使用哪种分词器 -
properties
:该字段的子字段
索引库操作
Elasticsaearch提供的所有API都是Restful的接口,遵循Restful的基本规范
字段名 | 字段类型 | 类型说明 | 是否 参与搜索 | 是否 参与分词 | 分词器 | |
---|---|---|---|---|---|---|
age |
| 整数 | —— | |||
weight |
| 浮点数 | —— | |||
isMarried |
| 布尔 | —— | |||
info |
| 字符串,但需要分词 | IK | |||
|
| 字符串,但是不分词 | —— | |||
score |
| 只看数组中元素类型 | —— | |||
name | firstName |
| 字符串,但是不分词 | —— | ||
lastName |
| 字符串,但是不分词 | —— |
创建索引库和映射
基本语法:
-
请求方式:
PUT
-
请求路径:
/索引库名
,可以自定义 -
请求参数:
mapping
映射
格式:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
索引库和mapping一旦创建无法修改,但是可以添加新的字段
查询索引库
基本语法:
-
请求方式:GET
-
请求路径:/索引库名
-
请求参数:无
格式:
GET /索引库名
示例:
GET /heima
修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段,或者更新索引库的基础属性。
语法说明:
PUT /索引库名/_mapping { "properties": { "新字段名":{ "type": "integer" } } }
删除索引库
语法:
-
请求方式:DELETE
-
请求路径:/索引库名
-
请求参数:无
格式:
DELETE /索引库名
示例:
DELETE /heima
全量修改
全量修改是覆盖原来的文档,其本质是两步操作:
-
根据指定的id删除文档
-
新增一个相同id的文档
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法:
PUT /{索引库名}/_doc/文档id { "字段1": "值1", "字段2": "值2", // ... 略 }
局部修改
局部修改是只修改指定id匹配的文档中的部分字段。
语法:
POST /{索引库名}/_update/文档id { "doc": { "字段名": "新的值", } }
批处理
批处理采用POST请求,基本语法如下:
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } } { "field1" : "value1" } { "delete" : { "_index" : "test", "_id" : "2" } } { "create" : { "_index" : "test", "_id" : "3" } } { "field1" : "value3" } { "update" : {"_id" : "1", "_index" : "test"} } { "doc" : {"field2" : "value2"} }
其中:
-
index
代表新增操作-
_index
:指定索引库名 -
_id
指定要操作的文档id -
{ "field1" : "value1" }
:则是要新增的文档内容
-
-
delete
代表删除操作-
_index
:指定索引库名 -
_id
指定要操作的文档id
-
-
update
代表更新操作-
_index
:指定索引库名 -
_id
指定要操作的文档id -
{ "doc" : {"field2" : "value2"} }
:要更新的文档字段
-
JavaRestClient
客户端初始化
1.引入es的RestHighLevelClient依赖
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>
2.因为SpringBoot默认的ES版本是7.17.0,所以我们需要覆盖默认的ES版本:
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>
3.初始化RestHighLevelClient:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.150.101:9200") ));
商品表Mapping映射
索引库操作
创建索引库的JavaAPI与Restful接口API对比
-
1)创建Request对象。
-
因为是创建索引库的操作,因此Request是
CreateIndexRequest
。
-
-
2)添加请求参数
-
其实就是Json格式的Mapping映射参数。因为json字符串很长,这里是定义了静态字符串常量
MAPPING_TEMPLATE
,让代码看起来更加优雅。
-
-
3)发送请求
-
client.
indices
()
方法的返回值是IndicesClient
类型,封装了所有与索引库操作有关的方法。例如创建索引、删除索引、判断索引是否存在等
-
文档操作
索引库操作的API非常类似,同样是三步走:
-
1)创建Request对象,这里是
IndexRequest
,因为添加文档就是创建倒排索引的过程 -
2)准备请求参数,本例中就是Json文档
-
3)发送请求
变化的地方在于,这里直接使用client.xxx()
的API,不再需要client.indices()
了。
批处理
DSL查询
Elasticsearch的查询可以分为两大类:
-
叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用。
-
复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。
GET /{索引库名}/_search
{
"query": {
"查询类型": {
// .. 查询条件
}
}
}
-
全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:
-
match
: -
multi_match
-
-
精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找keyword、数值、日期、boolean类型的字段。例如:
-
ids
-
term
-
range
-
-
地理坐标查询:用于搜索地理位置,搜索方式很多,例如:
-
geo_bounding_box
:按矩形搜索 -
geo_distance
:按点和半径搜索
-
复合查询
复合查询大致可以分为两类:
-
第一类:基于逻辑运算组合叶子查询,实现组合条件,例如
-
bool
-
-
第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名。例如:
-
function_score
-
dis_max
-
bool查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool查询支持的逻辑运算有:
-
must:必须匹配每个子查询,类似“与”
-
should:选择性匹配子查询,类似“或”
-
must_not:必须不匹配,不参与算分,类似“非”
-
filter:必须匹配,不参与算分
function score 查询中包含四部分内容:
-
原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
-
过滤条件:filter部分,符合该条件的文档才会重新算分
-
算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
-
weight:函数结果是常量
-
field_value_factor:以文档中的某个字段值作为函数结果
-
random_score:以随机数作为函数结果
-
script_score:自定义算分函数算法
-
-
运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
-
multiply:相乘
-
replace:用function score替换query score
-
其它,例如:sum、avg、max、min
-
排序和分页
elasticsearch默认是根据相关度算分(_score
)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword
类型、数值类型、地理坐标类型、日期类型等。
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"排序字段": {
"order": "排序方式asc和desc"
}
}
]
}
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
基础分页
elasticsearch中通过修改from
、size
参数来控制要返回的分页结果:
-
from
:从第几个文档开始 -
size
:总共查询几个文档
类似于mysql中的limit ?, ?
深度分页
elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。
比如一个索引库中有100000条数据,分别存储到4个分片,每个分片25000条数据。现在每页查询10条,查询第99页。那么分页查询的条件如下:
GET /items/_search
{
"from": 990, // 从第990条开始查询
"size": 10, // 每页查询10条
"sort": [
{
"price": "asc"
}
]
}
针对深度分页,elasticsearch提供了解决方案:
search after
:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
高亮原理
什么是高亮显示呢?
我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示
实现高亮的思路就是:
-
用户输入搜索关键字搜索数据
-
服务端根据搜索关键字到elasticsearch搜索,并给搜索结果中的关键字词条添加
html
标签 -
前端提前给约定好的
html
标签添加CSS
样式
GET /{索引库名}/_search
{
"query": {
"match": {
"搜索字段": "搜索关键字"
}
},
"highlight": {
"fields": {
"高亮字段名称": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
JavaRestClient查询
数据搜索的java代码分为两部分:
1.构建并发起请求
2.解析查询结果
可以通过响应结果,一层一层解析代码
构建查询条件
全文检索的查询条件
单字段
QueryBuilders.matchQuery(“ ”,“ ”);
多字段
QueryBuilders.multimatchQuery()
精确查询的查询条件
QueryBuilders.termQuery(" ", " " );
QueryBuilders.rangeQuery(" ").gte(100).let(150)
布尔查询的查询条件
BoolQueryBuilder boolQuery = (" ", " ");
boolQuery.must(
QueryBuilders.termQuery(" ", " "));
boolQuery.must(
QueryBuilders.rangeQuery(" ", " "));
排序和分页
@Test
void testPageAndSort() throws IOException {
int pageNo = 1, pageSize = 5;
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.搜索条件参数
request.source().query(QueryBuilders.matchQuery("name", ""));
// 2.2.排序参数
request.source().sort("price", SortOrder.ASC);
// 2.3.分页参数
request.source().from((pageNo - 1) * pageSize).size(pageSize);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
parseResponse(response);
}
高亮显示
@Test
void testHighlight() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.query条件
request.source().query(QueryBuilders.matchQuery("name", ""));
// 2.2.高亮条件
request.source().highlighter(
SearchSourceBuilder.highlight()
.field("name")
.preTags("<em>")
.postTags("</em>")
);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
parseResponse(response);
}
数据聚合
聚合常见的有三类:
-
桶(
Bucket
)聚合:用来对文档做分组-
TermAggregation
:按照文档字段值分组,例如按照品牌值分组、按照国家分组 -
Date Histogram
:按照日期阶梯分组,例如一周为一组,或者一月为一组
-
-
度量(
Metric
)聚合:用以计算一些值,比如:最大值、最小值、平均值等-
Avg
:求平均值 -
Max
:求最大值 -
Min
:求最小值 -
Stats
:同时求max
、min
、avg
、sum
等
-
-
管道(
pipeline
)聚合:其它聚合的结果为基础做进一步运算
注意:参加聚合的字段必须是keyword、日期、数值、布尔类型
DSL实现聚合
与之前的搜索功能类似,我们依然先学习DSL的语法,再学习JavaAPI.
Bucket聚合
例如我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category值一样的放在同一组,属于Bucket
聚合中的Term
聚合。
基本语法如下:
GET /items/_search { "size": 0, "aggs": { "category_agg": { "terms": { "field": "category", "size": 20 } } } }
语法说明:
-
size
:设置size
为0,就是每页查0条,则结果中就不包含文档,只包含聚合 -
aggs
:定义聚合-
category_agg
:聚合名称,自定义,但不能重复-
terms
:聚合的类型,按分类聚合,所以用term
-
field
:参与聚合的字段名称 -
size
:希望返回的聚合结果的最大数量
-
-
-
带条件聚合
默认情况下,Bucket聚合是对索引库的所有文档做聚合
Metric聚合
例如stat
聚合,就可以同时获取min
、max
、avg
等结果。
-
stats_meric
:聚合名称-
stats
:聚合类型,stats是metric
聚合的一种-
field
:聚合字段,这里选择price
,统计价格
-
-
Redis
搭建主从集群
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离
主从同步原理
当主从第一次同步连接或断开重连时,从节点都会发送psync请求,尝试数据同步:
大致步骤:
slave发出请求尝试连接master,master判断slave是否是第一次来同步,
是,master把自己所有数据全部发送slave,全量同步
否,master把slave缺少的数据发送slave,增量同步
每当master写数据时,都将命令传播给slave保持实时同步
1.master是怎么判断slave是第一次来同步数据?
每一个master节点都有自己的唯一id,replid, 在尝试psync,携带replid,
然后通过判断replid是否一致
2.master是怎么把自己的所有数据全部发送给slave的?
master会执行bgsave,生成RDB文件,保存在本地,发送RDB文件给slave
3.master是怎么把slave缺少的数据发送给slave的?
当进行第一次主从同步后,master会记录所有读写操作命令,保存在repl_backlog缓存区。
在repl_backlog写入过的数据长度叫offset,存在每个节点中,写操作越多,offset值越大,
主从的offset一致代表数据一致。
每当master写数据时自己的offset增大,将命令传给slave后,slave也执行命令,使offset保持一致
当出现网络问题,master进行写数据,offset不断增大,而slave收不到,offset不变,那么它们之间相差的offset就是slave缺少的命令。
所以在尝试psync,slave不止携带replid,还有offset
4. repl_backlog内存是怎么设计?
repl_backlog的内存不是无限大的,默认1M。存储数据采用环形数组,从0开始,当存满后继续覆盖存储。master执行命令时,slave也会同时追赶,但当出现问题slave进度被master超过一圈后,由于数据被覆盖,需要进行全量同步
5.还应该怎么优化Redis主从集群:
在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO
Redis单结点上的内存占用不要太大,减少RDB导致的过多磁盘IO
适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
限制一个master是的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
哨兵原理
Redis提供了哨兵(Sentine)机制来实现主从集群的自动故障恢复。
故障:Sentinel会不断检查你的master和slave是否按照预期工作
自动故障切换:如果master故障,Sentinel会将一个slave提升喂master。当故障实例恢复后也以新的master为主
通知:当集群发生故障转移时,Sentinel会将最新节点角色信息推法给Redis的客户端
1.哨兵如何去发现集群状态?
Sentinel基于心跳机制检测服务状态,每个1秒向集群的每个实例发送ping命令:
主观下线:如果sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。
quorum值最好超过Sentinel实例数量的一半。
选举新的master
一旦发现master故障,sentinel需要在slave中选择一个作为新的master,选择依据:
首先判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds* 10)则会排除该slave节点
然后判断slave节点的slave-priority(默认为1,需要配置)值,越小优先级越高,如果是0则永不参与选举
如果slave-priority值一样,会判断slave节点的offset值,越大说明数据新,优先级高
最后判断slave节点的运行id大小,越小优先级越高
2.如何实现故障转移
当选中了一个slave为新的master后,故障的转移的步骤:
sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
sentinel给所有其它slave发送(slaveof ip地址 port )命令,让这些slave成为新的master的从节点,开始从新的master上面同步数据
最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
搭建哨兵集群
搭建分片集群
主从和哨兵可以解决高可用性,高并发读的问题。但还有两个问题没有解决:
海量数据存储问题
高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
集群中有多个master,每个master保存不同数据
每个master都可以有多个slave节点
master之间通过ping监测彼此健康状态(不需要哨兵)
散列插槽
在Redis集群中,共有16384个hash slots,集群中的每一个master节点都会分配一定数量的hash slots
Redis如何判断某个key应该在哪个实例?
在Redis分片集群中,有16384个插槽,会把这些插槽分配给集群中的每一个master节点,当根据key进行数据读写时,会根据key做一个hash运算得到一个结果,再对这个结果16384取余,这个值就是key的插槽值,判断插槽在哪个节点上。
Redis数据结构
RedisObject
Redis中的任意数据类型的键和值都会被封装为一个RedisObject。
存在5中基本数据类型:string,hash,list,set,zset,占4个bit位
底层编码方式12种,占4个bit位
lru是记录最后一次访问的时间,占24个bit位,判断空闲时间太久的key
refcount对象引用计数器,为0说明无人引用,可以回收
SkipList
跳表是链表,但和传统链表有差异:
元素按照升序排列存储
节点可能包含多个指针,指针跨度不同
特点:
跳表是一个有序的双向链表
每个节点都可以包含多层指针,层数是1到32之间的随机数
不同层指针到下一个节点的跨度不同,层级越高,跨度越大
增删改查效率与红黑树基本一致,实现却更简单。但空间复杂度更高
SortedSet
数据结构特点:
每组数据都包含score和member
member唯一
可根据score排序
SortedSet的底层数据结构是怎么样?
存储member和score,能根据member找score,member具有唯一性,符合哈希表结构,以member为键,以score为value
其次SorteSet能根据score排序,底层还维护了一个跳表
当需要根据member查询score时,就去哈希表中查询
当需要根据score排序查询时,则基于跳表查询
Redis内存回收
过期Key的处理
Rdis提供了expire命令,给key设置TTL
当keyTTL到期后,key就不存在了,内存就释放了
Redis如何知道key是否过期?
Redis本身是键值型数据库,其所有数据都存在一个redisDB结构体中,包含两个哈希表
dict:保存Redis中所有的键值对
expires:保存Redis中所有的设置过期时间的key和到期时间
所以通过expires的key可以查到到期时间
TTL到期是怎么删除的?
Redis并不会实时监控key的过期时间,通过两种延迟删除的策略:
惰性删除:当有命令需要操作一个key的时候,检查该key的存活时间,如果已经过期才执行删除
周期删除:通过一个定时任务,周期性的抽取部分TTL的key,如果过期就删除
执行周期有两种:
SLOW模式:Redis会设置一个定时任务serverCron()
,按照server.hz
的频率来执行过期key清理
FAST模式:Redis的每个事件循环前执行过期key清理(事件循环就是NIO事件处理的循环)。
内存淘汰策略
内存淘汰:当Redis内存使用达到设置的阈值时,Redis主动挑选部分key删除以释放更多内存。
Redis会在每次处理客户端命令时都会对内存使用情况做判断,如果必要执行内存淘汰。
Redis支持8种不同的内存淘汰策略:
noeviction
: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
volatile
-ttl
: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
allkeys
-random
:对全体key ,随机进行淘汰。也就是直接从db->dict中随机挑选
volatile-random
:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。
allkeys-lru
: 对全体key,基于LRU算法进行淘汰
volatile-lru
: 对设置了TTL的key,基于LRU算法进行淘汰
allkeys-lfu
: 对全体key,基于LFU算法进行淘汰
volatile-lfu
: 对设置了TTL的key,基于LFI算法进行淘汰
Redis缓存
缓存一致性
Cache Aside Pattern 在更新数据库的同时更新缓存
并发安全问题怎么解决?
需要先修改数据库,再删除Redis数据
而不是先删除Redis数据,再修改数据库
如果同时有一个查询和修改命令并行,先删除Redis数据,若数据库数据还未完成修改,查询命令只能查到Redis为空,然后去查询数据库,查到的可能是旧数据。
但先修改数据库,再删除Redis数据也有可能出现并发问题,
如果同时有一个查询和修改命令并行,查询Redis可能缓存过期未命中,需要查询数据库,然后需要缓存数据到Redis,此时修改命令执行数据库数据被修改,Redis数据被删除,查询命令把旧数据缓存Redis(发生概率很小,需要在查询命令缓存数据时,修改命令同时完成了数据库修改和删除缓存数据)
1.低一致性需求:使用Redis的key过期清理方案
2.高一致性需求:主动更新,并以超时剔除作为兜底方案
读操作: 缓存命令直接返回,未命中查询数据库,写入缓存,设定超时时间
写操作:先修改数据库,在删除缓存,保持原子性
缓存穿透
客户端请求的数据在数据库中不存在,导致请求穿透缓存,直接打到数据库
解决方案:
1.缓存空对象:在写入缓存时,把缓存设置为null,并设置TTL短一些
实现简单维护方便,有额外的内存消耗
2.布隆过滤
一种数据统计的算法,用于检索一个元素是否存在一个集合中。但是布隆过滤器无需存储元素到集合,而是把元素映射到一个很长的二进制数位上
首先需要一个很长的二进制数,默认每一位都是0
然后需要N个不同算法的哈希函数
将集合中的元素根据N个哈希函数做运算,得到N个数字,然后将每个数字对应的bit位标记为1
要判断某个元素是否存在,只需要把元素按照上述方式运算,判断对应的bit位是否是1
内存占用少,没有多余key
实现复杂,存在误判可能
缓存雪崩
在同一时间段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案:
给不同的key的TTL添加随机值
利用Redis集群提高服务可用性
给缓存业务添加降级限流策略,可以拒绝一些请求,或熔断
给业务添加多级缓存
缓存击穿
热点key问题,一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
互斥锁:避免无数线程进行缓存重建,只有互斥锁成功的才可以重建,但线程需要等待,性能受影响,可能会死锁
逻辑过期:线程无需等待,返回旧数据直到数据缓存重建完成,不保证一致性,有额外内存消耗,实现复杂
微服务原理
分布式事务
CAP和BASE
CAP:
C一致性:用户访问分布式系统中的任意节点,得到的数据必须一致
A可用性:用户访问分布式系统时,读或写操作总能成功。
P分区:因为网络故障或者其他原因导致分布式系统中的部分节点与其他节点失去连接,形成独立分区。
容错:系统要能容忍网络分区现象,出现分区时,整个系统也要持续对外提高服务
如果此时只允许读不允许写,满足节点一致性,牺牲可用性符合CP
如果此时允许任意读写,满足可用性,但无法数据同步,不满足一致性,符合AP
BASE:
BASE是对CAP的一种解决思路,包含三个思想:
基本可用:分布式系统在出现故障时,允许损失部分可用性,保证核心可用
软状态:在一定时间内允许出现中间状态,比如临时状态不一致
最终一致性:虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致
AT模式的脏写问题
AT模式原理:执行一个更新命令,首先需要注册分支事务,再记录数据快照,执行业务sql
提交,报告事务状态(一阶段),之后判断是否可以提交,可以提交就,删除数据快照。不可以就读取快照,恢复之前数据(二阶段)
事务1一阶段执行成功,释放DB锁,事务2也执行成功,修改了数据,这时事务1二阶段回滚数据,出现脏写。
解决此问题需要引入全局锁在事务1执行sql后获取全局锁,然后释放DB锁,事务2才可以获取DB锁,到事务2执行完sql也要获取全局锁,但事务1占用了,事务2重试获取全局锁,直到获取失败,就会回滚业务,释放DB锁。事务1执行二阶段需要获取DB锁,但等待时间大于事务2重试时间,总能得到DB锁执行二阶段。
AT和XA模式区别在,两种锁的力度不同,DB锁力度要大些,它不释放,任何人都不会获取到,
而全局锁只是记录当前正在操作某行数据的业务
TCC模式
TCC模式和AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码实现数据恢复。需要实现三个方法:
Try:资源的检测和预留
Confirm:完成资源操作业务,要求Try成功Confirm一定要能成功。
Cancel:预留资源释放,可以理解位try的反向操作
最大努力通知
通过消息通知的方式来通知事务参与者完成业务执行,如果执行失败会多次通知。无需任何分布式组件介入
注册中心
环境隔离
企业实际开发中,往往会搭建多个运行环境,例如:
开发环境
测试环境
预发布环境
生产环境
这些不同环境之间的服务和数据之间需要隔离。
还有的企业中,会开发多个项目,共享nacos集群。此时,这些项目之间也需要把服务和数据隔离。
因此,Nacos提供了基于namespace
的环境隔离功能。
bootstrap.yml文件,添加服务发现配置,指定其namespace
分级模型
大厂的服务可能部署在多个不同的机房,物理上被隔离为多个集群。Nacos支持对这种集群的划分
服务--集群---实例
nacos注册表结构是嵌套Map
Map<String(Namespace环境隔离),
Map<String(Group服务分组),Service(
Map<String(集群),Cluster
(Set<Instance>)>)>>
Eureka与Nacos
Eureka是Netflix公司开源的一个服务注册中心组件,早期版本的SpringCloud都是使用Eureka作为注册中心。由于Eureka和Nacos的starter中提供的功能都是基于SpringCloudCommon规范,因此两者使用起来差别不大。
引入相应依赖,更改访问地址就可以使用了
Eureka和Nacos共同点:
都有服务注册和服务拉取,提供心跳健康检测
不同点:
1.Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
2.临时实例心跳不正常会被剔除,非临时实例则不会被剔除(很少用)
3.Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
4.Nacos集群默认采用AP方式(可用性重要),但也支持CP;Eureka采用AP
远程调用
负载均衡整体流程:
加入服务接口,根据服务名获取服务实例列表,写负载均衡,从实例列表挑选一个实例,
利用ResTemplate发起http请求
服务保护
线程隔离
两种实现方式:
线程池隔离(Hystix默认采用):创建线程池限制线程数量
信号量隔离(Sentinel):通过计数器计数方式限制线程数量
滑动窗口算法
固定窗口计数器算法:
将时间划分为多个窗口,窗口时间跨度称为Interval
每个窗口分别计数统计,每有一次请求就将计数器加一,通过设置计数器阈值限流
如果计数器超过限流阈值,超出的请求丢弃
但极端情况下会出现问题比如:
因为时间是连续的,以1000ms为一个时间跨度,0到500ms没有请求,500ms到1000ms有2个请求,1000ms到1500ms也有2个请求,会以500ms到1500ms为一个跨度,如果这时超限,超出的请求就会丢弃,导致请求失败。
要解决这个问题需要将一个窗口划分为更小的区间,每个小区间时间跨度变小,都有计数器(通过提高精度,提高准确度)
漏桶算法
将每个请求视作“水滴”放入“漏桶”进行存储
“漏桶"以固定速率向外”漏“出请求来执行,如果”漏桶“空了则停止”漏水“
令牌桶算法
以固定的速率生成令牌,存入令牌桶中,如果令牌桶满了以后,停止生成
请求进入后,必须先尝试从桶中获取令牌,获取到令牌后才可以被处理
如果令牌桶中吗,没有令牌,则请求等待或丢弃
存在问题:如果请求忽高忽低,没有请求的时候桶中已经生成满令牌,假设有大量请求的时候,会用完桶中的令牌并再生成和刚才数量相同的令牌,也就是说在瞬间处理了2倍的请求,超出许可上限了。因此桶容量和生成的令牌应该是服务所能处理的一半,但有浪费。
Sentinel的限流与Gateway的限流有说明差别?
限流算法常见有三种:滑动时间窗口,令牌桶算法,漏桶算法。Gateway则采用基于Redis实现的令牌桶算法,而Sentinel内部却比较复杂:
默认限流模式是基于滑动时间窗口算法,另外Sentinel中断器的计数也是基于滑动时间窗口算法
限流后可以快速失败和排队等待,其中排队等待基于漏桶算法
而热点参数限流则是基于令牌桶算法