Redis面试题
2022-10-28 / 可可西里

Redis面试常见问题汇总

1. Redis的数据结构及使用场景

  • String字符串

    字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型,而且 其他几种数据结构都是在字符串类型基础上构建的,我们常使用的 set key value 命令就是字符串。常用在缓存、计数、共享Session、限速等


  • Hash哈希

    在Redis中,哈希类型是指键值本身又是一个键值对结构,哈希可以用来存放用户信息,比如实现购物车


  • List列表(双向链表)

    列表(list)类型是用来存储多个有序的字符串。可以做简单的消息队列的功能


  • Set集合

    集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能


  • Sorted Set有序集合(跳表实现)

    Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作

2. 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)

3. 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最小的值然后将其淘汰

4. 单线程的Redis为什么快

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换
  • 合理高效的数据结构
  • 采用了非阻塞I/O多路复用机制

5. Redis的数据过期策略

Redis 中数据过期策略采用定期删除惰性删除策略:

  • 定期删除策略
    • Redis 启用一个定时器定时监视所有的 key,判断key是否过期,过期的话就删除
    • 这种策略可以保证过期的 key 最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有的数据,非常消耗 CPU资源,并且当 key 已过期,但是定时器还处于未唤起状态,这段时间内 key 仍然可以用

  • 惰性删除策略
    • 在获取 key 时,先判断 key 是否过期,如果过期则删除
    • 这种方式存在一个缺点:如果这个 key一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间

这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不在是每次扫描全部的 key 了,而是随机抽取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了为检查到的key,基本上满足了所有要求

但是有时候就是那么的巧,既没有被定时器抽取到,又没有被使用,这些数据又如何从内存中消失?这个时候就需要用到了,内存淘汰机制

5.1 内存淘汰机制
  • 当内存不足以容纳新写入数据时,新写入操作会报错。(Redis 默认策略)
  • 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(LRU推荐使用)
  • 当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key
  • 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用
  • 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key
  • 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除

6. 如何解决Redis缓存雪崩问题

  • 使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉
  • 缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效
  • 限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务

7. 如何解决Redis缓存穿透问题

  • 在接口层做校验
  • 存null值(缓存击穿加锁)
  • 布隆过滤器拦截:将所有可能的查询key 先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回
    布隆过滤器将值进行多次哈希bit存储,布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在

8. Redis并发竞争key如何解决

  • 可以利用分布式锁和时间戳来解决
  • 利用消息队列解决

9. Redis的主从模式和哨兵模式和集群模式区别

