转载于:https://mp.weixin.qq.com/s/qvXm1pU8T_2mCZCjkTR7QA
1 Redis常见面试问题
1.1 Redis是单线程还是多线程
Redis
不同版本之间采用的线程模型是不一样的,在Redis4.0
版本之前使用的是单线程模型
,在4.0
版本之后增加了多线程的支持。
在4.0
之前虽然说Redis
是单线程,也只是说它的网络I/O
线程以及Set
和 Get
操作是由一个线程完成的。但是Redis
的持久化
、集群同步
还是使用其他线程来完成。
4.0之后添加了多线程
的支持,主要是体现在大数据的异步删除
功能上,例如 unlink key、flushdb async、flushall async
等
1.2 使用单线程原因
那为什么Redis在4.0之前会选择使用单线程?而且使用单线程还那么快?
选择单线程主要是使用简单,不存在锁竞争
,可以在无锁的情况下完成所有操作,不存在死锁和线程切换带来的性能和时间上的开销,但同时单线程也不能完全发挥出多核CPU性能
为什么单线程那么快主要有以下几个原因:
-
Redis
的大部分操作都在内存中完成,内存中的执行效率本身就很快,并且采用了高效的数据结构,比如哈希表和跳表。 - 使用单线程避免了多线程的竞争,省去了多线程切换带来的时间和性能开销,并且不会出现死锁。
- 采用
I/O
多路复用机制处理大量客户端的Socket
请求,因为这是基于非阻塞的 I/O 模型,这就让Redis
可以高效地进行网络通信,I/O的读写流程也不再阻塞。 - 很多的客户端连接先到linux中的内核,内核和redis中间使用的是epoll(非阻塞的多路复用),那些进程一笔一笔的进行的。
在分布式情况下,这个数据一致性很重要。每个连接里边命令是顺序到达、顺序处理的,但是如果说里面有个key,这个key为a,那么两个客户端发了一个对a的操作,那么无论从网络当中跳跃谁先到达的,或者指定谁先轮到谁了。那么其实这两个人对一个的操作,很难判定是谁先谁后,但是如果是你一个人,它里边线性,而且没有使用多线程,线程还是安全的,虽然它可以有多线程,但是线程安全,对a的操作,这边能控制住,先创建a再删除a,只要这边能操作的话,那么这个数据是可以保证的。如果是单线程,这个客户端就是一个线程,就是一个socket里面也是一个线程,那么这个线程肯定是先发出一个创建a再发出一个删除a,但是如果客户端里边是多线程,那么这里一个创建命令和删除命令,指不定谁跑到前面了,如果线程不是安全的话,那么有可能先把删除的发出去,再把创建的发出去。
点击了解Linux中epoll原理机制
1.3 Redis高可用
Redis实现高可用主要有三种方式:主从复制
、哨兵模式
,以及 Redis 集群
1.3.1 主从复制
将从前的一台 Redis
服务器,同步数据到多台从 Redis
服务器上,即一主多从的模式,这个跟MySQL
主从复制的原理一样。
点击了解redis 持久化中的主从复制同步
1.3.2 哨兵模式
使用 Redis
主从复制的时候,会有一个问题,就是当 Redis
的主从服务器出现故障宕机时,需要手动进行恢复,为了解决这个问题,Redis
增加了哨兵模式(因为哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能)。
它专注于对 Redis 实例
(主节点、从节点)运行状态的监控,并能够在主节点发生故障时通过一系列的机制实现选主
及主从切换
,实现自动故障转移,确保整个 Redis 系统的可用性。
sentinel
主要做四件事情:
- 监控
master
和slave
状态,判断是否下线。
每秒一次的频率向master
和slave
以及其他sentinel
发送PING
命令,如果该节点距离最后一次响应 PING 的时间超过down-after-milliseconds
选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线,当master
被标记主观下线。
其他正在监视这个master
的所有sentinel
会按照每秒一次的频率确认master
是否主观下线。
当足够多的sentinel
都认为master
主观下线,则标记这个master
客观下线。 - 选举新
master
,如果 master 出现故障,sentine 需要选举一个 slave 晋升为新 master。晋升为新 master 的 slave 是有条件的,先过滤不满足条件的,再打分排优先级。-
slave
优先级,通过replica-priority 100
配置,值越低,优先级越高。 - 复制偏移量(
processed replication offset
),已复制的数据量越多越好,slave_repl_offset
与master_repl_offset
差值越小。 -
slave runID
,在优先级和复制进度都相同的情况下,runID
最小的 slave 得分最高,会被选为新主库。 - 过滤掉下线、网络异常的
slave
。 - 过滤掉经常与
master
断开的slave
。
-
- 选举领导者哨兵(Leader Sentinel),执行主从切换,从
sentinel
集群中选举一个leader
执行故障自动切换。
第一个判定master
主观下线的sentinel
收到其他sentinel
节点的回复并确定master
客观下线后,就会给其他sentinel
节点发送命令申请成为 leader。
选举领导者哨兵而不是直接从从节点中选举新的主节点,主要是为了以下原因:- 协调一致性:通过选举领导者哨兵,可以确保在集群中仅有一个哨兵负责主从切换,避免多个哨兵同时进行切换操作导致的不一致性。
- 集中决策:领导者哨兵集中管理整个主从切换过程,使得切换过程更加有序和可控。
- 分担职责:哨兵负责监控和管理 Redis 节点,而从节点主要用于数据复制和故障切换。在发生故障时,选举领导者哨兵来管理切换过程,可以让从节点专注于数据同步,分担系统负载
成为leader
的条件是收到的赞成票大于等于quorum
的值且赞半数以上。
- 通知,通知其他
slave
执行replicaof
与新的master
同步数据,并通知客户端与新 master 建立连接。
1.3.3 Redis Cluster(集群)
哨兵模式
基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此,Reids Cluster
集群(切片集群的实现方案)应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它可以保存大量数据,即分散数据到各个Redis实例,还提供复制和故障转移的功能。
Redis Cluster
是一种分布式去中心化的运行模式,是在 Redis 3.0 版本中推出的 Redis 集群方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis
服务的读写性能。
使用哨兵模式在数据上有副本数据做保证,在可用性上又有哨兵监控,一旦master
宕机会选举salve
节点为master
节点,那为什么还需要使用集群模式呢?
哨兵模式归根节点还是主从模式
,在主从模式下我们可以通过增加salve
节点来扩展读并发能力,但是没办法扩展写能力和存储能力,存储能力只能是master
节点能够承载的上限。所以为了扩展写能力和存储能力,我们就需要引入集群模式。
集群中那么多Master
节点,redis cluster
在存储的时候如何确定选择哪个节点呢?
Redis Cluster
采用的是数据分片
实现节点选择的
1.4 Redis内存(数据)淘汰策略
在redis
中,我们是可以去设置最大使用内存大小server.maxmemory
的,当redis
内存数据集大小上升到一定程度的时候,就会施行数据淘汰机制。
不同位数的操作系统,maxmemory
的默认值是不同的:
- 在 64 位操作系统中,
maxmemory
的默认值是 0,表示没有内存大小限制,那么不管用户存放多少数据到Redis
中,Redis
也不会对可用内存进行检查,直到 Redis 实例因内存不足而崩溃也无作为。 - 在 32 位操作系统中,
maxmemory
的默认值是3G
,因为 32 位的机器最大只支持4GB
的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位操作系统限制最大 3 GB 的可用内存是非常合理的,这样可以避免因为内存不足而导致Redis
实例崩溃。
Redis
提供了8种数据淘汰策略,分为不进行数据淘汰
和进行数据淘汰
两类策略,
不进行数据淘汰的策略 noeviction
(Redis3.0
之后,默认的内存淘汰策略),它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,会报错通知禁止写入,不淘汰任何数据,但是如果没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。
-
no-enviction
:禁止淘汰数据,如果redis写满了将不提供写请求,直接返回错误
进行数据淘汰的策略:
-
volatile-lru
:从已经设置过期时间的数据集中,挑选最近最少使用的数据淘汰,即:最久未使用的键值 -
volatile-ttl
:从已经设置过期时间的数据集中,挑选即将要过期的数据淘汰。 -
volatile-random
:从已经设置过期时间的数据集中,随机挑选数据淘汰。 -
volatile-lfu
:从已经设置过期时间的数据集中,会使用LFU
算法选择设置了过期时间的键值对,即:最不常用的键值 -
allkeys-lru
:从所有的数据集中,挑选最近最少使用的数据淘汰。 -
allkeys-random
:从所有的数据集中,随机挑选数据淘汰。 -
allkeys-lfu
:淘汰整个键值中最不常用的键值
附录:LRU
和LFU
是不同的:
-
LRU
是最近最少使用页面置换算法(Least Recently Used
),也就是首先淘汰最长时间未被使用的页面 -
LFU
是最近最不常用页面置换算法(Least Frequently Used
),也就是淘汰一定时期内被访问次数最少的页
使用策略规则:
- 如果数据呈现
幂律分布
,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
- 如果数据呈现
平等分布
,也就是所有的数据访问频率都相同,则使用allkeys-random
1.5 Redis过期键删除策略
Redis
过期键删除策略:
-
定时删除
:在设置键的过期时间的同时,创建一个timer
,让定时器在键的过期时间到达时,立即执行对键的删除操作。(主动删除)
对内存友好,但是对cpu时间不友好,有较多过期键的而情况下,删除过期键会占用相当一部分cpu时间。 -
惰性删除
:放任过期键不管,但是每次从键空间中获取键时,都检查取到的键是否过去,如果过期就删除,如果没过期就返回该键。(被动删除)
对cpu
时间友好,程序只会在取出键的时候才会对键进行过期检查,这不会在删除其他无关过期键上花费任何cpu时间,但是如果一个键已经过期,而这个键又保留在数据库中,那么只要这个过期键不被删除,他所占用的内存就不会释放,对内存不友好。 -
定期删除
:每隔一段时间就对数据库进行一次检查,删除里面的过期键。(主动删除)采用对内存
和cpu
时间折中的方法,每隔一段时间就对一些key
进行采样检查,检查是否过期,如果过期就进行删除
1、采样一定个数的key
,采样的个数可以进行配置,并将其中过期的key
全部删除;
2、如果过期key
的占比超过可接受的过期key
的百分比,则重复删除的过程,直到过期key
的比例降至可接受的过期key
的百分比以下
1.6 Redis的key和value可以存储的最大值分别是多少
虽然Key
的大小上限为512M
,但是一般建议key
的大小不要超过1KB
,这样既可以节约存储空间,又有利于Redis
进行检索。
value
的最大值也是512M
。对于String
类型的value
值上限为512M
,而集合、链表、哈希等key
类型,单个元素的value
上限也为512M
1.7 Redis实现数据的去重
-
Redis
的set
:它可以去除重复元素,也可以快速判断某一个元素是否存在于集合中,如果元素很多(比如上亿的计数),占用内存很大。 -
Redis
的bit
:它可以用来实现比set内存高度压缩的计数,它通过一个bit
设置为1
或者0
,表示存储某个元素是否存在信息。例如网站唯一访客计数,可以把user_id
作为bit
的偏移量offset
,如设置为1
表示有访问,使用1 MB
的空间就可以存放800多万用户的一天访问计数情况。 -
HyperLogLog
:实现超大数据量精确的唯一计数都是比较困难的,HyperLogLog
可以仅仅使用 12 k左右的内存,实现上亿的唯一计数,而且误差控制在百分之一左右。 -
bloomfilter
布隆过滤器:布隆过滤器是一种占用空间很小的数据结构,它由一个很长的二进制向量和一组Hash映射函数组成,它用于检索一个元素是否在一个集合中
1.8 Redis序列化
Redis
什么时候需要序列化?
-
序列化
:将Java
对象转换成字节流的过程。 -
反序列化
:将字节流转换成Java
对象的过程。
为什么需要序列化呢?
打个比喻:作为大城市漂泊的码农,搬家是常态。当我们搬书桌时,桌子太大了就通不过比较小的门,因此我们需要把它拆开再搬过去,这个拆桌子的过程就是序列化。而我们把书桌复原回来(安装)的过程就是反序列化啦。
比如想把内存中的对象状态保存到一个文件中或者数据库中的时候(最常用,如保存到redis
);再比喻想用套接字在网络上传送对象的时候,都需要序列化。
RedisSerializer
接口 是 Redis
序列化接口,用于 Redis KEY
和 VALUE
的序列化,有如下序列化方式:
-
JDK
序列化方式 (默认) -
String
序列化方式 -
JSON
序列化方式 -
XML
序列化方式
1.9 大key
1.9.1 定义
Redis
中的 大key
是指存储在Redis
中的占用内存较大的键值对。大key
可能会导致Redis
的性能下降,因为大key
占用的内存较多,需要较长的时间来进行读写操作。而且,当大key
被删除时,会阻塞Redis
的其他操作。
常见的大key包括:
- 存储大量数据的字符串类型键值对。
- 存储大量元素的列表、集合或有序集合。
- 包含大量字段的哈希表。
1.9.2 大Key解决方案
为了避免大key
对Redis
性能的影响,可以采取以下措施:
- 将
大key
拆分为多个较小的键值对,以减少每个键值对的内存占用。 - 使用分布式缓存,将
大key
分散到多个Redis
实例上。 - 使用压缩算法对
大key
进行压缩,减少内存占用。 - 使用
Redis
的分片功能,将大key
分散到多个分片上,减少单个Redis
实例的负载。 - 对
大Key
进行清理。将不适用Redis能力的数据存至其它存储,并在Redis中删除此类数据。注意,要使用异步删除。 - 监控Redis的内存水位。可以通过监控系统设置合理的Redis内存报警阈值进行提醒,例如Redis内存使用率超过70%、Redis的内存在1小时内增长率超过20%等。
- 对过期数据进行定期清。堆积大量过期数据会造成
大Ke
y的产生,例如在HASH数据类型中以增量的形式不断写入大量数据而忽略了数据的时效性。可以通过定时任务的方式对失效数据进行清理。
1.10 热Key
1.10.1 定义
Redis热key
是指在Redis
中被频繁访问的键。当某个键被频繁访问时,它就被认为是热key
。热key
通常是由于某些热门操作、热门数据或者高并发访问所导致的
通常以其接收到的 Key
被请求频率来判定,例如:
-
QPS
集中在特定的Key:Redis实例的总QPS(每秒查询率)为10,000,而其中一个Key的每秒访问量达到了7,000。 - 带宽使用率集中在特定的Key:对一个拥有上千个成员且总大小为1 MB的HASH Key每秒发送大量的HGETALL操作请求。
- CPU使用时间占比集中在特定的Key:对一个拥有数万个成员的Key(ZSET类型)每秒发送大量的ZRANGE操作请求。
1.10.2 如何解决热key
热key
可能对Redis
的性能产生重大影响。当一个键被频繁访问时,Redis
需要频繁地从内存中读取或写入该键的值,这可能导致内存带宽的瓶颈、CPU利用率的增加以及延迟的增加。此外,如果一个热key的值过大,可能会占用大量的内存,进一步影响Redis的性能
解决方案:
- 在
Redis
集群架构中对热Key
进行复制,数据分片
在Redis
集群架构中,由于热Key
的迁移粒度问题,无法将请求分散至其他数据分片,导致单个数据分片的压力无法下降。此时,可以将对应热Key进行复制并迁移至其他数据分片,例如将热Key foo复制出3个内容完全一样的Key并名为foo2、foo3、foo4,将这三个Key迁移到其他数据分片来解决单个数据分片的热Key压力。 - 使用读写分离架构。
如果热Key
的产生来自于读请求,您可以将实例改造成读写分离架构来降低每个数据分片的读请求压力,甚至可以不断地增加从节点。但是读写分离架构在增加业务代码复杂度的同时,也会增加Redis集群架构复杂度。不仅要为多个从节点提供转发层(如Proxy,LVS等)来实现负载均衡,还要考虑从节点数量显著增加后带来故障率增加的问题。Redis集群架构变更会为监控、运维、故障处理带来了更大的挑战。 - 使用合适的数据结构
根据实际情况选择合适的数据结构,如列表、集合、有序集合等,来存储热key的值,以提高读写性能。 - 缓存策略
使用合理的缓存策略,比如设置合适的过期时间、使用LRU算法等,以减少对热key的访问频率。 - 持久化策略
根据业务需求选择合适的持久化方式,如RDB快照、AOF日志等,以确保数据的安全性和可靠性。
1.11 Redis中缓冲区
Redis中的缓冲区主要根据其功能和用途进行划分,可以归纳为以下几种:
- 客户端缓冲区:
- 输入缓冲区:缓存客户端发送过来的命令,
Redis
主线程从该缓冲区中读取命令进行处理。 - 输出缓冲区:当
Redis
主线程处理完数据后,将结果写入该缓冲区,再返回给客户端。 - 缓存区溢出原因:
- 写入了BigKey,即一次性写入了大量数据,超过了缓冲区的大小。
- 服务端处理请求的速度过慢,导致无法及时处理请求,使得客户端发送的请求在缓冲区内越积越多。
- 输入缓冲区:缓存客户端发送过来的命令,
- 复制缓冲区:
- 复制缓冲区:在全量复制过程中,主节点在向从节点传输
RDB
文件的同时,会继续接收客户端发送的写命令请求。这些写命令会先保存在复制缓冲区中,等RDB
文件传输完成后,再发送给从节点去执行。 - 复制积压缓冲区:在增量复制时,主节点和从节点进行常规同步时,会把写命令暂存在
复制积压缓冲区
中。 - 溢出原因:
- 主库传输RDB文件以及从库加载RDB文件耗时长,同时主库接收的写命令操作较多。
- 缓冲区大小设置不合理。
- 复制缓冲区:在全量复制过程中,主节点在向从节点传输
- AOF缓冲区:
- AOF缓冲区:当
Redis
进行持久化时,会先将客户端传来的命令存放在AOF
缓冲区,再根据具体的策略(always、everysec、no)去写入磁盘中的AOF文件中。 - AOF重写缓冲区:在
Redis
进行AOF
重写时,主进程会fork一个子进程进行AOF
重写,此时主进程接收的指令会存放在AOF重写缓冲区中。当AOF重写完成后,这些指令会被追加到AOF文件中。
- AOF缓冲区:当
- 内存级缓存:
虽然不是严格意义上的缓冲区
,但Redis
的内存级缓存是其最常见的使用场景。通过将数据存储在内存中,减少读取数据库的频率,提高数据访问速度。 - 其他内存使用:
-
Redis
空进程自身内存消耗非常少,通常used_memory_rss
在3MB左右,used_memory在800KB左右。 - 对象内存:存储着用户所有的数据,消耗可以简单理解为sizeof(keys)+sizeof(values)。
- 内存碎片:正常的碎片率在1.03左右
-