Redis高级
Redis分布式缓存
Redis集群
单节点Redis问题
- 数据丢失问题:redis是内存存储,服务重启可能会导致数据丢失
- 并发能力问题:redis本身具有较强的并发能力,但是无法满足高并发的场景
- 故障恢复问题:Redis宕机会导致服务不可用,需要一种自动故障恢复的方法
- 存储能力问题:Redis单节点存储数据量难以满足海量数据需求
Redis持久化
RDB持久化
RDB(Redis Database Backup file-Redis数据备份文件,也称Redis数据快照)指将内存中的所有数据记录到磁盘中。当Redis实例故障重启时,从磁盘读取快照文件,恢复数据。
快照文件称为RDB文件,默认保存在当前运行目录中。Redis停机时会执行一次RDB。
RDB文件
关闭服务之前,进行一次RDB文件的保存
查看挂载目录下的RDB文件
Redis配置RDB
Redis内部存在触发RDB的机制,可以在配置文件redis.conf文件中进行配置。
Redis的Fork原理
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。异步完成fork后读取内存数据并写入RDB文件中。
在linux系统中,所有的进程都没有办法直接操作物理内存,操作系统会分配虚拟内存,而主进程只能操作虚拟内存,操作系统会维护一个虚拟内存与物理内存之间的映射关系表(称为页表),bgsave进行fork子进程时,只将页表进行复制。
为了防止同时读写带来的脏数据,fork采用copy-on-write技术
当主进程执行读操作时,访问只读的共享内存空间
当主进程执行写操作时,会拷贝一份数据副本,进行读写操作
总结
RDB的bgsave的基本流程
- fork主进程得到一个子进程,共享内存空间
- 子进程读取内存数据并写入新的RDB文件
- 用新RDB文件替换旧的RDB文件
RDB会执行时间,save命令的含义
- 默认是服务停止时执行
- save 60 1000代表60秒内至少执行1000次修改则触发RDB
RDB的缺点
- RDB执行时间间隔较长,两次RDB之间写入数据存在丢失的风险
- Fork子进程、压缩、创建RDB文件都是比较耗时
AOF持久化
AOF定义
AOF(Append Only File)是追加文件,Redis处理的每一个写命令都会记录到在AOF文件,可以看作是命令日志文件。
AOF开启配置
AOF默认是关闭状态,在配置文件redis.conf中开启AOF
1 | # 开启AOF功能,默认是关闭状态 |
通过redis.conf文件来配置AOF命令记录的频率
1 | # 表示每次执行一次写命令,立即记录到AOF文件中 |
appendfsync配置项 | 刷盘时机 | 优点 | 缺点 |
---|---|---|---|
always | 同步刷盘 | 可靠性高,数据几乎不会丢失 | 性能影响大 |
everysec | 每秒刷盘 | 性能适中 | 可能会丢失1秒的数据 |
no | 操作系统控制 | 性能最好 | 可靠性比较差,可能会丢失大量数据 |
关闭redis,会有一次AOF文件的同步
redis会从AOF文件中进行一次数据加载
由于AOF是记录命令,AOF文件会比RBD文件大,而且AOF会记录同一个key的多次写操作,只有最后一次写操作命令才有意义。
通过
brewriteaof
命令可以后台开启独立线程异步让AOF文件执行重写功能,用最少的命令达到相同的效果
Redis会在触发阈值时自动重写AOF文件,阈值可以在配置文件redis.conf中进行配置
1 | # AOF文件与上一次重写后文件增加超过百分比触发重写 |
总结
Redis主从
搭建主从架构
单节点redis的并发能力是存在上限的,要提高redis的并发能力,需要搭建主从集群,实现读写分离
搭建redis从节点
使用docker搭建从节点,端口分别是6378和6377
1 | version: '3.1' |
开启主从关系的两种方式
修改配置文件(永久生效)
1
2在配置文件redis.conf中添加配置
slaveof <masterip> <masterport>使用redis-cli客户端,执行slaveof命令(重启失效)
1
2
3
4在redis5.0之前的版本执行slaveof命令
slaveof <masterip> <masterport>
在redis5.0之后的版本执行replicaof命令
replicaof <masterip> <masterport>由于在docker容器内部进行操作,即使在一台主机上,也不能使用localhost代替masterip
进入主节点,查看主从关系
1 | INFO REPLICATION |
在主节点存储数据,在从节点可以查询
总计
主从复制原理
在2.8版本之前只有全量复制,在2.8版本之后有全量复制和增量复制
全量(同步)复制
:第一次同步增量(同步)复制
:会将主从库网络断连期间主库收到的命令,同步给从库
全量同步
当启动多个Redis实例时,它们之间就可以通过
replicaof
命令(或者slaveof
命令)形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步
第一阶段:主从库建立连接时,从库发送主库一个psync命令请求数据同步,主库根据这个命令的参数启动辅助,psync命令包括对应replid和偏移量offset两个参数,当replid与主库的replid不一致时,主库会使用全量复制并发送FULLRESYNC响应命令并带上两个参数:replid和offset(偏移量用于记录复制进度)
第二阶段:主库执行bgsave命令,依赖内存快照生成RDB文件并将文件发送到从库,从库接收到RDB文件后,会先清空本地当前数据,加载RDB文件。在主库将文件发送往从库的过程,这个过程时比较耗时的且有可能受网络波动的影响,但主库并不会被阻塞,正常接收请求,为了保证数据一致性,主库会将RDB文件生成后的所有写命令记录到内存中专门的repl_baklog中
第三阶段:将第二阶段记录的repl_baklog中的写命令发送给从库,从库接收到的命令并执行,完成同步。
从节点日志
主节点日志
全量复制总结
增量同步
主从第一次同步是全量同步,如果从节点重启或者网络闪断后,则执行增量同步
repl_baklog是环形缓冲区,存储大小存在上限,在从库重启或者网络闪断太久,从库会丢失掉那部分被新的写命令覆盖掉时,无法进行增量同步,从库和主库之间要进行全量复制。
从库记录着自己的relid,每个从库的复制进度不一定相同,从库重连时,主库会根据从库各自的复制进度决定这个从库是进行增量重复还是全量重复。
优化主从复制
优化全量同步性能
:
在master配置文件中配置repl-diskless-sync-yes启用无磁盘复制,不生成RDB文件直接发送数据给从节点,避免全量同步时的磁盘IO
减少Redis单节点上的内存占用,减少RDB导致过多的磁盘IO和网络IO
尽量减少全量同步
:
- 适当提高repl_baklog的大小,发现slave宕机或者网络闪断时,尽快实现故障恢复,尽可能避免全量同步
降低主节点同步压力
- 限制一个主节点上的从节点的数量,可以采用
主-从-从
的链式结构,减少master压力
总结
Redis哨兵
slave节点宕机恢复后,可以找到master节点同步数据,但是当master节点发生宕机时,我们需要怎么解决
在Redis主从集群中,哨兵机制是实现主从库自动切换的关键机制,它有效地解决主从复制模式下的故障转移问题
哨兵机制(Redis Sentinel)的作用和原理
在Redis2.8版本开始引入Redis Sentinel(Redis哨兵),哨兵的核心功能是实现主从集群的自动故障转移。
哨兵的作用
监控
(monitoring):哨兵不断检查master和slave是否按照预期工作自动故障恢复
(automatic failover):如果master不能正常工作,sentinel会将失效master的一个slave升级为新的master并让其他从节点同步新的master,当故障实例恢复后,以新的master作为主节点配置提供者
(configuration provider):在客户端进行初始化时,通过连接哨兵来获得当前redis集群服务的主节点地址通知
(notification):当集群发生故障转移时,哨兵会将最新的变更信息发送给redis客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令
主观下线
:如果某sentinel节点发现实例未在规定时间内响应,则认为该实例主观下线客观下线
:若超过指定数量(quorum:一般为sentinel实例数量的一半)的sentinel认为该实例主观下线,则该实例为客观下线
哨兵集群的选举
为了安全性,一般哨兵会搭建分布式集群,作为分布式集群,必然涉及共识问题(即选举问题)
哨兵的选举机制一般是一个简单的
Raft算法
:选举的票数大于等于num(sentinel)/2+1时,该选举者将成为新的主节点
新主库的选出
master被判定客观下线,sentinel要从剩余的从库中选择一个新的master
- 过滤掉不健康的(下线或断线),没有响应哨兵ping的slave
- 过滤掉与master断开时间长短的slave,如果超过指定值(down-after-milliseconds*10),则会直接被排除
- 选择slave节点中salve-priority值最小的即优先级最高的(在配置文件redis.conf中配置)
- 选择复制偏移量最大即offset值最大的,越大说明数据越新,复制最完整的从节点
- 选择slave节点的运行id较小的
故障的转移
- sentinel给选举出的slave节点发送
replicaof on one
命令,让其脱离从节点,升级为主节点 - 将其他从节点发送slaveof命令,指向新的主节点,从新的master同步数据
- 通知应用程序客户端RedisClient主节点的变更信息即新的主节点的地址
- 修改原主节点即故障节点的配置文件,将其标记为slave,当故障节点恢复后会自动变成新主节点的从节点
总结
搭建哨兵集群
创建配置文件
1 | port <port> # 哨兵实例运行端口 |
1 | port 27001 |
编写Docker-compose.yml配置文件
1 | version: '3' |
文件结构
运行服务
1 | cd /mydata/sentinel/ |
测试故障转移
关闭主节点
查看sentinel日志
查看转移的主节点
RedisTemplate哨兵模式
Sentinel集群监管下的Redis主从集群中,其主节点会由于自动故障转移而发生变化,Redis客户端必须感知变化并及时更新连接信息。Spring的
RedisTemplate
底层利用lettuce
实现了节点感知和自动切换。
Spring配置哨兵模式
引入redis的starter依赖
1 | <dependency> |
配置文件指定sentinel信息
1 | logging: |
配置主从读写分离
1 |
|
ReadFrom是读取策略:
MASTER
:从主节点读取MASTER_PREFERRED
:优先从主节点读取,主机点不可用才读取从节点REPLICA
:从从节点读取REPLICA_PREFERRED
:优先从从节点读取,所有的从节点不可用才读取主节点
Redis分片集群
分片集群
主从集群
和哨兵模式
解决了高可用,高读并发的问题,但是写能力和存储能力无法进行扩展:
- 海量数据存储问题
- 高并发写问题
主节点分片集群的可以存储海量数据,同时吸收高写并发能力:
- 集群中有多个主节点,每个主节点存储不同的数据
- 每个主节点分片可以有多个从节点
- 主节点之间通过ping监控彼此健康状态,实现故障转移
- 客户端请求可以访问集群中任意的节点并且被转发到正确的节点上
docker-compose部署redis集群
编写docker-compose文件
1 | version: '3' |
--cluster-enabled yes
:开启集群--cluster-config-file nodes.conf
:集群配置文件cluster-node-timeout 5000
:节点心跳失败的超时时间--cluster-announce-ip 1.117.34.49
:节点的注册实例ip--cluster-announce-port 7001
:节点的注册实例端口--cluster-announce-bus-port 17001
:节点的注册总线端口
--appendonly yes
:开启 备份 模式--protected-mode no
:关闭 保护 模式
启动集群
1 | docker-compose up -d |
建立集群
1 | docker exec -it redis-master1 redis-cli --cluster create --cluster-replicas 1 1.117.34.49:7000 1.117.34.49:7001 1.117.34.49:7002 1.117.34.49:7003 1.117.34.49:7004 1.117.34.49:700 |
redis-cli --cluster
:代表 集群命令操作create
:代表 创建集群操作--cluster-replicas 1
:代表 指定集群中每个master的副本个数是1,此时节点总数 = 节点总数 / (replicas + 1)即master的数量,其他节点都是slave节点,随机分配到不同的master
查看集群状态
1 | redis-cli -p 7000 cluster nodes |
散列插槽
Redis会将每一个master节点映射到0-16383(共16364个)插槽(hash slot)中
查看集群信息
数据key不是与节点绑定的绑定的,而是与插槽绑定
- key中包含”{}”并且”{}”中至少包含一个字符串,则”{}”为有效部分
- key中不包含”{}”,整个key都是有效部分
例如:当存储的key是 a 时,那么根据 a 计算;如果是{test}a,则根据{a}test计算。计算方式是利用CRC16算法得到一个 Hash 值,然后对于 15495 取余,得到的结果就是 slot 值
1 | docker exec -it redis-master1 /bin/bash |
总结
Redis如何判断某个key应该在哪一个实例
将16384个插槽分配不同的实例,根据key的有效部分计算出哈希值,将哈希值对16384取余,余数作为插槽,寻找插槽所处实例即可
如何将 同一类数据 固定的保持在同一个redis实例
这一类数据使用相同的有效部分({}中部分为有效部分,例如这些数据的key都以 {typeId} 为前缀)
集群伸缩
添加一个节点到集群
操作集群的命令
Redis提供了很多 操作集群 的命令
1 | redis-cli --cluster help |
新增节点到集群
1 | version: '3' |
1 | docker exec -it redis-master1 redis-cli --cluster add-node 1.117.34.49:7006 1.117.34.49:7000 |
查看集群状态
发现 新增的节点 没有 插槽
1 | docker exec -it redis-master1 redis-cli -p 7000 cluster nodes |
分配插槽
1 | # redis-cli --cluster reshard --cluster-from 迁出节点ID --cluster-to 接收节点ID --cluster-slots 迁出槽数量 迁出节点ip 端口 |
重新查看节点状态
1 | docker exec -it redis-master1 redis-cli -p 7000 cluster nodes |
故障转移
故障转移
- 实例与其他实例失去连接
- 其他实例心跳机制判断该节点是否宕机
- 确定节点宕机,自动提升一个从节点成为新的主节点
数据迁移
利用
cluster failover
命令 手动 让集群中的某个主节点 宕机,切换到执行 cluster failover 命令这个从节点,实现无感知到数据迁移
手动Failover支持的三种不同模式
- 缺省:默认流程
- force:省略对于2-3步对于offset的一致性校验
- takeover:直接执行第5步,忽略数据一致性,忽略master状态和其他master意见
RedisTemplate访问分片分配
RedisTemplate底层同样基于 lettuce 实现了 分片集群 的支持
1 | <dependency> |
1 | spring: |
1 |
|
多级缓存
传统缓存问题
多级缓存方案
多级缓存是充分利用请求处理的每一个环节,添加缓存,减轻Tomcat压力,提升服务器性能
Nginx代理
JVM进程缓存
本地进程缓存
缓存数据的读取速度非常快,能对大量减少对数据库的访问,减少数据库的压力
优点:集群部署支持数据共享,分片集群确保存储容量大,哨兵机制保证可靠性
缺点:访问缓存存在网络开销和网络延迟
场景:缓存数据量大、可靠性要求高,数据集群共享
优点:直接读取本地内存,没有网络开销,速度更快
缺点:存储容量有限,可靠性较低,无法共享
场景:性能要求较高,缓存数据量较小
Caffeine
Caffine是基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库
Caffine的使用
引入依赖
1 | <dependency> |
Cache手动创建
1 | CACHE = Caffeine.newBuilder() |
Caffine的三种缓存驱逐策略
- 基于容量:设置缓存的 数量上限
1 | CACHE = Caffeine.newBuilder() |
- 基于时间:设置缓存的 有效时间
1 | CACHE = Caffeine.newBuilder() |
- 基于引用:设置缓存为软引用和弱引用,利用GC回收缓存数据(性能较差,不建议使用)
在默认情况下,当一个缓存元素过期时,Caffine不会自动立即将其清理和驱逐,而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐
Lua语法
初始Lua
Lua是一种轻量小巧的脚本语言,用标准的C语言编写并以源代码形式开发,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
变量和循环
数据类型
变量
循环
数组和table都可以使用for循环来遍历
1 | -- 声明数组 key为索引的 table |
1 | -- 声明map即table |
条件控制和函数
函数
1 | function 函数名(args1, args2...argsn) |
1 | local arr = {'java', 'pyrhon','lua'} |
条件控制
1 | if (布尔表达式) |
1 | function printArr(arr) |
多级缓存
openResty
OpenResty是一个基于Nginx的高性能Web平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:
- 具备具备Nginx的完整功能
- 基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
- 允许使用Lua自定义业务逻辑、自定义库
安装openresty
安装容器
1 | docker pull openresty/openresty |
运行容器
1 | docker run --name openresty -d -p 90:80 openresty/openresty |
挂载目录
1 | mkdir -p /mydata/openresty/nginx |
1 | docker cp openresty:/usr/local/openresty/nginx/ /mydata/openresty/ |
重新运行容器
1 | docker rm -f openresty |
1 | docker run --name openrestry -d -p 90:80 \ |
openResty入门
步骤一:修改Nginx.conf文件
在nginx.conf的http中,添加对OpenResty的Lua模块的加载
1 | # 加载 lua 模块 |
在nginx.conf的server下面,添加对 指定路径 的监听
1 | location /api/product { |
步骤二:编写lua文件
在nginx目录创建文件夹lua以及新建lua文件
1 | cd /mydata/openresty/nginx |
编辑内容
1 | -- 返回模拟数据(ngx.say()函数用于写入数据到Response中) |
重启加载配置
1 | docker restart openresty |
请求参数处理
获取参数的API
OpenResty中提供了一些API用来获取不同类型的前端请求参数
路径占位符
1 | /product/1 |
请求头
1 | id:1 |
GET 请求参数
1 | ?id = 1 |
POST 请求参数
1 | id = 1 |
JSON 参数
1 | {"id": 1} |
示例
路径占位符
1 | # nginx.conf |
编辑lua文件
1 | -- product.lua |
查询Tomcat
之前我们通过 nginx 代理转发到 openresty 通过执行 lua 文件,返回模拟数据,在真实的场景中,需要 openresty 请求 tomcat 服务器拿到真实的数据进行返回
nginx内部发送http请求
nginx提供了内部API用以发送http请求
1 | -- product.lua |
返回的响应内容包括
resp.status
:响应状态码resp.header
:响应头(即table键值对)resp.body
:响应体(响应数据)
注意:/path 是路径,并不包括 IP 和 端口,这个请求会被nginx内部的server监听并进行反向代理至 tomcat 服务器
1 | # nginx.conf |
封装HTTP请求查询的函数
可以将http查询的请求封装为一个函数,放到OpenResty函数库中,方便后期使用
在/usr/local/openrrsty/lualib目录下创建get.lua文件
1 | vi /usr/local/openrrsty/lualib/common.lua |
在get.lua中封装http查询的函数
1 | -- 封装函数 发送 http 请求 并解析响应 |
示例
1 | -- 导入get函数库(由于文件就在lualib下,无需加上路径名,直接写文件名即可) |
JSON结果处理
OpenResty提供了一个cjson的模块用来处理JSON序列化和反序列化,可以使用此工具完成数据的组合工作
- 引入cjson模块
1 | local cjson = require "cjson" |
- 序列化
1 | local obj = { |
- 反序列化
1 | local json = '{"name": "jack", "age": 21}' |
Tomcat集群负载均衡
配置反向代理集群配置
1 | # 反向代理配置 将/item路径的请求代理到tomcat集群 |
修改负载均衡策略
由于 默认 的集群模式反向代理是采用轮询的方式进行访问,这就导致了同样参数的请求会落到不同的服务器,由于进程缓存不同步,导致不同的tomcat进程缓存都需加载
修改负载均衡策略,基于用户请求的uri做简单hash,让同一个请求始终到一台服务器上,让进程缓存只需在一台服务器上加载
1 | upstream tomcat_cluster { |
Redis缓存预热
在nginx请求tomcat之前,可以再加一层redis缓存,让请求优先查询缓存
冷启动和缓存预热
冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品都在第一次查询时添加到缓存中,可能会给数据库带来较大的压力
缓存预热:面对冷启动的问题,我们可以利用大数据统计用户访问的热点数据,在项目启动时,将这些热点数据提前查询并保存到缓存中。或者数据量比较少时,可以在启动时将所有数据放入缓存中
缓存预热
编写初始化类
1 |
|
查询Redis缓存
OpenResty提供了操作Redis的模块,使用只需要引入该模块就可以直接使用
1 | -- 引入 redis 模块 |
1 | -- 关闭 redis 连接的工具方法 其实释放进入连接池 |
1 | -- 查询 redis 方法 ip和port是redis地址 key是查询的key |
示例
将上述操作 Redis 客户端的函数封装到通用函数库里(与之前的流程一样)
根据具体业务封装函数
1 | -- 封装 函数,先查询 redis, redis 未命中 再去请求 tomcat 服务器 |
调用函数
1 | -- 获取路径参数 |
Nginx本地缓存
OpenResty为Nginx提供了shard dict的功能,可以为nginx的多个worker进程之间共享数据(内部共享内存),实现缓存功能
1 | # 开启共享字典 (本地缓存) 名称为:item_cache, 大小150m |
1 | -- 获取 本地缓存对象 |
缓存同步
缓存同步策略
基于Canal的异步通知
初始Canal
Canal(译为水道/管道/渠道),Canal是阿里巴巴旗下的一款开源项目,基于Java开发,基于数据库增量日志分析,提供增量数据订阅&消费。
Canal是基于 MySQL的主从同步 来实现的,MySQL 主从同步的原理如下:
- MySQL Master 将数据变更写入到二进制日志(binary log),其中记录的数据为 binary log events
- MySQL Slave 将 Master 的 binary log events 拷贝到它的中继日志(relay log)
- MySQL Slave 重放 Relay Log 中事件,将数据变更反映到它自己的数据
Canal 就会将自己 伪装成 一个 MySQL 的一个Slave 节点,从而监听 Master 的 binary log 的变化,再把得到的变化信息通知给 Canal 客户端,进而完成其他数据库的同步
安装Canal
指定binary log文件和database
修改MySQL配置文件
MySQL 配置文件挂载在宿主机的 /mydata/mysql/conf 目录中
1 | vim /mydata/mysql/conf/my.cnf |
1 | # 设置 binary log 文件的存放地址 /var/lib/mysql/ 和文件名 mysql-bin |
重启MySQL服务
1 | docker restart mysql |
查看 /mydata/mysql/data/ 目录下的 binary log 日志文件
设置用户权限
1 | CREATE user canal@'%' IDENTIFIED BY 'canal'; |
创建网络并将MySQL容器加入网络
1 | docker network create canal_default |
1 | docker network connect canal_default mysql |
创建Canal容器
1 | docker pull canal/canal-server |
1 | # 启动 容器 | 查看配置文件路径 | 拷贝 | 重新启动容器 |
修改配置文件 instance.properties
重新启动并查看日志
1 | docker restart canal |
监听Canal
Canal客户端
Canal提供了各种语言的客户端,当Canal监听到 binlog 变化时,会通知 canal 客户端
Canal 提供了各种语言的客户端,当 Canal 监听到 binlog 变化时,会通知 Canel 的客户端
编写Canal客户端
这里使用 第三方开源的canal-starter
引入依赖
1 | <!-- Canal --> |
编写配置文件
1 | canal: |
编写监听器
1 | // 指定监听的表名 |
Canal 推送给 canal客户端 的是 被修改的这一行数据(row),而我们引入的canal客户端需要我们把数据封建到实体类中,这个过程需要知道数据库与实体类的映射关系,需要用的JPA的几个注解
1 | import javax.persistence.*; |
总结
Redis最佳实践
Redis键值设计
Key结构
Redis的Key可以自定义,但是最好尊循下面几个最佳实践的约定:
- 遵循基本格式:[业务名称]:[数据名]:[id]
- 长度不超过44字节
- 不包括特殊字符
优点:1、可读性强,易于管理、避免冲突;2、节省内存空间:key是string类型,底层编码包括int、embstr和raw三种(embstr在小于44个字节使用,采用连续内存空间,内存占用更小)
拒绝BigKey
BigKey 通常以 Key的大小 和 Key中成员的数量 来综合判定
- Key 本身的数据量过大
- Key 中成员数量过多
- Key 中成员的数据量过大
1 | MEMORY USAGE |
推荐值
- 单个Key的Value小于10KB
- 对于集合类型的Key,建议元素数量小于1000
Bigkey的危害
- 网络阻塞:对Bigkey执行读请求时,少量的QPS就可能会导致带宽使用率被占满
- 数据倾斜:BigKey所在的Redis实例内存使用率远超其他实例,无使数据分片的内存资源达到均衡
- Redis阻塞:对于元素较多的hash、list、zset等做运算耗时较久,使得主线程(Redis是单线程)被阻塞
- CPU压力:对BigKey的数据序列化和反序列化会导致CPU的使用率飙升
发现BigKey
1 | redis-cli -a 密码 --bigkeys |
利用第三方工具,如 Redis-Rdb-Tools
分析 RDB 快照文件,全面分析内存使用情况
自定义监控,监控进出Redis的网络数据,超出预警值主动告警
删除BigKey
BigKey 内存占用较多,即便删除也需要消耗很长的时间,导致Redis主进程阻塞,引发一系列的问题
- Redis 3.0 及以下版本
如果是集合类型,则遍历BigKey元素,先逐个删除单个元素,最后删除BigKey
Redis在 4.0 后提供了 异步删除的命令-
unlink
恰当的数据类型
对象
推荐使用
hash
结构
json字符串:优点-实现简单粗暴 缺点-数据耦合没有灵活性
字段分散:灵活访问对象任意字段 缺点-占用空间大,无法同一控制
Hash:底层使用ziplist,空间占用小,灵活访问对象的任意字段 缺点-代码相对复杂
大键值对
存在问题
- Hash 的
entry
数量超过512
时,会使用哈希表
而不是ZipList
,内存占用较多 - 通过
hash-max-ziplist-entries
配置entry 上限
,但是 如果 entry 过多会导致 Bigkey 问题
如果拆分为String类型,由于string结构底层没有太多的内存优化,存在很多元数据等要存,内存占用较多,其次当业务上想要批量当获取这些数据时比较麻烦
划分Hash
拆分为小的Hash,将id/100作为key,将id%100作为field,每100个元素作为一个Hash
总结
Key
- 固定格式:**[业务名]:[数据名]:[id]**
- 足够简短:不超过44字节
- 不包括特殊字符
Value
- 合理的拆分数据,拒绝BigKey
- 选择合适的数据结构
- Hash结构的entry数量不要超过1000
- 设置合理的超时时间
批处理优化
Pipeline
单个命令的执行流程
N个命令的执行流程
N个命令批量执行
MSET
不要在一次批处理中传输大多的命令,否则单次命令占用带宽过多,会导致网络阻塞
Pipeline
MSET 虽然可以进行批处理,但是只能操作部分数据类型,因此如果有对复杂的数据类型的批处理需求时,建议使用Pipeline功能
1 |
|
1 | /** |
总结
批处理方案:1、原生的m操作 2、Pipeline批量操作(推荐)
注意事项:1、批处理不建议一次携带太多命令 2、Pipeline的多个命令之间不具备原子性
集群下的批处理
如果
MSET
或者``Pipeline在一次请求中携带了多条命令,而此时如果
Redis`是一个集群,那么批量的命令多个Key计算出来的hash值必须落到一个插槽中,否则会导致执行失败(No way to dispatch this command to Redis Cluster because keys have different slots)
串行slot
和 并行slot
的区别在于是否开启多个线程异步的执行各组命令,hash_tag
虽然耗时非常短,但是容易出现数据倾斜
redisTemplate.opsForValue().multiSet(Map<? extends K, ? extends V> map)
底层就是 并行slot
在客户端计算每个 key
的 slot
,将 slot 一致分为一组,每组都利用 Pipeline
批处理,异步并行执行各组命令
1 | // 批量设置 |
1 | // 批量查询 |
服务端优化
持久化配置
Redis 持久化可以保证数据安全,但是会带来额外的开销,因此持久化要遵循下列建议
用来作为缓存业务的Redis实例尽量不要开启持久化功能,对于安全性要求比较高的业务可以开启持久化
建议关闭RDB持久化功能,使用AOF持久化
利用脚本定期在Slave节点做RDB,实现数据备份
在使用AOF持久化时,设置合理的rewrite阈值,避免频繁的bgrewrite
- 配置
no-appendfsync-on-rewrite=yes
,禁止rewrite期间进行AOF,避免因AOF带来巨大的磁盘IO,引起的主线程阻塞
部署建议
- Redis实例的物理机要预留足够的内存,以应对fork和rewrite
- 单个Redis实例内存上限不要过高(4/8G),可以加快fork速度,减少主从同步,数据迁移的压力
- 不要与CPU密集型应用部署在一起
- 不要与高硬盘负载应用一起部署,例如数据库,消息队列
慢查询
慢查询:在Redis执行时耗时超过某个阈值的命令,称为慢查询
由于Redis是单线程执行命令即主线程执行命令,Redis有一个队列让命令请求进行排队依次执行,当一个查询命令执行时间过长时,会导致其他后面的请求命令等待超时
慢查询的阈值可以通过配资指定
slowlog-log-slower-than
:慢查询阈值,单位微秒,默认是10000(10ms),建议1000(1ms)
慢查询会被存放在慢查询日志中,日志的长度存在上限,可以通过配置指定
slowlog-max-len
:慢查询日志(本质是一个队列)的长度,默认128,建议1000
查看慢查询日志列表
slowlog len
:查询慢查询日志长度slowly get [n]
:读取n条慢查询日志slow reset
:清空慢查询列表
命令和安全配置
Redis 默认会绑定在
0.0.0.0:6379
,这会将Redis服务暴露到公网上,如果Redis没有做身份认证,会出现严重的安全漏洞
漏洞出现核心原因:
- Redis
未设置密码
(默认的 redis服务 没有密码) - 利用 Redis 的
config set 命令动态修改
Redis 配置 - 使用了
Root 账号权限
启动 Redis
为了避免这样的漏洞:
- Redis 一定要
设置密码
- 禁止线上使用如下命令:
keys
、flushall
、flushed
、config set
等命令,可以利用rename-command
禁用
1 | rename-command CONFIG CONFIGURATION # 此时 CONFIG 被修改为 CONFIGURATION |
- 配置
bind
限制网卡,禁止外网网卡访问
1 | bind 0.0.0.0 # 默认是 开放 0.0.0.0 |
- 开启
防火墙
1 | systemctl enable firewalld |
不用使用root 账户
启动 Redis,防止 root 权限修改本地目录和文件- 尽量
不要使用默认端口
内存配置
当Redis内存不足时,可能会导致 key 频繁被删除,响应时间变长,QPS不稳定等问题,当内存使用率达到90%以上时需要警惕并快速定位到内存占用的原因
Redis 内存分配
数据内存:Redis最主要的部分,存储Redis键值信息,主要信息是
BigKey
问题以及内存碎片
问题进程内存:Redis主进程本身运行占用内存,如代码、常量池等等,这部分内存占用一般几兆,在大多数生产环境中,它与Redis数据占用内存相比可以忽略不计
缓存区内存:一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区包括输入缓冲区和输出缓冲区两种,这部分内存占用波动较大,如果不当使用BigKey,可能会导致内存溢出
查看内存分配命令
Redis 提供了一些命令,可以查看Redis目前的内存分配状态
- INFO MEMORY
- MEMORY STATS
内存缓冲区配置
内存缓冲区的常见三种:
复制缓冲区:主从复制用于增加同步的
repl_backlog_buf
,如果该缓冲区的大小设置的大小,会增量的数据缓冲区无法全部保存,从而导致频繁的全量同步,影响性能:可以通过repl-backlog-size
来设置,默认是1mbAOF缓冲区:AOF 刷新磁盘之前的缓存区域,AOF 执行 rewrite 的缓冲区,无法设置容量的上限
客户端缓冲区:分为输入缓存区和输出缓冲区,输入缓冲区最大1G且不能设置,输出缓冲区可以设置
1
2
3
4client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
# <class> 客户端类型:normal:普通客户端 replica:主从辅助客户端 pubsub:PubSub客户端
# <hard limit>:缓冲区上限在超过 limit 后断开
# <soft limit> <soft seconds> 缓冲区上限,在超过soft limit并且持续soft seconds秒后断开客户端
也可以通过命令查看内存缓冲区大小和每个客户端的缓冲区占用情况
1 | # 查看客户端的输入缓冲区和输出缓冲区 |
集群最佳实践
在Redis的默认配置中, 如果发现任意一个插槽不可用,则整个集群都会停止对外服务
可以通过修改 cluster-require-full-coverage false
让部分插槽不可用时,其他插槽依旧对外正常服务,来保证汲取高可用的特性
1 | require-full-coverage false |
集群节点之间会不断的互相 ping
来确定集群中其他节点的状态,每次 ping
携带的信息至少包括:插槽信息
和集群状态信息
。这就导致集群中节点越多,集群的状态信息数量也就越大,10个节点的相关信息可能就到达了1kb,此时每次集群的互通所需的带宽就会非常高
解决途径
- 避免大集群,集群节点数据不要过多,最好少于1000,如果业务比较大,则可以建立多个集群
- 避免在单个物理机上运行太多的Redis实例,因为单个物理机的带宽是有限的
- 配置合适的
cluster-node-timeout
值,不要让ping
的频率过高,也不能让频率过慢,否则会导致可用性降低
注意:单体Redis(主从Redis已经可以达到万级别的QPS,并且由于哨兵机制,它也具备很强的高可用特性,如果主从能够满足业务需求的情况下,尽量不要搭建Redis集群)