最近结合多位程序员朋友的面试反馈,抽时间整理了下Redis常见面试题,此篇算作上一篇《面试Redis没底?这40道面试题让你不再慌(附答案)》 的助力补充吧,便于大家查漏补缺,完善知识点,强烈建议大家收藏为敬,哈哈哈。
应用场景
- 缓存
- 共享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的管道pipeline
对于单线程阻塞式的Redis,Pipeline可以满足批量的操作,把多个命令连续的发送给Redis Server,然后一一解析响应结果。Pipelining可以提高批量处理性能,提升的原因主要是TCP连接中减少了“交互往返”的时间。pipeline 底层是通过把所有的操作封装成流,redis有定义自己的出入输出流。在 sync() 方法执行操作,每次请求放在队列里面,解析响应包。
Redis与Mysql双写一致性方案
先更新数据库,再删缓存。数据库的读操作的速度远快于写操作的,所以脏数据很难出现。可以对异步延时删除策略,保证读请求完成以后,再进行删除操作。
福利:关注本公众号,在底部菜单栏「福利专区」-面试资料,即可免费获取大量面试相关技术图书资料。
- END -
作者:架构精进之路,专注软件架构研究,技术学习与个人成长,关注并私信我回复“01”,送你一份程序员成长进阶大礼包。
「技术架构精进」专注架构研究,技术分享
Thanks for reading!