Redis集群方式共有三种:主从模式哨兵模式集群(cluster)模式

  • 主从模式

    主从模式是三种集群方式里最简单的。它主要是基于Redis的主从复制特性架构的。通常我们会设置一个主节点,N个从节点;默认情况下,主节点负责处理使用者的IO操作,而从节点则会对主节点的数据进行备份,并且也会对外提供读操作的处理。主要的特点如下:

    • 主从模式下,当某一节点损坏时,因为其会将数据备份到其它Redis实例上,这样做在很大程度上可以恢复丢失的数据
    • 主从模式下,可以保证负载均衡.
    • 主从模式下,主节点和从节点是读写分离的。使用者不仅可以从主节点上读取数据,还可以很方便的从从节点上读取到数据,这在一定程度上缓解了主机的压力

    从节点也是能够支持写入数据的,只不过从从节点写入的数据不会同步到主节点以及其它的从节点下。从以上,我们不难看出Redis在主从模式下,必须保证主节点不会宕机——一旦主节点宕机,其它节点不会竞争称为主节点,此时,Redis将丧失写的能力。这点在生产环境中,是致命的


  • 哨兵模式

    哨兵模式是基于主从模式做的一定变化,它能够为Redis提供了高可用性

    在实际生产中,服务器难免不会遇到一些突发状况:服务器宕机,停电,硬件损坏等。这些情况一旦发生,其后果往往是不可估量的

    而哨兵模式在一定程度上能够帮我们规避掉这些意外导致的灾难性后果。其实,哨兵模式的核心还是主从复制

    只不过相对于主从模式在主节点宕机导致不可写的情况下,多了一个竞选机制——从所有的从节点竞选出新的主节点。竞选机制的实现,是依赖于在系统中启动一个sentinel进程。sentinel特点:

    • 监控:它会监听主服务器和从服务器之间是否在正常工作
    • 通知:它能够通过API告诉系统管理员或者程序,集群中某个实例出了问题。
    • 故障转移:它在主节点出了问题的情况下,会在所有的从节点中竞选出一个节点,并将其作为新的主节点。
    • 提供主服务器地址:它还能够向使用者提供当前主节点的地址。这在故障转移后,使用者不用做任何修改就可以知道当前主节点地址

    sentinel,也可以集群,部署多个哨兵,sentinel可以通过发布与订阅来自动发现Redis集群上的其它sentinel。sentinel在发现其它sentinel进程后,会将其放入一个列表中,这个列表存储了所有已被发现的sentinel

    集群中的所有sentinel不会并发着去对同一个主节点进行故障转移。故障转移只会从第一个sentinel开始,当第一个故障转移失败后,才会尝试下一个

    当选择一个从节点作为新的主节点后,故障转移即成功了(而不会等到所有的从节点配置了新的主节点后)。这过程中,如果重启了旧的主节点,那么就会出现无主节点的情况,这种情况下,只能重启集群

    当竞选出新的主节点后,被选为新的主节点的从节点的配置信息会被sentinel改写为旧的主节点的配置信息。完成改写后,再将新主节点的配置广播给所有的从节点


  • 集群模式

    Redis 集群是一个提供在多个Redis间节点间共享数据的程序集, 其中Redis集群分为主节点和从节点。主节点用于处理槽,而从节点用于复制某个主节点,并在被复制的主节点下线时,代替下线的主节点继续处理命令请求

    Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误

    Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令. Redis 集群的优势:自动分割数据到不同的节点上

    整个集群的部分节点失败或者不可达的情况下能够继续处理命令

    Redis集群的数据分片 Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念

    Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽

10. 单机、主从、哨兵、集群模式优缺点

10.1 单机模式

Redis 单副本,采用单个 Redis 节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景

