2021 年最新 Redis 面试题汇总
最近结合多位程序员朋友的面试反馈,抽时间整理了下 Redis 常见面试题,便于大家查漏补缺,完善知识点。
应用场景
缓存
共享 Session
消息队列系统
分布式锁
单线程的 Redis 为什么快
纯内存操作
单线程操作,避免了频繁的上下文切换
合理高效的数据结构
采用了非阻塞 I/O 多路复用机制(有一个文件描述符同时监听多个文件描述符是否有数据到来)
Redis 的数据结构及使用场景
String 字符串:字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型,而且 其他几种数据结构都是在字符串类型基础上构建的,我们常使用的 set key value 命令就是字符串。常用在缓存、计数、共享 Session、限速等。
Hash 哈希:在 Redis 中,哈希类型是指键值本身又是一个键值对结构,哈希可以用来存放用户信息,比如实现购物车。
List 列表(双向链表):列表(list)类型是用来存储多个有序的字符串。可以做简单的消息队列的功能。
Set 集合:集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
Sorted Set 有序集合(跳表实现):Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。
Redis 的数据过期策略
Redis 中数据过期策略采用定期删除+惰性删除策略
定期删除策略:Redis 启用一个定时器定时监视所有的 key,判断 key 是否过期,过期的话就删除。这种策略可以保证过期的 key 最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有的数据,非常消耗 CPU 资源,并且当 key 已过期,但是定时器还处于未唤起状态,这段时间内 key 仍然可以用。
惰性删除策略:在获取 key 时,先判断 key 是否过期,如果过期则删除。这种方式存在一个缺点:如果这个 key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间。
这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不再是每次扫描全部的 key 了,而是随机抽取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了未检查到的 key,基本上满足了所有要求。但是有时候就是那么的巧,既没有被定时器抽取到,又没有被使用,这些数据又如何从内存中消失?没关系,还有内存淘汰机制,当内存不够用时,内存淘汰机制就会上场。淘汰策略分为:当内存不足以容纳新写入数据时,新写入操作会报错。(Redis 默认策略)当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(LRU 推荐使用)当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。
Redis 的 set 和 setnx
Redis 中 setnx 不支持设置过期时间,做分布式锁时要想避免某一客户端中断导致死锁,需设置 lock 过期时间,在高并发时 setnx 与 expire 不能实现原子操作,如果要用,得在程序代码上显示的加锁。使用 SET 代替 SETNX ,相当于 SETNX+EXPIRE 实现了原子性,不必担心 SETNX 成功,EXPIRE 失败的问题。
Redis 的 LRU 具体实现:
传统的 LRU 是使用栈的形式,每次都将最新使用的移入栈顶,但是用栈的形式会导致执行 select *的时候大量非热点数据占领头部数据,所以需要改进。Redis 每次按 key 获取一个值的时候,都会更新 value 中的 lru 字段为当前秒级别的时间戳。Redis 初始的实现算法很简单,随机从 dict 中取出五个 key,淘汰一个 lru 字段值最小的。在 3.0 的时候,又改进了一版算法,首先第一次随机选取的 key 都会放入一个 pool 中(pool 的大小为 16),pool 中的 key 是按 lru 大小顺序排列的。接下来每次随机选取的 keylru 值必须小于 pool 中最小的 lru 才会继续放入,直到将 pool 放满。放满之后,每次如果有新的 key 需要放入,需要将 pool 中 lru 最大的一个 key 取出。淘汰的时候,直接从 pool 中选取一个 lru 最小的值然后将其淘汰。
Redis 如何发现热点 key
凭借经验,进行预估:例如提前知道了某个活动的开启,那么就将此 Key 作为热点 Key。
服务端收集:在操作 redis 之前,加入一行代码进行数据统计。
抓包进行评估:Redis 使用 TCP 协议与客户端进行通信,通信协议采用的是 RESP,所以自己写程序监听端口也能进行拦截包进行解析。
在 proxy 层,对每一个 redis 请求进行收集上报。
Redis 自带命令查询:Redis4.0.4 版本提供了 redis-cli –hotkeys 就能找出热点 Key。(如果要用 Redis 自带命令查询时,要注意需要先把内存逐出策略设置为 allkeys-lfu 或者 volatile-lfu,否则会返回错误。进入 Redis 中使用 config set maxmemory-policy allkeys-lfu 即可。)
Redis 的热点 key 解决方案
服务端缓存:即将热点数据缓存至服务端的内存中.(利用 Redis 自带的消息通知机制来保证 Redis 和服务端热点 Key 的数据一致性,对于热点 Key 客户端建立一个监听,当热点 Key 有更新操作的时候,服务端也随之更新。)
备份热点 Key:即将热点 Key+随机数,随机分配至 Redis 其他节点中。这样访问热点 key 的时候就不会全部命中到一台机器上了。
如何解决 Redis 缓存雪崩问题
使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉
缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效
限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务
如何解决 Redis 缓存穿透问题
在接口做校验
存 null 值(缓存击穿加锁,或设置不过期)
布隆过滤器拦截: 将所有可能的查询 key 先映射到布隆过滤器中,查询时先判断 key 是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。布隆过滤器将值进行多次哈希 bit 存储,布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。
Redis 的持久化机制
Redis 为了保证效率,数据缓存在了内存中,但是会周期性地把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。Redis 的持久化策略有两种:
RDB:快照形式是直接把内存中的数据保存到一个 dump 的文件中,定时保存,保存策略。当 Redis 需要做持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时 RDB 文件中。当子进程完成写临时文件后,将原来的 RDB 替换掉。
AOF:把所有的对 Redis 的服务器进行修改的命令都存到一个文件里,命令的集合。
使用 AOF 做持久化,每一个写命令都通过 write 函数追加到 appendonly.aof 中。aof 的默认策略是每秒钟 fsync 一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF 的文件体积通常要大于 RDB 文件的体积。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。 Redis 默认是快照 RDB 的持久化方式。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。
Redis 的事务
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。总结说:redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis 事务没有隔离级别的概念,批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
Redis 中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
Redis 事务相关命令
watch key1 key2 ... : 监视一或多个 key,如果在事务执行之前,被监视的 key 被其他命令改动,则事务被打断(类似乐观锁)
multi : 标记一个事务块的开始(queued)
exec : 执行所有事务块的命令(一旦执行 exec 后,之前加的监控锁都会被取消掉)
discard : 取消事务,放弃事务块中的所有命令
unwatch : 取消 watch 对所有 key 的监控
Redis 和 memcached 的区别
存储方式上:memcache 会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。redis 有部分数据存在硬盘上,这样能保证数据的持久性。
数据支持类型上:memcache 对数据类型的支持简单,只支持简单的 key-value,,而 redis 支持五种数据类型。
用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。redis 直接自己构建了 VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
value 的大小:redis 可以达到 1GB,而 memcache 只有 1MB。
Redis 的几种集群模式
主从复制
哨兵模式
cluster 模式
Redis 的哨兵模式
哨兵是一个分布式系统,在主从复制的基础上你可以在一个架构中运行多个哨兵进程,这些进程使用流言协议来接收关于 Master 是否下线的信息,并使用投票协议来决定是否执行自动故障迁移,以及选择哪个 Slave 作为新的 Master。
每个哨兵会向其它哨兵、master、slave 定时发送消息,以确认对方是否活着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的”主观认为宕机”)。
若“哨兵群“中的多数 sentinel,都报告某一 master 没响应,系统才认为该 master"彻底死亡"(即:客观上的真正 down 机),通过一定的 vote 算法,从剩下的 slave 节点中,选一台提升为 master,然后自动修改相关配置。
Redis 的 rehash
Redis 的 rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的,redis 会维护维持一个索引计数器变量 rehashidx 来表示 rehash 的进度。
这种渐进式的 rehash 避免了集中式 rehash 带来的庞大计算量和内存操作,但是需要注意的是 redis 在进行 rehash 的时候,正常的访问请求可能需要做多要访问两次 hashtable(ht[0], ht[1]),例如键值被 rehash 到新 ht1,则需要先访问 ht0,如果 ht0 中找不到,则去 ht1 中找。
Redis 的 hash 表被扩展的条件
哈希表中保存的 key 数量超过了哈希表的大小.
Redis 服务器目前没有在执行 BGSAVE 命令(rdb)或 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1.
Redis 服务器目前在执行 BGSAVE 命令(rdb)或 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 5.(负载因子=哈希表已保存节点数量 / 哈希表大小,当哈希表的负载因子小于 0.1 时,对哈希表执行收缩操作。)
Redis 并发竞争 key 的解决方案
分布式锁+时间戳
利用消息队列
Redis 与 Mysql 双写一致性方案
先更新数据库,再删缓存。数据库的读操作的速度远快于写操作的,所以脏数据很难出现。可以对异步延时删除策略,保证读请求完成以后,再进行删除操作。
Redis 的管道 pipeline
对于单线程阻塞式的 Redis,Pipeline 可以满足批量的操作,把多个命令连续的发送给 Redis Server,然后一一解析响应结果。Pipelining 可以提高批量处理性能,提升的原因主要是 TCP 连接中减少了“交互往返”的时间。pipeline 底层是通过把所有的操作封装成流,redis 有定义自己的出入输出流。在 sync() 方法执行操作,每次请求放在队列里面,解析响应包。
🎉 关注公众号 | 架构精进之路,即时获取更新
本人十年后端研发经验,任职架构师,曾“混迹”多个互联网大厂,专注软件架构技术研究学习,希望能够不断沉淀、学习以及分享,将自己工作中的问题和技术总结输出,分享影响到更多的人;
公众号专注:软件架构研究,技术学习与职业成长。内容涵盖:系统架构应用汇总、消息中间件、MySQL 实用探秘、职业认知升级 四大模块,大家可以在公众号底部菜单“精选专题”里随时查阅;
大家看我的公众号头像图片像是一个陀螺,其实是寓意螺旋式上升,让技术和自我能够不断精进。
文章首发于个人同名公众号《架构精进之路》,原文链接:2021年最新Redis面试题汇总,值得收藏
Thanks for reading!
版权声明: 本文为 InfoQ 作者【架构精进之路】的原创文章。
原文链接:【http://xie.infoq.cn/article/868bcf39afb7f46b73358f362】。文章转载请联系作者。
评论