微服务
微服务
Ⅰ实用
一 微服务治理
1.1 认识微服务
1.1.1 服务架构演变
分布式架构需要考虑的问题
- 服务粒度
- 服务集群地址如何维护
- 服务之间如何远程调用
- 服务健康如何感知
微服务
微服务是一种经过良好架构设计的分布式架构方案
微服务架构特征:
单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
面向服务:微服务对外暴露业务接口
自治:团队独立、技术独立、数据独立、部署独立
隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
1.1.2 微服务技术对比
- 微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是
SpringCloud
和阿里巴巴的Dubbo
。
- 微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是
1.1.3 SpringCloud
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
本文档
Hoxton.SR10
、使用2.3.x版本的SpringBoot
1.2 微服务拆分调用
1.2.1 服务拆分
- 注意事项
- 单一职责:不同微服务,不要重复开发相同业务
- 数据独立:不要访问其它微服务的数据库
- 面向服务:将自己的业务暴露为接口,供其它微服务调用
- 注意事项
1.2.2 服务调用
本质:服务A发送http请求到服务B,获取返回数据
注册RestTemplate
服务远程调用RestTemplate
注意
- 基于RestTemplate发起的http请求实现远程调用
- http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可。
1.2.3 提供者与消费者
- 服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
- 服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
1.3 eureka注册中心
1.3.1 远程调用的问题
- 服务消费者该如何获取服务提供者的地址信息?
- 服务提供者启动时向eureka注册自己的信息
- eureka保存这些信息
- 消费者根据服务名称向eureka拉取提供者信息
- 如果有多个服务提供者,消费者该如何选择?
- 服务消费者利用负载均衡算法,从服务列表中挑选一个
- 消费者如何得知服务提供者的健康状态?
- 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
- eureka会更新记录服务列表信息,心跳不正常会被剔除
- 消费者就可以拉取到最新的信息
- 服务消费者该如何获取服务提供者的地址信息?
1.3.2 eureka原理
1.3.3 搭建EurekaServer
无论是消费者还是提供者都可以注册Eureka服务
导入
spring-cloud-starter-netflix-eureka-server
坐标编写启动类,添加@EnableEurekaServer注解
添加application.yml文件,编写配置:
1.3.4 服务拉取
服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡
步骤
修改OrderService的代码,修改访问的url路径,用服务名代替ip、端口:
在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解:
1.4 Ribbon负载均衡原理
1.4.1 负载均衡原理
请求流程
负载均衡流程
1.4.2 负载均衡策略
通过定义
IRule
实现可以修改负载均衡规则,有两种方式:代码方式:在order-service中的OrderApplication类中,定义一个新的IRule:
配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:
1.4.3 懒加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
1.5 nacos注册中心
1.5.1 简介
- Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。
1.5.2 入门
解压即可
conf目录下有一个配置文件
application.properties
,默认端口8848bin目录为可执行文件,启动命令windows
1
startup.cmd -m standalone
默认账号和密码: nacos
服务注册到nacos
在cloud-demo父工程中添加
spring-cloud-alilbaba
的管理依赖:注释掉order-service和user-service中原有的eureka依赖
添加nacos的客户端依赖:
修改user-service&order-service中的application.yml文件,注释eureka地址,添加nacos地址:
启动并测试
1.5.3 Nacos服务分级存储模型
问题
- 服务调用尽可能选择本地集群的服务,跨集群调用延迟较高
- 本地集群不可访问时,再去访问其它集群
服务集群属性
- 集群属性
负载均衡策略
根据权重负载均衡
- 在Nacos控制台可以设置实例的权重值,首先选中实例后面的编辑按钮
- 将权重设置为0.1,测试可以发现8081被访问到的频率大大降低
1.5.4 Nacos环境隔离
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
- 不同环境之间的服务不能相互访问
步骤
在Nacos控制台可以创建namespace,用来隔离不同环境
然后填写一个新的命名空间信息
保存后会在控制台看到这个命名空间的id
修改order-service的application.yml,添加namespace
重启order-service后,再来查看控制台
此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错
1.5.5 nacos和eureka对比
心跳检测失败对于非临时实例nacos不会直接挂掉它
临时实例和非临时实例设置
其他对比
- 相同点
- 都支持服务注册和服务拉取
- 都支持服务提供者心跳方式做健康检测
- 不同点
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
- 相同点
1.6 Nacos配置管理
1.6.1 统一配置管理
配置更改热更新
步骤
在Nacos中添加配置信息
- 配置文件id:
[服务名称]-[profile].[后缀]
如:userservice-dev.yaml
- 格式支持
yaml
properties
- 配置文件id:
在弹出表单中填写配置信息
配置获取的步骤如下
在微服务中读取统一配置
引入Nacos的配置管理客户端依赖:
在userservice中的resource目录添加一个
bootstrap.yml
文件,这个文件是引导文件,优先级高于application.yml
注意:
- 不是所有的配置都适合放到配置中心,维护起来比较麻烦
- 建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置
1.6.2 配置热更新
- 方式一:在@Value注入的变量所在类上添加注解@RefreshScope
- 方式二:使用@ConfigurationProperties注解
1.6.3 配置共享
微服务启动时会从nacos读取多个配置文件:
[spring.application.name]-[spring.profiles.active].yaml
,例如:userservice-dev.yaml
[spring.application.name].yaml
,例如:userservice.yaml
无论profile如何变化,
[spring.application.name].yaml
这个文件一定会加载,因此多环境共享配置可以写入这个文件多种配置的优先级
1.6.4 搭建Nacos集群
- 步骤
- 搭建MySQL集群并初始化数据库表
- 下载解压nacos
- 修改集群配置(节点信息)、数据库
- 分别启动多个nacos节点
- nginx反向代理
- 步骤
1.7 Feign
1.7.1 RestTemplate方式调用存在的问题
- 问题
- 可读性差
- 参数复杂URL难以维护
- 问题
1.7.2 Feign简介
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
使用步骤
引入依赖
在order-service的启动类添加注解开启Feign的功能
@EnableFeignClients
编写Feign客户端
用Feign客户端代替RestTemplate
1.7.3 自定义Feign配置
- 两种方式修改,一个是修改配置文件,另一种是声明Bean
1.7.4 Feign性能优化
Feign底层
URLConnection
:默认实现,不支持连接池Apache HttpClient
:支持连接池OKHttp
:支持连接池
因此优化Feign的性能主要包括:
- 使用连接池代替默认的
URLConnection
- 日志级别,最好用
basic
或none
- 使用连接池代替默认的
性能优化-连接池配置
引入依赖
- feign-httpclient
配置连接池
1.7.5 Feign最佳实践
方式一:继承,给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。
- 缺点是会造成紧耦合,父接口参数列表中的映射不会被继承
方式二:抽取,将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
缺点是需要写所有的东西,比如order-service引入提取出来的公共模块 feign-api 模块时,也有可能把不需要使用的远程调用方法引入进来了
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种方式解决:
- 方式一:指定FeignClient所在包
- 方式二:指定FeignClient字节码
1.8 统一网关Gateway
1.8.1 为什么需要
在SpringCloud中网关的实现包括两种:
- gateway
- zuul
Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
1.8.2 入门
创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖:
编写路由配置及nacos地址
路由配置
- 路由id:路由的唯一标示
- 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
- 路由断言(predicates):判断路由的规则
- 路由过滤器(filters):对请求或响应做处理
1.8.3 断言工厂
Route Predicate Factory
- 路由id
- uri
- predicates
- filters
在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
例如
Path=/user/**
是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的像这样的断言工厂在SpringCloudGateway还有十几个
Spring提供的11种基本的Predicate工厂
1.8.4 过滤器工厂GatewayFilter
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
Spring提供了31种不同的路由过滤器工厂.
默认过滤器
如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:
1.8.5 全局过滤器GlobalFilter
区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现。
定义方式是实现
GlobalFilter
接口。demo案例:定义全局过滤器,拦截并判断用户身份
自定义类,实现GlobalFilter接口,添加@Order注解:
1.8.6 过滤器执行顺序
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
执行顺序
每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
1.8.7 跨域问题
跨域:域名不一致就是跨域,主要包括:
- 域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
- 域名相同,端口不同:localhost:8080和localhost8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS
网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:
二 Docker
2.1 简介
Docker如何解决依赖的兼容问题的?
- 将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包
- 将每个应用放到一个隔离容器去运行,避免互相干扰
Docker如何解决不同系统环境的问题?
- Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包
- Docker运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行
Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
- Docker允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像
- Docker应用运行在容器中,使用沙箱机制,相互隔离
- Docker如何解决开发、测试、生产环境有差异的问题
- Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行
2.1.1 Docker与虚拟机
虚拟机(virtual machine)是在操作系统中模拟硬件设备,然后运行另一个操作系统,比如在 Windows 系统里面运行 Ubuntu 系统,这样就可以运行任意的Ubuntu应用了。
差异
- docker是一个系统进程;虚拟机是在操作系统中的操作系统
- docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般
2.1.2 Docker架构
镜像(image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。
DockerHub:DockerHub是一个Docker镜像的托管平台。这样的平台称为Docker Registry。
- 类似的还有阿里云镜像服务,统称为DockerRegistry
docker架构
- CS架构
服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
- 注:一般使用docker就是发送指令获取到镜像
2.2 安装
如果之前安装过旧版本的Docker,可以使用下面命令卸载:
1
2
3
4
5
6
7
8
9
10
11yum remove docker /
docker-client /
docker-client-latest /
docker-common /
docker-latest /
docker-latest-logrotate /
docker-logrotate /
docker-selinux /
docker-engine-selinux /
docker-engine /
docker-ce虚拟机需要联网,安装yum工具
1
2
3yum install -y yum-utils /
device-mapper-persistent-data /
lvm2 --skip-broken然后更新本地镜像源:
1
2
3
4
5
6
7
8设置docker镜像源
yum-config-manager /
--add-repo /
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sed -i 's/download.docker.com/mirrors.aliyun.com//docker-ce/g' /etc/yum.repos.d/docker-ce.repo
yum makecache fast然后输入命令:
1
yum install -y docker-ce
docker-ce为社区免费版本。稍等片刻,docker即可安装成功。
启动前关闭防火墙,因为Docker应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦
1
2
3
4# 关闭
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld通过命令启动docker:
1
2
3
4
5systemctl start docker # 启动docker服务
systemctl stop docker # 停止docker服务
systemctl restart docker # 重启docker服务然后输入命令,可以查看docker版本:
1
docker -v
查看状态
1
systemctl status docker
配置镜像加速
2.3 一般操作
镜像相关命令
- 镜像名称一般分两部分组成:
[repository]:[tag]
。- eg:
mysql:5.7
- eg:
- 在没有指定tag时,默认是latest,代表最新版本的镜像
- 镜像名称一般分两部分组成:
镜像操作
容器操作
这里的
docker rm
删除的是硬盘中的容器对象docker run
的常见参数--name
:指定容器名称-p
:指定端口-d
:后台运行
- 外部访问宿主机的80端口(前面的)的时候就会映射到内部指定的80端口(后面的 ),从而访问到nginx
数据卷
容器与数据耦合的问题
- 修改不方便:需要进入容器内部修改
- 数据不可复用:在容器内的修改对外不可见
- 升级困难:升级必然会删除旧容器
数据卷:一个虚拟目录,指向宿主机文件系统中的某个目录
数据卷操作
docker volume [COMMAND]
- create 创建一个volume
- inspect 显示一个或多个volume信息
- ls 列出所有volume
- prune 删除未使用的volume
- rm 删除一个或多个指定的volume
将数据卷挂载到容器中
-v volumeName:/targetContainerPath
- 如果容器运行时vlolume不存在,会被自动创建出来
练习
2.4 自定义镜像
镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。
镜像结构
- 基础镜像BaseImage:应用依赖的系统函数库、环境、配置、文件等
- 层Layer:在BaseImage基础上添加安装包、依赖、配置等,每次操作都形成新的一层
- 为了节省升级成本
- 入口Entrypoint:镜像运行入口,一般是程序启动的脚本和参数
Dockerfile:就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer
2.5 DockerCompose
Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器
Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行(对比上面黑底白字的指令)
2.6 镜像仓库
公共仓库:例如Docker官方的 Docker Hub,国内也有一些云服务商提供类似于 Docker Hub 的公开服务,比如 网易云镜像服务、DaoCloud 镜像服务、阿里云镜像服务等。
除了使用公开仓库外,用户还可以在本地搭建私有 Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现。
简化版
1
2
3
4
5
6docker run -d /
--restart=always /
--name registry /
-p 5000:5000 /
-v registry-data:/var/lib/registry /
registry命令中挂载了一个数据卷registry-data到容器内的/var/lib/registry 目录,这是私有镜像库存放数据的目录。
访问http://YourIp:5000/v2/_catalog 可以查看当前私有镜像服务中包含的镜像
带有图形化界面版本(新建一个docker-compose.yml文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15version: '3.0'
services:
registry:
image: registry
volumes:
- ./registry-data:/var/lib/registry
ui:
image: joxit/docker-registry-ui:static
ports:
- 8080:80
environment:
- REGISTRY_TITLE=传智教育私有仓库
- REGISTRY_URL=http://registry:5000
depends_on:
- registry- 执行命令
docker-compose up -d
- 执行命令
配置docker信任地址
我们的私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:
1
2
3
4
5
6
7
8# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:
"insecure-registries":["http://192.168.150.101:8080"]
# 重加载
systemctl daemon-reload
# 重启docker
systemctl restart docker
三 异步通信
3.1 MQ
3.1.1 简介
- 同步通讯和异步通讯区别
- 视频和文字聊天的区别
- 微服务之间的基于Feign的调用就属于同步方式,存在一些问题
- 耦合度高,每次加入新的需求,都要修改原来的代码
- 性能下降,调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和。
- 资源浪费,调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
- 级联失败,如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障
- 同步调用的优点是时效性强,可以立即得到结果
3.1.2 异步通讯
异步调用常见实现就是事件驱动模式
优点
- 服务解耦
- 性能提升,吞吐量提高
- 服务没有强依赖,不担心级联失败问题
- 流量削峰
缺点
- 依赖于Broker的可靠性、安全性、吞吐能力
- 架构复杂了,业务没有明显的流程线,不好追踪管理
3.1.3 MQ常见框架
MQ (MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
3.2 RabbitMQ
3.2.1 简介
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:https://www.rabbitmq.com/
相关概念
- channel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
3.2.2 常见消息模型
基本消息队列(BasicQueue)
工作消息队列(WorkQueue)
发布订阅(Publish、Subscribe),又根据交换机类型不同分为三种:
- Fanout Exchange:广播
- Direct Exchange:路由
- Topic Exchange:主题
3.2.3 demo
基本消息队列的消息发送流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.利用channel向队列发送消息
基本消息队列的消息接收流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.定义consumer的消费行为
handleDelivery()
5.利用channel将消费者与队列绑定
3.3 SpringAMQP
- Advanced Message Queuing Protocol,是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
- SpringAMQP是基于AMQP协议定义的一套API规范,提供了模板来接收和发送消息。包含两部分,其中spring-smqp是基础抽象,spring-rabbit是底层的默认实现
- springAMQP如何发送消息
- 引入amqp的starter依赖
- 配置RabbitMQ地址
- 利用RabbitTemplate的
convertAndSend
方法
3.3.1 简单队列 BasicQueue
1对1
引入依赖
编写publisher,向simple.queue发送消息
编写consumer,监听simple.queue
3.3.2 工作队列 WorkQueue
一个publisher对多个consumer
- 可以提高消息处理速度,避免队列消息堆积
操作过程同上,只不过consumer中多了几个
@RabbitListener
注解的方法3.3.3 发布、订阅模型
交换机的作用
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- FanoutExchange的会将消息路由到每个绑定的队列
声明队列、交换机、绑定关系的Bean
Queue
FanoutExchange
Binding
Fanout
所有的队列都能接收
demo
- 在consumer服务中,利用代码声明队列、交换机,并将两者绑定
- 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
- 在publisher中编写测试方法,向itcast.fanout发送消息
Direct
Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)。
- 每一个Queue都与Exchange设置一个BindingKey
- 发布者发送消息时,指定消息的RoutingKey
- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
- 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
demo
在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
并利用@RabbitListener声明Exchange、Queue、RoutingKey(基于注解声明)
@Queue
@Exchange
Topic
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以 . 分割。
Queue与Exchange指定BindingKey时可以使用通配符:
:代指0个或多个单词
*:代指一个单词
demo
在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2,
并利用@RabbitListener声明Exchange、Queue、RoutingKey
测试
<img src=(https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/微服务.assets/image-20240103111413444.png“ alt=”image-20240103111413444” style=”zoom:200%;” />
3.3.4 消息转换器
说明:在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送。
Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化
注意发送方与接收方必须使用相同的MessageConverter
JSON方式序列化demo
publisher服务
引入依赖
声明MessageConverter
consumer服务
引入依赖
同上
定义MessageConverter
同上
定义一个消费者,监听object.queue队列并消费消息:
四 分布式搜索引擎-ES基础
4.1 elasticsearch
4.1.1 ES
elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域。
elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。
发展
- Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:https://lucene.apache.org/ 。
- 优势:易扩展、高性能(基于倒排索引)
- 缺点:只限于java、学习曲线陡峭、不支持水平扩展
- 2004年Shay Banon基于Lucene开发了Compass
- 2010年Shay Banon 重写了Compass,取名为Elasticsearch。
- 相比与lucene,elasticsearch具备优势:
- 支持分布式,可水平扩展
- 提供Restful接口,可被任何语言调用
- 相比与lucene,elasticsearch具备优势:
- 搜索引擎技术排名:
- Elasticsearch:开源的分布式搜索引擎
- Splunk:商业项目
- Solr:Apache的开源搜索引擎
- Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:https://lucene.apache.org/ 。
4.1.2 倒排索引
传统数据库(如MySQL)采用正向索引,例如给下表(tb_goods)中的id创建索引:
elasticsearch采用倒排索引:
- 文档(document):每条数据就是一个文档
- 词条(term):文档按照语义分成的词语
索引
<img src=(https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/微服务.assets/image-20240103113341607.png“ alt=”image-20240103113341607” style=”zoom: 50%;” />
倒排索引中包含两部分内容:
- 词条词典(Term Dictionary):记录所有词条,以及词条与倒排列表(Posting List)之间的关系,会给词条创建索引,提高查询和插入效率
- 倒排列表(Posting List):记录词条所在的文档id、词条出现频率 、词条在文档中的位置等信息
- 文档id:用于快速获取文档
- 词条频率(TF):文档在词条出现的次数,用于评分
4.1.3 es概念
文档:elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。
文档数据会被序列化为json格式后存储在elasticsearch中。
索引(index):相同类型的文档的集合
映射(mapping):索引中文档的字段约束信息,类似表的结构约束
与mysql概念对比
- 架构
- Mysql:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算
- 架构
4.1.4 安装es
创建网络(因为还需要部署kibana容器,因此需要让es和kibana容器互联)
1
docker network create es-net
加载镜像
运行
1
2
3
4
5
6
7
8
9
10
11docker run -d /
--name es /
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" /
-e "discovery.type=single-node" /
-v es-data:/usr/share/elasticsearch/data /
-v es-plugins:/usr/share/elasticsearch/plugins /
--privileged /
--network es-net /
-p 9200:9200 /
-p 9300:9300 /
elasticsearch:7.12.1-e "cluster.name=es-docker-cluster"
:设置集群名称-e "http.host=0.0.0.0"
:监听的地址,可以外网访问-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:内存大小-e "discovery.type=single-node"
:非集群模式-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录--privileged
:授予逻辑卷访问权--network es-net
:加入一个名为es-net的网络中-p 9200:9200
:端口映射配置
在浏览器中输入:http://192.168.71.22:9200 即可看到elasticsearch的响应结果(虚拟机地址)
4.1.5 安装kibana
kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。
运行docker命令,部署kibana
1
2
3
4
5
6docker run -d /
--name kibana /
-e ELASTICSEARCH_HOSTS=http://es:9200 /
--network=es-net /
-p 5601:5601 /
kibana:7.12.1--network es-net
:加入一个名为es-net的网络中,与elasticsearch在同一个网络中-e ELASTICSEARCH_HOSTS=http://es:9200"
:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch-p 5601:5601
:端口映射配置
kibana启动一般比较慢,需要多等待一会,可以通过命令:
1
docker logs -f kibana
此时,在浏览器输入地址访问:http://192.168.71.22:5601,即可看到结果
4.1.6 分词器
es在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好
处理中文分词,一般会使用IK分词器。https://github.com/medcl/elasticsearch-analysis-ik
安装IK分词器
查看elasticsearch的数据卷目录,通过下面命令查看:
1
docker volume inspect es-plugins
显示结果:
1
2
3
4
5
6
7
8
9
10
11[
{
"CreatedAt": "2022-05-06T10:06:34+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
"Name": "es-plugins",
"Options": null,
"Scope": "local"
}
]说明plugins目录被挂载到了:
/var/lib/docker/volumes/es-plugins/_data
这个目录中。
ik分词器包含两种模式:
- ik_smart:最少切分,粗粒度
- ik_max_word:最细切分,细粒度
扩展ik分词器
需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:
然后在名为ext.dic的文件中,添加想要拓展的词语即可:
要禁用某些敏感词条,只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:
然后在名为stopword.dic的文件中,添加想要拓展的词语即可:
4.2 索引库操作(数据列)
4.2.1 mapping映射属性
- mapping是对索引库中文档的约束,常见的mapping属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
- type:字段数据类型,常见的简单类型有:
- 注意:没有数组类型!一个数组的类型就是里面的元素的类型
4.2.2 索引库的CRUD
ES中通过Restful请求操作索引库、文档。请求内容用DSL语句来表示。
创建
1
PUT/索引名
查看
1
GET/索引名
1
GET/heima
删除
1
DELETE/索引名
1
DELETE/heima
修改
索引库和mapping一旦创建无法修改,但是可以添加新的字段,语法如下:
4.3 文档操作(数据行)
新增DSL语法:
查看
删除
修改
4.4 RestAPI
- 本质:组装DSL语句,通过http请求发送给ES。
官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
mapping要考虑的问题:
- 字段名、数据类型、是否参与搜索、是否分词、如果分词,分词器是什么?
demo
引入依赖
覆盖默认的版本(SpringBoot默认的ES版本是7.6.2)
初始化
RestHighLevelClient
创建索引库
删除索引库
判断索引库是否存在
总结索引库基本操作
- 初始化RestHighLevelClient
- 创建XxxIndexRequest。XXX是Create、Get、Delete
- 准备DSL( Create时需要)
- 发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete
操作文档demo
初始化
JavaRestClient
添加数据到索引库
查询
修改
删除
4.5 批量导入数据到ES
- 利用mp查询数据库中所以数据
- 将查询到的数据转换为文档类型数据
- 利用JavaRestClient中的Bulk批量处理,实现批量新增文档
五 分布式搜索引擎-ES功能
5.1 DSL查询文档
5.1.1 DSL查询分类
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
- 查询所有:查询出所有数据,一般测试用。例如:
- match_all
- 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
- match_query
- multi_match_query
- 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如:
- ids
- range
- term
- 地理(geo)查询:根据经纬度查询。例如:
- geo_distance
- geo_bounding_box
- 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
- bool
- function_score
- 查询所有:查询出所有数据,一般测试用。例如:
基本语法
5.1.2 全文检索查询
match查询:全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索
multi_match查询:与match查询类似,只不过允许同时查询多个字段
- 注意:参与查询字段越多,查询性能越差
5.1.3 精确查询
- 精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的
- term:根据词条精确值查询
- range:根据值的范围查询
5.1.4 地理坐标查询
geo_bounding_box:查询geo_point值落在某个矩形范围的所有文档
geo_distance:查询到指定中心点小于某个距离值的所有文档
5.1.5 组合查询
复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑
- 算法查询
- 布尔查询
fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价
- 使用 function score query,可以修改文档的相关性算分(query score),根据新得到的算分排序。
算分demo:让”如家“这个酒店的排名靠前一点
布尔查询 Boolean Query:是一个或多个查询子句的组合。子查询的组合方式有:
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
布尔查询demo
搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
5.2 搜索结果处理
5.2.1排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
5.2.2 分页
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
elasticsearch中通过修改from、size参数来控制要返回的分页结果:
深度分页问题:ES是分布式的,所以会面临深度分页问题。例如按price排序后,获取from = 990,size =10的数据。
首先在每个数据分片上都排序并查询前1000条文档。
然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档
最后从这1000条中,选取从990开始的10条文档
问题:如果搜索页数过深,或者结果集(from + size)越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000
深度分页问题解决方案
- 方案一:search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:只能向后逐页查询,不支持随机翻页
- 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
- 方案二:scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。
- 优点:没有查询上限(单次查询的size不超过10000)
- 缺点:会有额外内存消耗,并且搜索结果是非实时的
- 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。
- 方案一:search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
5.2.3 高亮
就是在搜索结果中把搜索关键字突出显示
原理
- 将搜索结果中的关键字用标签标记出来
- 在页面中给标签添加css样式
5.2.4 搜索结果处理整体语法
<img src=(https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/微服务.assets/image-20240103200025546.png“ alt=”image-20240103200025546” style=”zoom: 50%;” />
5.3 RestClient查询文档及结果处理
5.3.1 demo
查询
- RestAPI中其中构建DSL是通过HighLevelRestClient中的
source()
来实现的,其中包含了查询、排序、分页、高亮 - RestAPI中其中构建查询条件的核心部分是由一个名为
QueryBuilders
的工具类提供的,其中包含了各种查询方法
- RestAPI中其中构建DSL是通过HighLevelRestClient中的
解析结果
5.3.2 match查询
全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件,也就是query的部分。
同样是利用QueryBuilders提供的方法:
5.3.3 精确查询
精确查询常见的有term查询和range查询,同样利用QueryBuilders实现:
5.3.4 复合查询
5.3.5 排序、分页、高亮
排序和分页
高亮
高亮结果解析
5.3.6 总结
- 要构建查询条件,只要记住一个类:QueryBuilders
5.4 黑马旅游案例
5.4.1 关键字搜索功能
- 定义实体类
定义
Controller
,接收页面请求,调用Service
对象的search()
方法在
Service
中的search()
方法中实现查询,利用match
查询实现根据关键字搜索酒店信息
5.4.2 添加品牌、城市、星级、价格等过滤功能
- 修改RequestParams类,添加brand、city、starName、minPrice、maxPrice等参数
- 修改search方法的实现,在关键字搜索时,如果brand等参数存在,对其做过滤
5.4.3 附近的酒店功能
- 前端发送位置,返回一个List
- 修改RequestParams参数,接收location字段
- 修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
5.4.4 让指定的酒店在搜索结果中排名置顶
- 给HotelDoc类添加isAD字段,Boolean类型
- 挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
- 修改search方法,添加function score功能,给isAD值为true的酒店增加权重
六 分布式搜索引擎-ES深入
6.1 数据聚合
6.1.1 聚合的种类
- 聚合(aggregations)可以实现对文档数据的统计、分析、运算。聚合常见的有三类:
- 桶(Bucket)聚合:用来对文档做分组
- TermAggregation:按照文档字段值分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
- 管道(pipeline)聚合:其它聚合的结果为基础做聚合
- 桶(Bucket)聚合:用来对文档做分组
- 参与聚合的字段类型必须是
- keyword
- 数值
- 日期
- 布尔
6.1.2 DSL实现聚合
DSL实现Bucket聚合
要统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合。
类型为term类型,DSL示例:
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。
我们可以修改结果排序方式:
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件即可
- aggs代表聚合,与query同级,此时query的作用是:限定聚合的的文档范围
DSL实现Metrics聚合
例如,我们要求获取每个品牌的用户评分的min、max、avg等值.
我们可以利用stats聚合
6.1.3 Rest API实现聚合
请求组装
结果解析
6.2 自动补全
6.2.1 拼音分词器
- 安装与IK分词器一样
- 下载pinyin分词器
- 解压放到es的plugin目录
- 重启
6.2.2 自定义分词器
elasticsearch中分词器(analyzer)的组成包含三部分:
- character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
- tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
- tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
在创建索引库的时候,通过settings来配置自定义的analyzer(分词器)
自定义分词器步骤
- 创建索引库的时候,在settings中配置,可以包含三部分
- character filter
- tokenizer
- filter
注意事项
- 为避免搜索到同音字,搜索时不要使用拼音分词器
6.2.3 自动补全查询
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
参与补全查询的字段必须是completion类型。
字段的内容一般是用来补全的多个词条形成的数组。
查询语法
6.2.4 实现搜索框自动补全
- 思路
- 修改hotel索引库结构,设置自定义拼音分词器
- 修改索引库的name、all字段,使用自定义分词器
- 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
- 给HotelDoc类添加suggestion字段,内容包含brand、business
- 重新导入数据到hotel库
6.3 数据同步
- elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步。
6.3.1 思路分析
方案一:同步调用
- 简单粗暴。但是业务耦合,影响性能
方案二:异步通知
- 解决了业务耦合问题(推荐),但是依赖于MQ的可靠性
方案三:监听binlog
- 完全解除服务间耦合,但是开启binlog增加数据库负担,实现复杂度高
6.3.2 利用MQ实现ES与数据库的数据同步
- 目标:当酒店数据发生增、删、改时,要求对elasticsearch中数据也要完成相同操作。
- 步骤
- 导入工程完成数据的CRUD
- 声明exchange、queue、RoutingKey
- 配置文件中定义好rabbitmq的配置(host、port、username、password、virtual-host)
- 声明一个配置类,在里面定义交换机(1个)、队列(2个)及他们的绑定关系(2个)的bean
- 在hotel-admin中的增、删、改业务中完成消息发送
- 在service中修改or Controller中
- 调用RabbieTemplate中发送消息,消息内容是对象的id(不发送整个对象,耗内存)
- 在hotel-demo中完成消息监听,并更新elasticsearch中数据
- 在新建一个类,单独完成mq的消息监听
- 在方法上使用
@RabbitListener
定义好监听的队列 - 完成方法内容:根据id查询内容、准备Request、准备DSL、准备发送请求…
- 启动并测试数据同步功能
6.4 集群
6.4.1 搭建ES集群
单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题。
- 海量数据存储问题:将索引库从逻辑上拆分为N个分片(shard),存储到多个节点
- 单点故障问题:将分片数据在不同节点备份(replica)
计划使用3个docker容器模拟3个es节点
集群节点不同的职责划分
6.4.2 集群脑裂问题
- 默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其它候选节点会选举一个成为主节点。当主节点与其他节点网络故障时,可能发生脑裂问题。
- 为了避免脑裂,需要要求选票超过 ( eligible节点数量 + 1 )/ 2 才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes,在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题
6.4.3 集群分布式存储
当新增文档时,应该保存到不同分片,保证数据均衡,那么coordinating node如何确定数据该存储到哪个分片呢?
elasticsearch会通过hash算法来计算文档应该存储到哪个分片:
1
shard = hash(_routing) % number_of_shards
- _routing默认是文档的id
- 算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!
新增文档流程
6.4.4 集群分布式查询
elasticsearch的查询分成两个阶段:
scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户
6.4.5 集群故障转移
集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其它节点,确保数据安全,这个叫做故障转移。
Ⅱ 高级
一 微服务保护
1.1 Sentinel
1.1.1 雪崩问题及解决方案
微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
解决雪崩问题的常见方式
- 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
- 舱壁模式:限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
- 熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
- 流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。
1.1.2 服务保护技术对比
1.1.3 Sentinel简介
Sentinel是阿里巴巴开源的一款微服务流量控制组件。
特点
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
安装sentinel控制台
github下载jar包
运行jar包,访问localhost:8080
默认的账号密码为:sentinel
修改配置项
1
java -jar sentinel-dashboard-1.8.1.jar -Dserver.port=8090
修改端口
1.1.4 微服务整合Sentinel
引入demo工程
在order-service中整合sentinel,并连接控制台
引入sentinel依赖
配置控制台地址
访问微服务的任意端点,触发sentinel监控
1.2 流量控制
簇点链路:就是项目内的调用链路,链路中被监控的每个接口就是一个资源。默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。
流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
1.2.1 demo
- 可以使用jemeter测试:
1.2.2 流控模式
在添加限流规则时,点击高级选项,可以选择三种流控模式:
- 直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式
- 关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
- 比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是有限支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。
- 链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流
Sentinel默认只标记Controller中的方法为资源,如果要标记其它方法,需要利用@SentinelResource注解,示例:
Sentinel默认会将Controller方法做context整合,导致链路模式的流控失效,需要修改application.yml,添加配置:
1.2.3 流控效果
- 流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
- 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。
- warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
- 例如,我设置QPS的threshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10.
- 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
1.2.4 热点参数限流
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。
配置示例
<img src=(https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/微服务.assets/image-20240104163329088.png“ alt=”image-20240104163329088” style=”zoom:50%;” />
注意:热点参数限流对默认的springMVC资源无效
在Controller上添加注解
@SentinelResource("hot")
,其中hot是自定义名称
1.3 隔离和降级
- 虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
- 不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。
1.3.1 FeignClient整合Sentinel
SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。
修改OrderService的application.yml文件,开启Feign的Sentinel功能
给FeignClient编写失败后的降级逻辑
- 方式一:FallbackClass,无法对远程调用的异常做处理
- 方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种
FallbackFactory步骤
在feing-api项目中定义类,实现FallbackFactory
在feing-api项目中的DefaultFeignConfiguration类中将UserClientFallbackFactory注册为一个Bean:
在feing-api项目中的UserClient接口中使用UserClientFallbackFactory:
1.3.2 线程隔离(舱壁模式)
线程隔离有两种方式实现:
- 线程池隔离
- 优点:支持主动超时、支持异步调用
- 缺点:额外开销大
- 场景:低扇出
- 信号量隔离(Sentinel默认采用)
- 优点:轻量、无额外开销
- 缺点:不支持主动超时、不支持异步调用
- 场景:高频调用、高扇出
- 注意:扇出是指这个请求后续依赖的服务数量,后续服务多就是高扇出
- 网关服务就是高扇出
- 线程池隔离
在添加限流规则时,可以选择两种阈值类型:
- QPS:就是每秒的请求数,在快速入门中已经演示过
- 线程数:是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量,实现舱壁模式。
1.3.3 熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器熔断策略:
- 慢调用
- 异常比例
- 异常数
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。例如:
异常比例或异常系数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。例如:
1.4 授权规则
1.4.1 授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
- 然而这个名称不是gateway
具体做法:
Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。
例如,我们尝试从request中获取一个名为origin的请求头,作为origin的值:
我们还需要在gateway服务中,利用网关的过滤器添加名为gateway的origin头:
给/order/{orderId} 配置授权规则:
1.4.2 自定义异常结果
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。如果要自定义异常时的返回结果,需要实现
BlockExceptionHandler
接口:而
BlockException
包含很多个子类,分别对应不同的场景:实现
在order-service中定义类,实现
BlockExceptionHandler
接口
1.5 规则持久化
1.5.1 规则管理模式
Sentinel的控制台规则管理有三种模式:
原始模式:控制台配置的规则直接推送到Sentinel客户端,也就是我们的应用。然后保存在内存中,服务重启则丢失(保存在内存)
pull模式:控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。(保存在本地文件或数据库,定时读取)
push模式:控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。(保存在nasco,监听变更实时更新)
1.5.2 实现push模式
push模式实现最为复杂,依赖于nacos,并且需要改在Sentinel控制台。整体步骤如下:
修改order-service服务,使其监听Nacos配置中心
引入nacos依赖
配置nacos地址
修改Sentinel-dashboard源码,配置nacos数据源
- 复杂
修改Sentinel-dashboard源码,修改前端页面
- 复杂
重新编译、打包-dashboard源码
也可以花钱用阿里的云服务器
二 分布式事务
事务的ACID原则
- 原子性:事务中的所有操作,要么全部成功,要么全部失败
- 一致性:要保证数据库内部完整性约束、声明性约束
- 隔离性:对同一资源操作的事务不能同时发生
- 持久性:对数据库做的一切修改将永久保存,不管是否出现故障
在分布式系统下,一个业务跨越多个服务或数据源,每个服务都是一个分支事务,要保证所有分支事务最终状态一致,这样的事务就是分布式事务。
2.1 理论基础
2.1.1 CAP定理
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标:
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分区容错性)
Eric Brewer 说,分布式系统无法同时满足这三个指标。这个结论就叫做 CAP 定理。
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致
Availability(可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝
Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
Tolerance (容错):在集群出现分区时,整个系统也要持续对外提供服务
CP和CA只能满足其中一种
- ES集群出现分区时,故障节点会被剔除集群,数据分片会重新分配到其它节点,保证数据一致。因此是低可用性,高一致性,属于CP
2.1.2 BASE理论
- BASE理论是对CAP的一种解决思路,包含三个思想:
- Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
- 而分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:
- AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
- CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
- 解决分布式事务,各个子系统之间必须能感知到彼此的事务状态,才能保证状态一致,因此需要一个事务协调者来协调每一个事务的参与者(子系统事务)。
- 这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务
2.2 Seata
2.2.1 简介、架构
- Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/
Seata事务管理中有三个重要的角色:
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
- TCC模式:最终一致的分阶段事务模式,有业务侵入
- AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
- SAGA模式:长事务模式,有业务侵入
2.2.2 部署TC服务
略
2.2.3 微服务集成Seata
- 引入seata依赖
- 配置application.yml,让微服务通过注册中心找到seata-tc-server
2.3 实践
2.3.1 XA模式
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
seata的XA模式做了一些调整,但大体相似:
RM一阶段的工作:
- 注册分支事务到TC
- 执行分支业务sql但不提交
- 报告执行状态到TC
TC二阶段的工作:
TC检测各分支事务执行状态
a.如果都成功,通知所有RM提交事务
b.如果有失败,通知所有RM回滚事务
RM二阶段的工作:接收TC指令,提交或回滚事务
2.3.2 AT模式
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
阶段一RM的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可
阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
2.3.3 TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
- Try:资源的检测和预留;
- Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
- Cancel:预留资源释放,可以理解为try的反向操作。
2.3.4 SAGA模式
- Saga模式是SEATA提供的长事务解决方案。也分为两个阶段:
- 一阶段:直接提交本地事务
- 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
2.3.5 四种模式对比
2.4 高可用
三 分布式缓存
- 单点redis问题
- 数据丢失
- 并发能力:单点redis并发能力虽然不错,但无法满足如618这样的高并发场景
- 故障恢复问题
- 存储能力问题:难以满足海量级的数据需求
3.1 redis持久化
3.1.1 RDB持久化
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
1
save #由redis主进程来执行RDB,会阻塞所有命令
1
bgsave #开启子进程执行,避免主进程受影响
redis停机的时候会执行一次RDB,默认保存在当前运行目录
redis内部有触发RDB的机制,在redis.conf文件中找到
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:
- 当主进程执行读操作时,访问共享内存;
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作。
RDB缺点:
- RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
- fork子进程、压缩、写出RDB文件都比较耗时
3.1.2 AOF持久化
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:
AOF的命令记录的频率也可以通过redis.conf文件来配:
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置:
3.1.3 对比
3.2 redis主从
3.2.1 搭建主从架构
- 单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
- redis读操作多一些,所以留一个master负责写其他从节点负责读
比如有7001 7002 7003 三个端口的redis,现在要把7001作为主节点
1
2
3
4# 连接 7002
redis-cli -p 7002
# 执行slaveof
slaveof 192.168.150.101 70011
2
3
4# 连接 7003
redis-cli -p 7003
# 执行slaveof
slaveof 192.168.150.101 7001然后可以查看集群状态
1
2
3
4# 连接 7001
redis-cli -p 7001
# 查看状态
info replication
3.2.2 主从数据同步原理
- 主从第一次同步是全量同步
过程
- slave节点请求增量同步
- master节点判断replid,发现不一致,拒绝增量同步
- master将完整内存数据生成RDB,发送RDB到slave
- slave清空本地数据,加载master的RDB
- master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave、
- slave执行接收到的命令,保持与master之间的同步
master如何判断slave是不是第一次来同步数据?这里会用到两个很重要的概念:
- Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid/
- offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据
增量同步
- 从repl_baklog中获取offset之后的数据,主节点发送命令过去,从节点执行
- 注意:repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。
主从集群优化
- 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。
- Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
- 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
- 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
3.3 redis哨兵
3.3.1 哨兵作用和原理
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:
监控:Sentinel 会不断检查您的master和slave是否按预期工作
自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
服务状态监控
- Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。、
- 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
- Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
选举新的master
- 一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
- 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
- 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
- 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
- 最后是判断slave节点的运行id大小,越小优先级越高。
- 一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
如何实现故障转移
- sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
- sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
- 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
3.3.2 搭建哨兵集群
3.3.3 RedisTemplate的哨兵模式
Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。
步骤
引入redis的starter依赖
配置文件application.yml中指定sentinel信息(因为主节点可能会不断的变化)
配置主从读写分离
这里的ReadFrom是配置Redis的读取策略,是一个枚举,包括下面选择:
- MASTER:从主节点读取
- MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
- REPLICA:从slave(replica)节点读
- REPLICA _PREFERRED:优先从slave(replica)节点读取,所有的slave都不可用才读取master
3.4 redis分片集群
3.4.1 搭建分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
搭建
启动六个redis
```sh
redis-cli —cluster create —cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:80031
2
3
4
5
6
7
8
9
- `redis-cli --cluster`或者`./redis-trib.rb`:代表集群操作命令
- `create`:代表是创建集群
- `--replicas 1`或者`--cluster-replicas 1` :指定集群中每个master的副本个数为1,此时`节点总数 ÷ (replicas + 1)` 得到的就是master的数量。因此节点列表中的前n个就是master,其它节点都是slave节点,随机分配到不同master
- 查看集群状态
```sh
redis-cli -p 7001 cluster nodes
3.4.2 散列插槽
Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到
数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
- key中包含”{}”,且“{}”中至少包含1个字符,“{}”中的部分是有效部分
- key中不包含“{}”,整个key都是有效部分
例如:key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。
一个节点对应的插槽是很多的
3.4.3 集群伸缩
- 添加一个节点到集群
- 启动一个新的redis实例,端口为7004
- 添加7004到之前的集群,并作为一个master节点
- 给7004节点分配插槽,使得num这个key可以存储到7004实例
- 删除一个节点
3.4.4 故障转移
集群中一个master宕机
- 首先是该实例与其它实例失去连接
- 然后是疑似宕机:
- 最后是确定下线,自动提升一个slave为新的master
数据迁移
- 利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移。其流程如下:
<img src=(https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/微服务.assets/image-20240105105202163.png“ alt=”image-20240105105202163” style=”zoom:67%;” />
- 场景:老旧机器的更换
手动的Failover支持三种不同模式:
- 缺省:默认的流程,如图1~6歩
- force:省略了对offset的一致性校验
- takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见
3.4.5 RedisTemplate访问分片集群
RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:
- 引入redis的starter依赖
- 配置分片集群地址
- 配置读写分离
与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:
四 多级缓存
传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,存在下面的问题:
- 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
- Redis缓存失效时,会对数据库产生冲击
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能:
4.1 JVM进程缓存
4.1.1 Caffeine
- 缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:
- 分布式缓存,例如Redis:
- 优点:存储容量更大、可靠性更好、可以在集群间共享
- 缺点:访问缓存有网络开销
- 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
- 进程本地缓存,例如HashMap、GuavaCache:
- 优点:读取本地内存,没有网络开销,速度更快
- 缺点:存储容量有限、可靠性较低、无法共
- 场景:性能要求较高,缓存数据量较小
- 分布式缓存,例如Redis:
Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址:https://github.com/ben-manes/caffeine
demo
caffeine的三种缓存驱逐策略
基于容量:设置缓存的数量上限
基于时间:设置缓存的有效时间
基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐
4.1.2 实现进程缓存
新建一个配置类,声明多个Cache bean
然后在Controller中注入bean并调用
意思是优先查询cache中的“id”,如果没有就调用itemService的query
4.2 Lua语法入门
4.2.1 初识Lua
- Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:https://www.lua.org/
4.2.2 变量和循环
数据类型
变量
Lua声明变量的时候不需要指定数据类型
访问table
循环
数组、table都可以利用for循环来遍历
遍历数组
遍历table
4.2.3 条件控制、函数
定义函数的语法
举例
条件控制
其中布尔表达式中的逻辑运算符是基于英文单词的
4.3 多级缓存
4.3.1 安装OpenResty
- OpenResty® 是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:
- 具备Nginx的完整功能
- 基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
- 允许使用Lua自定义业务逻辑、自定义库
- 官方网站: https://openresty.org/cn/
4.3.2 OpenResty入门
实现商品详情页数据查询
1.在nginx.conf的http下面,添加对OpenResty的Lua模块的加载:
2.在nginx.conf的server下面,添加对/api/item这个路径的监听:
3.在nginx目录创建lua/item.lua文件
4.重新加载配置
1
nginx -s reload
4.3.3 请求参数处理
4.3.4 查询Tomcat
4.3.5 redis缓存预热
- 冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。
- 缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。
4.3.6 查询redis缓存
openresty查询redis
引入redis模块,并初始化redis对象
封装函数,用来释放redis连接,其实是放入连接池
封装函数,从Redis读数据并返回
4.3.7 Nginx本地缓存
4.4 缓存同步策略
4.4.1 数据同步策略
设置数据同步策略的三种:
设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
- 优势:简单、方便
- 缺点:时效性差,缓存过期之前可能不一致
- 场景:更新频率较低,时效性要求低的业务
同步双写:在修改数据库的同时,直接修改缓存
- 优势:时效性强,缓存与数据库强一致
- 缺点:有代码侵入,耦合度高;
- 场景:对一致性、时效性要求较高的缓存数据
异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
优势:低耦合,可以同时通知多个缓存服务
缺点:时效性一般,可能存在中间不一致状态
场景:时效性要求一般,有多个服务需要同步
基于MQ的异步通知
基于Canal的异步通知
4.4.2 安装Canal
- canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。GitHub的地址:https://github.com/alibaba/canal
- Canal是基于mysql的主从同步来实现的,MySQL主从同步的原理如下:
- MySQL master 将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events
- MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)
- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
4.4.3 监听Canal
引入依赖canal-starter
配置
编写监听器,监听Canal消息
4.5 多级缓存总结
五 服务异步通讯
- MQ 的一些常见问题
- 消息可靠性:如何确保发送的消息至少被消费一次
- 延迟消息问题:如何实现消息的延迟投递
- 消息堆积问题:如何解决数百万消息堆积,无法及时消费的问题
- 高可用问题:如何避免单点的MQ故障而导致的不可用问题
5.1 消息可靠性
消息从生产者发送到exchange,再到queue,再到消费者,有哪些导致消息丢失的可能性?
- 发送时丢失:
- 生产者发送的消息未送达exchange
- 消息到达exchange后未到达queue
- MQ宕机,queue将消息丢失
- consumer接收到消息后未消费就宕机
- 发送时丢失:
5.1.1 生产者消息确认
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。结果有两种请求:
publisher-confirm,发送者确认
- 消息成功投递到交换机,返回ack
- 消息未投递到交换机,返回nack
publisher-return,发送者回执
- 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。
注意:确认机制发送消息时,需要给每个消息设置一个全局唯一id,以区分不同消息,避免ack冲突
5.1.2 消息持久化
MQ默认是内存存储消息,开启持久化功能可以确保缓存在MQ中的消息不丢失。
交换机持久化
队列持久化
消息持久化
5.1.3 消费者消息确认
RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息。而SpringAMQP则允许配置三种确认模式:
manual:手动ack,需要在业务代码结束后,调用api发送ack。
auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
5.1.4 消费失败重试机制
当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力:
解决:利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:
RejectAndDontRequeueRecoverer
:重试耗尽后,直接reject,丢弃消息。默认就是这种方式ImmediateRequeueMessageRecoverer
:重试耗尽后,返回nack,消息重新入队RepublishMessageRecoverer
:重试耗尽后,将失败消息投递到指定的交换机
步骤
首先定义接收失败消息的交换机、队列及其绑定关系:
然后定义
RejectAndDontRequeueRecoverer
5.2 死信交换机
5.2.1 简介
当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):
- 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是一个过期消息,超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
如果该队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)。
5.2.2 TTL
TTL,也就是Time-To-Live。如果一个队列中的消息TTL结束仍未消费,则会变为死信,ttl超时分为两种情况:
- 消息所在的队列设置了存活时间
- 消息本身设置了存活时间
声明一组死信交换机和队列,基于注解
要给队列设置超时时间,需要在声明队列时配置x-message-ttl属性:
发送消息时,给消息本身设置超时时间
5.2.3 延迟队列
利用TTL结合死信交换机,我们实现了消息发出后,消费者延迟收到消息的效果。这种消息模式就称为延迟队列(Delay Queue)模式。
延迟队列的使用场景包括:
- 延迟发送短信
- 用户下单,如果用户在15 分钟内未支付,则自动取消
- 预约工作会议,20分钟后自动通知所有参会人员
因为延迟队列的需求非常多,所以RabbitMQ的官方也推出了一个插件,原生支持延迟队列效果。
SpringAMQP使用延迟队列插件
基于注解(推荐)
基于代码
然后向这个delay为true的交换机中发送消息,一定要给消息添加一个header:x-delay,值为延迟的时间,单位为毫秒:
5.3 惰性队列
5.3.1 消息堆积问题
当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。最早接收到的消息,可能就会成为死信,会被丢弃,这就是消息堆积问题。
解决思路
- 增加更多消费者,提高消费速度
- 在消费者内开启线程池加快消息处理速度
- 扩大队列容积,提高堆积上限
5.3.2 惰性队列
从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。
惰性队列的特征如下:
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储
如何声明:只需要在声明队列时,指定x-queue-mode属性为lazy即可。可以通过命令行将一个运行中的队列修改为惰性队列:
用SpringAMQP声明惰性队列的两种方式
@Bean
(简单一些)注解
5.4 MQ集群
5.4.1 集群分类
- RabbitMQ的是基于Erlang语言编写,而Erlang又是一个面向并发的语言,天然支持集群模式。RabbitMQ的集群有两种模式:
- 普通集群:是一种分布式集群,将队列分散到集群的各个节点,从而提高整个集群的并发能力。
- 镜像集群:是一种主从集群,普通集群的基础上,添加了主从备份功能,提高集群的数据可用性。
- 镜像集群虽然支持主从,但主从同步并不是强一致的,某些情况下可能有数据丢失的风险。因此在RabbitMQ的3.8版本以后,推出了新的功能:仲裁队列来代替镜像集群,底层采用Raft协议确保主从的数据一致性。
5.4.2 普通集群
- 普通集群,或者叫标准集群(classic cluster),具备下列特征:
- 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。
- 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
- 队列所在节点宕机,队列中的消息就会丢失
5.4.3 镜像集群
- 镜像集群:本质是主从模式,具备下面的特征:
- 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。
- 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
- 一个队列的主节点可能是另一个队列的镜像节点
- 所有操作都是主节点完成,然后同步给镜像节点
- 主宕机后,镜像节点会替代成新的主
5.4.4 仲裁队列
3.8之后的新功能,用于替换镜像队列。特征:
- 与镜像队列一样,都是主从模式,支持主从数据同步
- 使用非常简单,没有复杂的配置
- 主从同步基于Raft协议,强一致
SpringAMQP创建仲裁队列
定义队列
SpringAMQP连接集群,只需要在yaml中配置即可: