Redis面试题整理
Redis
1、Redis主从集群
1.1、搭建主从集群
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离
1.2、主从同步原理
当主从第一次同步连接或断开重连时,从节点都会发送psync请求,尝试数据同步:
- replicationID:每一个master节点都有自己的唯一id,简称replid
- offset:repl_backlog中写入过的数据长度,写操作越多,offset值越大,主从的offset一致代表数据一致
可以从一下几个方面来优化Redis主从就集群:
- 在master中配置repl-diskless-sync yes启动无磁盘赋值,避免全量同步时的磁盘IO
- Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
- 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
- 限制一个master上的slave节点数量,如果实在时太多slave,则可以采用主-从-从链式结构,减少master压力
总结:
1、简述全量同步和增量同步区别?
- 全量同步:master将完整内存数据生成RDB,发送RDB到slave
- 增量同步:slave提交自己的offset到master,master获取repl_baklog中slave的offset之后的命令给slave
2、什么时候执行全量同步?
- slave节点第一次两节master时
- slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
3、什么时候执行增量同步?
- slave节点断开又恢复,并且在repl_baklog中能找到offset时
1.3、哨兵原理
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的具体作业如下:
- 监控:Sentinel会不断检查您的master和slave是否按预期工作
- 自动故障切换:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
- 通知:当集群发生故障转移时,Sentinel会将最新节点角色信息推送给Redis客户端
1.3.1、服务状态监控
Sentinel基于心跳机制检测服务状态,每隔1秒钟向集群的每个实例发送ping命令:
- 主观下线:如果某Sentinel节点发现某实例未在规定时间内响应,则认为该实例主观下线
- 客观下线:若超过指定数量(quorum)的Sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一般
1.3.2、选举新的master
一旦发现master故障,Sentinel需要在slave中选择一个作为新的master,选择依据是这样的:
- 首先判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
- 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
- 如果slave-priority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
- 最后是判断slave节点的运行id大小,越小优先级越高
1.3.3、如何实现故障转移
当选中了其中一个slave为新的master后,故障转移的步骤如下:
- Sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
- Sentinel给所有其他slave发送slaveof 192.168.150.101 7002命令,让这些slave成为新的master从节点,开始从新的master上同步数据
- 最后,Sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
总结:
1、Sentinel的三个作用是什么?
- 监控
- 故障转移
- 通知
2、Sentinel如何判断一个redis实例是否健康?
- 每隔1秒发送一次ping命令,如果超过一定时间没有响应则认为是主观下线
- 如果大多数Sentinel都认为实例主观下线,则判定服务下线
3、故障转移步骤有哪些?
- 首先选定一个slave作为新的master,执行slaveof no one
- 然后让所有节点都执行slaveof 新master
- 修改故障节点,执行slaveof 新master
1.4、搭建哨兵集群
首先,停掉之前的redis集群:
# 老版本DockerCompose
docker-compose down
# 新版本Docker
docker compose down
然后,配置文件sentinel.conf文件:
sentinel announce-ip "192.168.150.101"
sentinel monitor hmaster 192.168.150.101 7001 2
sentinel down-after-milliseconds hmaster 5000
sentinel failover-timeout hmaster 60000
说明:
sentinel announce-ip "192.168.150.101"
:声明当前sentinel的ipsentinel monitor hmaster 192.168.150.101 7001 2
:指定集群的主节点信息hmaster
:主节点名称,自定义,任意写192.168.150.101 7001
:主节点的ip和端口2
:认定master
下线时的quorum
值
sentinel down-after-milliseconds hmaster 5000
:声明master节点超时多久后被标记下线sentinel failover-timeout hmaster 60000
:在第一次故障转移失败后多久再次重试
我们在虚拟机的/root/redis
目录下新建3个文件夹:s1
、s2
、s3
:
将sentinel.conf文件分别拷贝一份到3个文件夹中
接着修改docker-compose.yaml文件,内容如下:
version: "3.2"
services:
r1:
image: redis
container_name: r1
network_mode: "host"
entrypoint: ["redis-server", "--port", "7001"]
r2:
image: redis
container_name: r2
network_mode: "host"
entrypoint: ["redis-server", "--port", "7002", "--slaveof", "192.168.150.101", "7001"]
r3:
image: redis
container_name: r3
network_mode: "host"
entrypoint: ["redis-server", "--port", "7003", "--slaveof", "192.168.150.101", "7001"]
s1:
image: redis
container_name: s1
volumes:
- /root/redis/s1:/etc/redis
network_mode: "host"
entrypoint: ["redis-sentinel", "/etc/redis/sentinel.conf", "--port", "27001"]
s2:
image: redis
container_name: s2
volumes:
- /root/redis/s2:/etc/redis
network_mode: "host"
entrypoint: ["redis-sentinel", "/etc/redis/sentinel.conf", "--port", "27002"]
s3:
image: redis
container_name: s3
volumes:
- /root/redis/s3:/etc/redis
network_mode: "host"
entrypoint: ["redis-sentinel", "/etc/redis/sentinel.conf", "--port", "27003"]
直接运行命令,启动集群:
docker-compose up -d
运行结果:
我们以s1节点为例,查看其运行日志:
# Sentinel ID is 8e91bd24ea8e5eb2aee38f1cf796dcb26bb88acf
# +monitor master hmaster 192.168.150.101 7001 quorum 2
* +slave slave 192.168.150.101:7003 192.168.150.101 7003 @ hmaster 192.168.150.101 7001
* +sentinel sentinel 5bafeb97fc16a82b431c339f67b015a51dad5e4f 192.168.150.101 27002 @ hmaster 192.168.150.101 7001
* +sentinel sentinel 56546568a2f7977da36abd3d2d7324c6c3f06b8d 192.168.150.101 27003 @ hmaster 192.168.150.101 7001
* +slave slave 192.168.150.101:7002 192.168.150.101 7002 @ hmaster 192.168.150.101 7001
可以看到sentinel
已经联系到了7001
这个节点,并且与其它几个哨兵也建立了链接。哨兵信息如下:
27001
:Sentinel ID
是8e91bd24ea8e5eb2aee38f1cf796dcb26bb88acf
27002
:Sentinel ID
是5bafeb97fc16a82b431c339f67b015a51dad5e4f
27003
:Sentinel ID
是56546568a2f7977da36abd3d2d7324c6c3f06b8d
2、Redis分片集群
2.1、搭建分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
- 集群有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping检测彼此健康状态
Redis分片集群最少也需要3个master节点,由于我们的机器性能有限,我们只给每个master配置1个slave,形成最小的分片集群:
计划部署的节点信息如下:
2.1.1、集群配置
分片集群中的Redis节点必须开启集群模式,一般在配置文件中添加下面参数:
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
中有3个我们没见过的参数:
cluster-enabled
:是否开启集群模式cluster-config-file
:集群模式的配置文件名称,无需手动创建,由集群自动维护cluster-node-timeout
:集群中节点之间心跳超时时间
一般搭建部署集群肯定是给每个节点都配置上述参数,不过考虑到我们计划用docker-compose
部署,因此可以直接在启动命令中指定参数,偷个懒。
在虚拟机的/root
目录下新建一个redis-cluster
目录,然后在其中新建一个docker-compose.yaml
文件,内容如下:
version: "3.2"
services:
r1:
image: redis
container_name: r1
network_mode: "host"
entrypoint: ["redis-server", "--port", "7001", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r2:
image: redis
container_name: r2
network_mode: "host"
entrypoint: ["redis-server", "--port", "7002", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r3:
image: redis
container_name: r3
network_mode: "host"
entrypoint: ["redis-server", "--port", "7003", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r4:
image: redis
container_name: r4
network_mode: "host"
entrypoint: ["redis-server", "--port", "7004", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r5:
image: redis
container_name: r5
network_mode: "host"
entrypoint: ["redis-server", "--port", "7005", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
r6:
image: redis
container_name: r6
network_mode: "host"
entrypoint: ["redis-server", "--port", "7006", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
注意:使用Docker部署Redis集群,network模式必须采用host
2.1.2、启动集群
进入/root/redis-cluster
目录,使用命令启动redis:
docker-compose up -d
启动成功,可以通过命令查看启动进程:
ps -ef | grep redis
# 结果:
root 4822 4743 0 14:29 ? 00:00:02 redis-server *:7002 [cluster]
root 4827 4745 0 14:29 ? 00:00:01 redis-server *:7005 [cluster]
root 4897 4778 0 14:29 ? 00:00:01 redis-server *:7004 [cluster]
root 4903 4759 0 14:29 ? 00:00:01 redis-server *:7006 [cluster]
root 4905 4775 0 14:29 ? 00:00:02 redis-server *:7001 [cluster]
root 4912 4732 0 14:29 ? 00:00:01 redis-server *:7003 [cluster]
可以发现每个redis节点都以cluster模式运行。不过节点与节点之间并未建立连接。
接下来,我们使用命令创建集群:
# 进入任意节点容器
docker exec -it r1 bash
# 然后,执行命令
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:7004 192.168.150.101:7005 192.168.150.101:7006
命令说明:
redis-cli --cluster
:代表集群操作命令create
:代表是创建集群--cluster-replicas 1
:指定集群中每个master的副本个数为1- 此时节点总数 / (replicas + 1)得到的就是master的数量n。因此节点列表中的前n个节点就是master,其它节点都是slave节点,随机分配到不同master
输入命令后控制台会弹出下面的信息:
这里展示了集群中master与slave节点分配情况,并询问你是否同意,节点信息如下:
7001
是master
,节点id
后6位是da134f
7002
是master
,节点id
后6位是862fa0
7003
是master
,节点id
后6位是ad5083
7004
是slave
,节点id
后6位是391f8b
,认ad5083
(7003)为master
7005
是slave
,节点id
后6位是e152cd
,认da134f
(7001)为master
7006
是slave
,节点id
后6位是4a018a
,认862fa0
(7002)为master
输入yes然后回车吗。会发现集群开始创建,并输出下列信息:
接着,我们可以通过命令查看集群状态:
redis-cli -p 7001 cluster nodes
结果:
2.2、散列插槽
在Redis集群中,共有16384个hash slots,集群中的每一个master节点都会分配一定数量的hash slots:
Redis数据不是与节点绑定,而是与插槽slot绑定。当我们读写数据时,Redis基于CRC16算法对key做hash运算1,得到的结果与16384取余,就计算出这个key的slot值。然后到slot所在的Redis节点执行读写操作.
redis在计算key的hash值是不一定是根据整个key计算,分两种情况:
- 当key中包含{}中,根据{}之间的字符串计算hash slot
- 当key中不包含{}时,则根据整个key字符串计算hash slot
例如:key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。
总结:
Redis如何判断某个key应该在哪个实例?
- 将16384个插槽分配到不同的实例
- 根据key的有效部分计算哈希值,对16384取余
- 余数作为插槽,寻找插槽所在实例即可
3、Redis数据结构
3.1、RedisObject
Redis中的任意数据类型的值和键都会被封装为一个RedisObject,也叫做Redis对象,源码如下:
Redis中会根据存储的数据类型不同,选择不同的编码方式,共包含12种不同类型:
Redis中会根据存储的数据类型不同,选择不同的编码方式。每种数据类型的使用的编码方式如下:
3.2、SkipList
**SkipList(跳表)**首先是链表,但与传统链表相比有几点差异:
- 元素按照升序排列存储
- 节点可能包含多个指针,指针跨度不同。
SkipList的特点:
- 跳跃表是一个有序的双向链表
- 每个节点都可以包含多层指针,层数是1到32之间的随机数
- 不同层指针到下一个节点的跨度不同,层数越高,跨度越大
- 增删改查效率与红黑树基本一致,实现却更简单。但空间复杂度更高
3.3、SortedSet
SortedSet数据结构的特点是:
- 每组数据都包含score和member
- member唯一
- 可根据score排序
总结:SortedSet的底层数据结构是怎样的?
- 首先SortedSet需要能存储score和member值,而且要快捷的根据member查询score,因此底层有一个哈希白哦,以member为键,以score为value
- 其次SortedSet还需要能根据score排序,因此底层还维护了一个跳表
- 当需要根据member查询score时,就去哈希表中查询
- 当需要根据score排序查询时,则基于跳表查询
4、Redis内存回收
4.1、过期KEY处理
Redis提供了expire命令,给key设置TTL(存活时间):
可以发现,当key的TTL到期以后,再次访问name返回的是null,说明这个key已经不存在了,对应的内存也得到了释放。从而起到内存回收的目的。
这里有两个问题需要思考:
1、Redis是如何知道一个key是否过期呢?
Redis的本身是键值型数据库,其所有数据都存在一个redisDB的结构体中,其中包含两个哈希表:
- dict:保存Redis中所有的键值对
- expires:保存Redis中所有的设置了过期时间的KEY及其到期时间(写入时间+TTL)
2、是不是TTl到期就立即删除了呢?
Redis并不会实时监测key的过期时间,在key过期后立刻删除,而是采用两种延迟删除的策略:
- 惰性删除:当有命令需要操作一个key的时候,检查该key的存活时间,如果已经过期才执行删除
- 周期删除:通过一个定时任务,周期型的抽样部分有TTL的key,如果过期则执行删除
周期删除的定时任务执行周期有两种:
- SLOW模式:默认执行频率为每秒10次,但每次执行时长不能超过25ms,瘦server.hz参数影响
- FAST模式:频率不固定,跟随Redis内部IO时间循环执行。两次任务之间间隔不低于2ms,执行时长不超过1ms
4.2、内存淘汰策略
内存淘汰:就是当Redis内存使用达到设置的阈值时,Redis主动挑选部分key删除以释放更多内存的流程
Redis会在每次处理客户端命令时都会对内存使用情况做判断,如果必要则执行内存淘汰。内存淘汰的策略有:
- negeviction:不淘汰任何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,基于LFU算法进行淘汰
比较容易混淆的有两个:
- LRU(Least Recently Used),最近最少使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高
- LFU(Least Frequently Used),最少频率使用,会统计每个key的访问频率,值越小淘汰优先级越高
5、Redis缓存
5.1、缓存一致性
缓存一致性策略的最佳实践方案:
- 低一致性需求:使用Redis的key过期清理方案
- 高一致性需求:主动更新,并以超时剔除作为兜底方案
- 读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
- 写操作:
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
- 读操作:
5.2、缓存穿透
缓存穿透是指客户端请求的数据在数据库中根本不存在,从而导致请求穿透缓存,直接打到数据库的问题。
常见的解决方案有两种:
- 缓存空对象(最常见)
- 优点:实现简单,维护方便
- 缺点:额外消耗缓存
- 布隆过滤
- 布隆过滤式一种数据统计的算法,用于检索一个元素是否存在一个集合中。但是布隆过滤无需存储元素到集合,而是把元素映射到一个很长的二进制数位上。
- 首先需要一个很长的二进制数,默认每一位都是0
- 然后需要N个不同算法的哈希函数
- 将集合中的元素根据N个哈希函数做运算,得到N个数字,然后将每个数字对应的bit位标记为1
- 要判断某个元素是否存在,只需要把元素按照上述方式运算,判断对应的bit位是否为1即可
- 优点:内存占用较少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能(它判断不存在一定不存在,它判断存在不一定存在)
- 布隆过滤式一种数据统计的算法,用于检索一个元素是否存在一个集合中。但是布隆过滤无需存储元素到集合,而是把元素映射到一个很长的二进制数位上。
5.3、缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存(浏览器缓存、Nginx缓存(更新频率低)、JVM本地缓存、Redis缓存、数据库)
5.4、缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效了,无效的请求访问会在瞬间给数据库带来巨大的冲击
常见解决方案有两种;
- 互斥锁
- 优点:
- 没有额外的内存消耗
- 保持一致性
- 实现简单
- 缺点:
- 线程需要等待,性能受影响
- 可能会有死锁的风险
- 优点:
- 逻辑过期
- 优点:
- 线程无需等待,性能较好
- 缺点:
- 不保证一致性
- 有额外内存消耗
- 实现复杂
- 优点: