共计 6898 个字符,预计需要花费 18 分钟才能阅读完成。
又到了秋招跳槽季,好多同学曾经开始口头了。明天我来助力一把,送出这套 Redis 面试题,助力大家通关。心愿可能帮忙到正在筹备面试的你,节俭你上网搜寻的工夫!
Redis 为什么响应快
1. 数据保留在内存中
Redis 数据保留在内存中,读写操作只有拜访内存,不须要磁盘 IO。
2. 底层数据结构
如下:
- Redis 的数据以 key:value 的格局存储在散列表中,工夫复杂度 o(1)。
- Redis 为 value 定义了丰盛的数据结构,包含动静字符串、双向链表、压缩列表、hash、跳表和整数数组,能够依据 value 的个性抉择抉择最高效的数据结构。
3. 单线程模型
Redis 的网络 IO 和数据读写应用单线程模型,能够绑定 CPU,这防止了线程上下文切换带来的开销。
留神:Redis 6.0 对网络申请引入了多线程模型,读写操作还是用单线程。
Redis 多线程网络模型见下图:
5.IO 多路复用
Redis 采纳 Epoll 网络模型,如下图:
内核会始终监听新的 socket 连贯事件的和已建设 socket 连贯的读写事件,把监听到的事件放到事件队列,Redis 应用单线程不停的解决这个事件队列。这防止了阻塞期待连贯和读写事件到来。
这些事件绑定了回调函数,会调用 Redis 的处理函数进行解决。
Redis 底层数据结构
1.Redis 有 5 种数据类型,包含:字符串、列表、汇合、有序汇合和字典。
2.Redis 底层的数据结构有 6 种,包含:动静字符串、双向链表、压缩列表(ziplist)、hash 表、跳表(skip list)和整数数组。
Redis 数据类型和底层数据结构有如下对应关系:
1. 字符串类型
底层数据结构是动静字符串。
2. 列表
如果同时满足上面条件,就应用压缩列表,否则应用双向链表:
- 列表中单个元素小于 64 字节
- 列表中元素个数少于 512
压缩列表在内存中是一块儿间断的内存空间,构造如下:
压缩列表查找时间复杂度是 o(n)。
3. 汇合
如果同时满足上面条件,就应用有序整数数组,否则应用 hash 表:
- 汇合中元素都是整数类型
- 汇合中元素个数不超过 512 个
4. 有序汇合
如果同时满足上面 2 个条件,就应用压缩列表,否则应用跳表:
- 汇合中元素都小于 64 字节
- 汇合中元素个数小于 128 个
留神:有序汇合还有一个 HASH 表用于保留汇合中元素的分数,做 ZSCORE 操作时,查问的就是这个 HASH 表,所以效率很高。
跳表的构造如下:
如果不加索引,查找 10 这个数字须要查问 10 次,应用了二级索引,查找 10 这个数字须要 5 次,而应用一级索引,须要查问 3 次。
跳表的每一层都是一个有序链表,最上面一层保留了全副数据。跳表插入、删除、查问的工夫复杂度是 o(logN)。跳表须要存储额定的索引节点,会减少额定的空间开销。
5. 字典
如果同时满足上面 2 个条件,就应用压缩列表,否则应用 hash 表:
- 字典中每个 entry 的 key/value 都小于 64 字节
- 字典中元素个数小于 512 个
Redis 缓存淘汰策略
Redis 总共有 8 种淘汰策略,如下图:
volatile-lfu 和 allkeys-lfu 策略是 4.0 版本新增的:
- lru:是依照数据的最近起码拜访准则来淘汰数据,可能存在的问题是如果大批量冷数据最近被拜访了一次,就会占用大量内存空间,如果缓存满了,局部热数据就会被淘汰掉。
- lfu: 是依照数据的最小拜访频率拜访次数准则来淘汰数据,如果两个数据的拜访次数雷同,则把拜访工夫较早的数据淘汰。
Redis 数据长久化
Redis 长久化的形式有 2 种,一种是写后日志(AOF),一种是内存快照(RDB)。
1.AOF 日志
AOF 日志记录了每一条收到的命令,Redis 故障宕机复原时,能够加载 AOF 日志中的命令进行重放来进行故障复原。
AOF 有 3 种同步策略,如下图:
如果不是对失落数据特地敏感的业务,举荐应用 everysec,对主线程的阻塞少,故障后失落数据只有 1s。
2.RDB 快照
RDB 快照是一个内存快照,记录了 Redis 某一时刻的全副数据。
3. 混合日志
从 Redis 4.0 开始,AOF 文件也能够保留 RDB 快照,AOF 重写的时候 Redis 会把 AOF 文件内容清空,先记录一份 RDB 快照,这份数据以 ”REDIS” 结尾。
记录 RDB 内容后,AOF 文件会接着记录 AOF 命令。故障复原时,先加载 AOF 文件中 RDB 快照,而后回放 AOF 文件中前面的命令。
4. 主从同步
Redis 主从同步时,主节点会学生成一份 RDB 快照发送给从节点,把快照之后的命令写入主从同步缓存区(replication buffer),从节点把 RDB 文件加载实现后,主节点把缓存区命令发送给从节点。
5.AOF 重写
AOF 日志是用记录命令的形式追加的,这样可能存在对同一个 key 的多条命令,这些命令是能够合并成 1 条的。比方对同一个 key 的多个 set 操作日志,能够合成一条。
6. 阻塞点
AOF 重写和 RDB 快照执行的过程中,Redis 都会 Fork 一个子过程来执行操作,子过程执行过程中是不是阻塞主线程的。
然而要留神 2 点:
- Fork 子过程的过程中,Redis 主线程会拷贝一份内存页表(记录了虚拟内存和物理内存的映射关系)给子过程,这个过程是阻塞的,Redis 主线程内存越大,阻塞工夫越长。
- 子过程和 Redis 主线程共用一块儿物理内存,如果新的申请到来,必须应用 copy on write 的形式,拷贝要批改的数据页到新的内存空间进行批改。如下图:
留神:如果开启了内存大页,每次拷贝都须要调配 2MB 的内存。
Redis 高可用
下图是一个“一主二从三哨兵”的架构图:
从图咱们能够看到哨兵之间、哨兵和主从节点之间、哨兵和客户端之间都建设了连贯。
如果主节点挂了,哨兵集群须要实现主从切换,如下图:
上面咱们顺次来聊一下这 4 个步骤。
1. 判断主节点下线
当一个哨兵监控到主节点下线时,就会给其余哨兵发送确认命令,其余命令会依据本人的判断回复 ”Y” 或 ”N”。
如果有 n/2+1 以上数量的哨兵都认为主节点下线了,才会断定主节点下线。这里的 n 是哨兵集群的数量。
n/2+1 这个参数由 quorum 参数配置,比方有 5 个哨兵,这里个别配置成 3。也能够配置成其余值。
2. 选举新主节点
主节点被断定下线后,哨兵集群会从新抉择新的主节点。
淘汰不稳固从节点:依据配置参数 down-after-milliseconds * 10 来淘汰。
down-after-milliseconds 示意主从节点断开工夫,10 示意次数,如果从节点跟主节点断开工夫超过 down-after-milliseconds 的次数达到了 10 次以上,从节点就被淘汰了。
slave-priority 参数:配置了从节点的优先级,抉择从节点时哨兵会优先选择优先级高的从节点。
复制进度:Redis 有一个记录主从增量复制的缓存区叫 repl_backlog_buffer。
这是一个环形构造的缓冲区,如下图:
主节点有一个写偏移量 master_repl_offset,从节点也有一个偏移量 slave_repl_offset。
优先选择 slave_repl_offset 最靠近 master_repl_offset 的从节点作为新的主节点。
所以,上图中偏移量为 114 的从节点优先被选为新的主节点。
ID 编号:优先级和参数都一样的状况下,ID 编号小的从节点优先被选为新主节点。
3. 选举哨兵 Leader
第一个判断主节点下线的哨兵节点收到其余节点的回复并确定主节点下线后,就会给其余哨兵发送命令申请成为哨兵 Leader。
成为 Leader 的条件如下:
- 收到赞成票必须大于等 quorum 值
- 必须拿到半数以上的赞成票
如果集群配置了 5 个哨兵,quorum 的值设置为 3,其中一个哨兵节点挂了,很有可能会判断到主节点下线,然而因为选举不出哨兵 Leader 而不能切换。
如果集群有 2 个哨兵,其中一个挂了,那必然选不出哨兵 Leader。
上面的图展现了哨兵一胜利入选 Leader 的过程:
4. 主节点切换
选出新主节点和哨兵 Leader 后,哨兵 Leader 会执行主从切换的操作。
实现后会做一些事件告诉:
- 告诉其余哨兵新主节点地址
- 告诉所有从节点新的主节点地址,从节点收到后向新主节点申请主从同步
- 告诉客户端连贯新主节点
5. 主从切换过程中申请解决
如果客户端的读申请会发送到从节点,能够失常解决。在客户端收到新主节点地址告诉前写申请会失败。客户端能够采取一些应急措施应答主节点下线,比方缓存写申请。
为了可能及时获取到新主节点信息,客户端能够订阅哨兵的主节点下线事件和新主节点变更事件。
Redis 为什么变慢了
Redis 变慢了的起因有很多,总结一下有 11 个,见下图:
从图中看出,Redis 变慢起因次要有两类:阻塞主线程和操作系统限度。
1. 主线程阻塞
AOF 重写和 RDB 快照:后面曾经讲过了,Redis 在 AOF 重写时,主线程会 Fork 出一个 bgrewriteaof 子过程。Redis 进行 RDB 快照时主线程会 Fork 出一个 bgsave 子过程。
这两个操作外表上看不阻塞主线程,但 Fork 子过程的这个过程是在主线程实现的。
Fork 子过程时 Redis 须要拷贝内存页表,如果 Redis 实例很大,这个拷贝会消耗大量的 CPU 资源,阻塞主线程的工夫也会变长。
内存大页:Redis 默认反对内存大页是 2MB,应用内存大页,肯定水平上能够缩小 Redis 的内存调配次数,然而对数据长久化会有肯定影响。
Redis 在 AOF 重写和 RDB 快照过程中,如果主线程收到新的写申请,就须要 CopyOnWrite。
应用了内存大页,即便 Redis 只批改其中一个大小是 1kb 的 key,也须要拷贝一整页的数据,即 2MB。在写入量较多时,大量拷贝就会导致 Redis 性能降落。
命令复杂度高:执行复杂度高的命令是造成 Redis 阻塞的常见起因。比方对一个 set 或者 list 数据类型执行 SORT 操作,复杂度是 O(N+M*log(M))。
bigkey 操作:如果一个 key 的 value 十分大,创立的时候分配内存会很耗时,删除的时候开释内存也很耗时。
Redis 4.0 当前引入了 layfree 机制,能够应用子过程异步删除,从而不影响主线程执行。用 UNLINK 命令代替 DEL 命令,就能够应用子过程异步删除。
Redis 6.0 减少了配置项 lazyfree-lazy-user-del,配置成 yes 后,del 命令也能够用子过程异步删除。
如果 lazyfree-lazy-user-del 不设置为 yes,那 Redis 是否采纳异步删除,是要看删除的机会的。
对于 String 类型和底层采纳整数数组和压缩列表的数据类型,Redis 是不会采纳异步删除的。
从节点全量同步:从节点全量同步过程中,须要先革除内存中的数据,而后再加载 RDB 文件,这个过程中是阻塞的,如果有读申请到来,只能等到加载 RDB 文件实现后能力解决申请,所以响应会很慢。
另外,如果 Redis 实例很大,也会造成 RDB 文件太大,从库加载工夫长。所以尽量放弃 Redis 实例不要太大,比方单个实例限度 4G,如果超出就采纳切片集群。
AOF 同步写盘:appendfsync 策略有 3 种:always、everysec、no,如果采纳 always,每个命令都会同步写盘,这个过程是阻塞的,等写盘胜利后能力解决下一条命令。
除非是严格不能丢数据的场景,否则尽量不要抉择 always 策略,举荐尽量抉择 everysec 策略,如果对失落数据不敏感,能够采纳 no。
内存达到 maxmemory:须要应用淘汰策略来淘汰局部 key。即便采纳 lazyfree 异步删除,抉择 key 的过程也是阻塞的。
能够抉择较快的淘汰策略,比方用随机淘汰来替换 LRU 和 LFU 算法淘汰。也能够扩充切片数量来加重淘汰 key 的工夫耗费。
2. 操作系统限度
应用了 swap:应用 swap 的起因是操作系统不能给 Redis 调配足够大的内存,如果操作其余开启了 swap,内存数据就须要不停地跟 swap 换入和换出,对性能影响十分大。
操作系统没有能力分配内存的起因也可能是其余过程应用了大量的内存。
网络问题:如果网卡负载很大,对 Redis 性能影响会很大。这一方面有可能 Redis 的访问量的确很高,另一方面也可能是有其余流量大的程序占用了带宽。
这个最好从运维层面进行监控。
线程上下文切换:Redis 尽管是单线程的,然而在多核 CPU 的状况下,也可能会产生上下文切换。
如果主线程从一个物理核切换到了另一个物理核,那就不能应用 CPU 高效的一级缓存和二级缓存了。
如下图所示:
为避免这种状况,能够把 Redis 绑定到一个 CPU 物理核。
磁盘性能低:对于 AOF 同步写盘的应用场景,如果磁盘性能低,也会影响 Redis 的响应。能够优先采纳性能更好的 SSD 硬盘。
设计排行榜性能
Redis 的 zset 类型保留了分数值,能够不便的实现排行榜的性能。
比方要统计 10 篇文章的排行榜,能够先建设一个寄存 10 篇文章的 zset,每当有读者浏览一篇文章时,就用 ZINCRBY 命令给这篇文章的分数加 1,最初能够用 range 命令统计排行榜前几位的文章。
Redis 实现分布式锁
1.Redis 单节点的分布式锁
如下图,一个服务部署了 2 个客户端,获取分布式锁时一个胜利,另一个就失败了。
Redis 个别应用 setnx 实现分布式锁,命令如下:
SETNX KEY_NAME VALUE
设置胜利返回 1,设置失败返回 0。应用单节点分布式锁存在一些问题。
客户端 1 获取锁后产生了故障:后果锁就不能开释了,其余客户端永远获取不到锁。
解决办法是用上面命令对 key 设置过期工夫:
SET key value [EX seconds] [PX milliseconds] NX
客户端 2 误删除了锁:解决办法是对 key 设置 value 时退出一个客户端示意,比方在客户端 1 设置 key 时在 value 前拼接一个字符串 application1,删除的时候做一下判断。
2.Redis 红锁
Redis 单节点会有可靠性问题,节点故障后锁操作就会失败。Redis 为了应答单点故障的问题,设计了多节点的分布式锁,也叫红锁。
次要思维是客户端跟多个 Redis 实例申请加锁,只有超过半数的实例加锁胜利,才认为胜利获取了分布式锁。
如下图,客户端别离跟 3 个实例申请加锁,有 2 个实例加锁胜利,所以获取分布式锁胜利:
缓存雪崩、击穿、穿透
1. 缓存雪崩
Redis 做缓存时,如果同一时间大量缓存数据生效,客户端申请会大量发送到数据库,导致数据库压力激增。
如下图:
应答办法次要有 3 个:
- 给 key 设置过期工夫时加一个小的随机数
- 限流
- 服务降级
2. 缓存击穿
某个热点 key,忽然过期了,大量申请发送到了数据库。解决方案是给热点 key 不设置过期工夫。
3. 缓存穿透
某个热点 key,查问缓存和查询数据库都没有,就产生了缓存穿透。
如下图:
应答办法次要有 2 个:
- 缓存热点的空值和缺省值
- 查询数据库之前先查问布隆过滤器
数据歪斜
什么是数据歪斜?看上面这个面试题:如果 Redis 有一个热点 key,QPS 能达到 100w,该如何存储?
如果这个热点 key 被放到一个 Redis 实例上,这个实例面临的拜访压力会十分大。
如下图,redis3 这个实例保留了 foo 这个热点 key,拜访压力会很大:
解决办法次要有两个:
1. 应用客户端本地缓存来缓存 key。
这样革新会有两个问题:
- 客户端缓存的热点 key 可能耗费大量内存。
- 客户端须要保障本地缓存和 Redis 缓存的一致性。
2. 给热点 key 加一个随机前缀,让它保留到不同的 Redis 实例上。
这样也会存在两个问题:
- 客户端在拜访的时候须要给这个 key 加前缀
- 客户端在删除的时候须要依据所有前缀来删除不同实例上保留的这个 key
Bitmap 应用
有一道经典的面试题,10 亿整数怎么在内存中去重排序?
咱们先算一下 10 亿整数占的内存,Java 一个整数类型占四字节,占用内存大小约:
10 亿 * 4 / 1024 / 1024 = 3.7G
占得内存太大了,如果内存不够,怎么办呢?
1.Bitmap 介绍
Bitmap 类型应用的数据结构是 String,底层存储格局是二进制的 bit 数组。如果咱们有 1、4、6、9 四个数,保留在 bit 数组中如下图:
在这个 bit 数组中用 10 个 bit 的空间保留了四个整数,占用空间十分小。
再回到面试题,咱们应用 bit 数组长度是 10 亿整数中(最大值 - 最小值 +1)。
如果有正数,须要进行一个转化,所有数字加最小正数的绝对值。比方 {-2, 0, 1, 3},咱们转换成 {0, 2, 3, 5},因为数组下标必须从 0 开始。
2. 应用场景
员工打卡记录:在一个有 100 个员工的公司,要统计一个月内员工全勤的人数,能够每天创立一个 Bitmap,签到的员工 bit 地位为 1。
要统计当天签到的员工只有用 BITCOUNT 命令就能够。
要统计当月全勤的员工,只有对当月每天的 Bitmap 做交加运算就能够。
命令如下:
BITOP AND srckey1 srckey2 srckey3 ... srckey30
srckeyN 示意第 N 天的打卡记录 Bitmap。
统计网站日沉闷用户:比方网站有 10 万个用户,这样咱们创立一个长度为 10 万的 Bitmap,每个用户 id 占一个位,如果用户登录,就把 bit 地位为 1,日终的时候用 BITCOUNT 命令统计出当天登录过的用户总数。
最初
既然都看到这里了,那就点个赞吧!!