优点:

  • 架构简单,部署方便
  • 高性价比:缓存使用时无需备用节点(单实例可用性可以用 supervisor 或 crontab 保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务
  • 高性能

缺点:

  • 不保证数据的可靠性
  • 在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务
  • 高性能受限于单核 CPU 的处理能力(Redis 是单线程机制),CPU 为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用 Memcached 替代
10.2 主从模式

主(master)和 从(slave)部署在不同的服务器上,当主节点服务器写入数据时会同步到从节点的服务器上,一般主节点负责写入数据,从节点负责读取数据

从节点设置只读属性,而主节点没有只写属性,因此,主节点可读可以写

优点:

  • 读写分离,提高效率

    主节点负责写操作,从节点负责读操作;如果写少读多场景,配置多个从节点的话,效率非常高

  • 数据热备份,提供多个副本

    从节点宕机,影响较小

缺点:

  • 主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预

    因为只有主节点能进行写操作,一旦主节点宕机,整个服务就无法使用。当然此时从节点仍可以进行读操作,但是对于整个服务流程来说,是无法使用的

  • Master的写的压力难以降低

    如果写操作比较多,那么只有一个主节点的话,无法分担压力

  • 主节点存储能力受到单击限制

    主节点只能有一个,因此单节点内存大小不会太大,因此存储数据量受限

  • 主从数据同步,可能产生部分的性能影响甚至同步风暴

    风暴问题,对于任何集群分布式来说都存在,要合理分布节点

10.3 哨兵模式

为了解决这两个问题,在2.8版本之后redis正式提供了sentinel架构

image.png

在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态

如果master节点异常,则会做主从切换,将某一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般

优点:

  • 对节点进行监控,来完成自动的故障发现与转移

缺点:

  • 特别是在主从切换的瞬间存在访问瞬断的情况,等待时间比较长,至少十来秒不可用
  • 哨兵模式只有一个主节点对外提供服务,没法支持很高的并发
  • 单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率

与主从相比,哨兵仅解决了手动切换主从节点问题,至于其他的问题,基本上仍然存在

哨兵的主要问题还是由于中心架构,仅存在一个master节点引起的,写的效率太低

10.4 集群模式

Redis Cluster 是 3.0 版后推出的 Redis 分布式集群解决方案,主要解决 Redis 分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster 能起到很好的负载均衡的目的

Redis Cluster 集群节点最小配置 6 个节点以上(3 主 3 从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据

注意:集群模式下 从节点不提供读写,与主从模式不一样。 总结一下经验,分布式 场景下:集群模式一般从节点不参与读写,仅作为备用节点。而主从一般都要负责读或写,都要参与具体的工作

image.png

优点:

  • 无中心架构

    即有多个master节点,不像哨兵模式下仅有一个。这样写的压力就可以分散了;并且存储量也可以扩展了,因为多个主节点都可以存储一部分数据,总量要远大于单主节点架构

  • 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布

  • 可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除

  • 高可用性:部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够 实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升

当然,如果某个槽归属的小群内都不可用时,整个服务仍然是不可用的!通过cluster-require-full-coverageyes 控制该特性, 默认yes 即需要集群完整,方可对外提供服务,设置为no ,其他的小集群仍然可以对外提供服务

缺点:

  • 如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了

11. Redis事物的了解CheckAndSet操作实现乐观锁

和众多其它数据库一样,Redis作为NoSQL数据库也同样提供了事务机制。在Redis中,MULTIEXECDISCARDWATCH这四个命令是我们实现事务的基石。相信对有关系型数据库开发经验的开发者而言这一概念并不陌生,即便如此,我们还是会简要的列出 Redis中事务的实现特征 :

  • 在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行
  • 和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行
  • 我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以将其理解为”BEGIN TRANSACTION”语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXECDISCARD命令来提交,回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLBACK语句
  • 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行
  • 当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了

12. Redis有序集合zset底层怎么实现的

  • 跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表
  • 跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找
  • 跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能

  • 跳表是可以实现二分查找的有序链表
  • 每个元素插入时随机生成它的level
  • 最低层包含所有的元素
  • 如果一个元素出现在level(x),那么它肯定出现在x以下的level中
  • 每个索引节点包含两个指针,一个向下,一个向右
  • 跳表查询、插入、删除的时间复杂度为O(log n),与平衡二叉树接近

12.1 为什么Redis选择使用跳表而不是红黑树来实现有序集合?(O(logN))

首先,我们来分析下Redis的有序集合支持的操作:

  • 插入元素
  • 删除元素
  • 查找元素
  • 有序输出所有元素
  • 查找区间内所有元素

其中,前4项红黑树都可以完成,且时间复杂度与跳表一致。但是,最后一项,红黑树的效率就没有跳表高了。在跳表中,要查找区间的元素,我们只要定位到两个区间端点在最低层级的位置,然后按顺序遍历元素就可以了,非常高效

而红黑树只能定位到端点后,再从首位置开始每次都要查找后继节点,相对来说是比较耗时的。此外,跳表实现起来很容易且易读,红黑树实现起来相对困难,所以Redis选择使用跳表来实现有序集合

13. 跳表的查询过程是怎么样的,查询和插入的时间复杂度

先从第一层查找,不满足就下沉到第二层找,因为每一层都是有序的,写入和插入的时间复杂度都是O(logN)


本文链接:
https://huajun-chen.github.io/2022/10/28/Redis面试题/