Redis面试题

1. 谈谈对Redis的认识

Redis是一个高性能非关系型(NoSQL)的键值对数据库。Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。
优点:

  • 读写性能优异;支持数据持久化;
  • 支持事务,操作都是原子性的;
  • 数据结构丰富;
  • 支持主从复制,可以进行读写分离

缺点:

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写;
  • Redis 不具备自动容错和恢复功能;
  • 主机宕机,宕机前有部分数据未能及时同步到从机,会引入数据不一致的问题,降低了系统的可用性;
  • Redis 较难支持在线扩容

2. Redis为什么这么快

  • 基于内存的操作,数据访问快
  • 使用了 I/O 多路复用模型,select、epoll 等。
  • 单线程可以避免不必要的上下文切换和竞争条件,减少了这方面的性能消耗。
  • 数据结构简单,对数据操作也简单。

3. Redis为什么单线程

通常情况下CPU不会是redis的瓶颈,redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了,因为如果使用多线程的话会更复杂,同时需要引入上下文切换、加锁等等,会带来额外的性能消耗。

4. Redis 常见的数据结构

  • String:字符串,最基础的数据类型。value除了是字符串还可以是数字。底层是char类型的数组
  • List:列表。按照插入顺序排序底层是双向链表
  • Hash:哈希对象。是一个散列表结构,使用拉链法解决哈希冲突。
  • Set:集合。Set就是一种简化的Hash,只变动key,而value使用默认值填充
  • Sorted Set:有序集合,Set 的基础上加了个分值。ZSet 可以实现有序性操作,从而实现排行榜等功能。

5. 三种特殊数据类型

  • HyperLogLog:通常用于基数统计。适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的UV统计。
  • Geospatial:可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作:获取2个位置的距离、根据给定地理位置坐标获取指定范围内的地理位置集合。
  • Bitmap:位图。

6. Hash 对象底层结构

redis的哈希对象的底层存储可以使用ziplist(压缩列表)和hashtable。当hash对象可以同时满足一下两个条件时,哈希对象使用ziplist编码。

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
  • 哈希对象保存的键值对数量小于512个

ziplist(压缩列表):
使用压缩列表实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的节点推入到压缩列表的表尾,然后再将保存了值的节点推入到压缩列表表尾。因此保存了同一键值对的两个节点总是紧挨在一起;先添加的键值对会被放在压缩列表的表头方向,而后来添加的会被放在表尾方向。

使用压缩列表的条件:哈希对象保存的所有键值对的键和值的字符串长度都小于64字节且哈希对象保存的键值对数量小于512个

hashtable:
redis的hash架构就是标准的hashtable的结构,通过挂链解决冲突问题,与java的HashMap类似

7. Sorted Set对象的底层结构

Sorted Set(有序集合)当前有两种编码:ziplist、skiplist

  • ziplist:使用压缩列表实现,当保存的元素长度都小于64字节,同时数量小于128时,使用该编码方式,否则会使用 skiplist
  • skiplist:字典(dict)和跳跃表(zskiplist)存储方式。

跳跃表可具体参考:https://blog.csdn.net/qq_39033181/article/details/116560794

在这里插入图片描述

8. Sorted Set 为什么同时使用字典和跳跃表?

主要是为了提升性能。

  • 单独使用字典:在执行范围型操作,比如 zrank、zrange,字典需要进行排序,至少需要 O(NlogN) 的时间复杂度及额外 O(N) 的内存空间。
  • 单独使用跳跃表:根据成员查找分值操作的复杂度从 O(1) 上升为 O(logN)。

9. Sorted Set 为什么使用跳跃表,而不是红黑树?

主要有以下几个原因:

  1. 跳表的性能和红黑树差不多。
  2. 插入速度非常快速,因为不需要进行旋转等操作来维持平衡
  3. 跳表更容易实现和调试。

10. Hash 对象的扩容流程

hash 对象在扩容时使用了一种叫“渐进式 rehash”的方式,步骤如下:

  1. 为新表 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
  2. 将 rehash 索引计数器变量 rehashidx 的值设置为0,表示 rehash 正式开始。
  3. 从 ht[0] 表的 rehashidx 索引位置上开始向后查找,找到第一个不为空的索引位置,将该索引位置的所有节点 rehash 到 ht[1],当本次 rehash 工作完成之后,将 ht[0] 索引位置为 rehashidx 的节点清空,并令rehashidx++。
  4. 渐进式 rehash,每次对字典执行添加、删除、査找、更新操作时,程序除了执行指定的操作以外,还会触发额外的 rehash 操作。
  5. 随着操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被rehash 至 ht[1],此时 rehash 流程完成,会执行最后的清理工作:释放ht[0] 的空间、将 ht[0] 指向 ht[1]、重置 ht[1]、重置 rehashidx 的值为 -1。

11. 渐进式 rehash 的优缺点

优点:

  • 它采取分而治之的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 而带来的庞大计算量。

缺点:

  • 扩容期开始时,会先给 ht[1] 申请空间,所以在整个扩容期间,会同时存在 ht[0] 和 ht[1],会占用额外的空间
  • 扩容期间同时存在 ht[0] 和 ht[1],查找、删除、更新等操作有概率需要操作两张表,耗时会增加

12. 持久化——RDB

RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。

工作原理
在进行 RDB 的时候,redis 的主进程是不会做 io 操作的,主线程会 fork 一个子进程来完成该操作;

  1. Redis 调用forks。同时拥有父进程和子进程。
  2. 子进程将数据集写入到一个临时 RDB 文件中。
  3. 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

触发机制

  1. 使用save命令。
  2. 使用bgsave命令,是异步进行,进行持久化的时候,redis 还可以将继续响应客户端请求
  3. 执行flushall命令,也会触发我们的rdb原则
  4. 退出redis,也会自动产生rdb文件
  5. save的规则满足的情况下,会自动触发rdb原则

优点:

  • 适合大规模的数据恢复
  • 对数据的完整性要求不高

缺点:

  • 有一定的时间间隔进行操作,redis意外宕机了,这个最后一次修改的数据就没有了
  • fork进程的时候,会占用一定的内容空间。

13. 持久化——AOF

将我们所有的命令都记录下来,恢复的时候就把这个文件全部再执行一遍。将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件。AOF 持久化默认是关闭的,可以通过配置:appendonly yes 开启。
在这里插入图片描述
优点

  • 每一次修改都会同步,文件的完整性会更加好
  • 没秒同步一次,可能会丢失一秒的数据

缺点

  • 相对于数据文件来说,AOF远远大于RDB,修复速度比RDB慢!
  • AOF运行效率也要比RDB慢,所以我们redis默认的配置就是RDB持久化

14. 为什么需要AOF重写

AOF 持久化是通过保存被执行的写命令来记录数据库状态的,随着写入命令的不断增加,AOF 文件中的内容会越来越多,文件的体积也会越来越大。如果不加以控制,体积过大的 AOF 文件可能会对 Redis 服务器、甚至整个宿主机造成影响,并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越多。

14.5 AOF的重写机制

  1. 重写AOF的时候,创建一个重写子进程,然后读取旧的AOF文件,压缩并写入到一个临时AOF。
  2. 在此期间,主进程一边将接收到的指令累计到一个缓冲区中,一边将指令写入到旧的AOF。
    (这样的好处,保证AOF文件的可用性,避免写过程时出意外)
  3. 子进程写完后,向主进程发送一个信号量,主进程就将缓冲区中的指令追加到新AOF。
  4. 用新的AOF替换旧的AOF,之后的新指令就追加到新的AOF。

15. AOF 重写以及存在的问题、如何解决

  • 如何重写:
    遍历所有数据库的所有键,从数据库读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。Redis 生成新的 AOF 文件来代替旧 AOF 文件,这个新的 AOF 文件包含重建当前数据集所需的最少命令。
  • 存在问题:
    子进程在进行 AOF 重写期间,服务器主进程还需要继续处理命令请求,新的命令可能会对现有的数据库状态进行修改,从而使得当前的数据库状态和重写后的 AOF 文件保存的数据库状态不一致。
  • 解决方法:
    Redis 引入了 AOF 重写缓冲区。AOF 重写开始,服务器执行的所有写命令会被记录到 AOF 重写缓冲区里面,当子进程完成 AOF 重写工作后,父进程会将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致

16. Redis 删除过期键的策略

  1. 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。 对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  2. 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  3. 定期过期:每隔一定的时间,程序就对数据库进行一次检査,随机删除里面的过期键,该策略是前两者的一个折中方案,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

redis采用的是定期删除+惰性删除策略

17. Redis 的内存淘汰策略(后两种新增)

LRU的意思是:Least Recently Used最近最少使用的,LFU的意思是:Least Frequently Used最不常用的

  • noeviction:默认策略,不淘汰任何 key,直接返回错误
  • allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分 key
  • allkeys-random:在所有的 key 中,随机淘汰部分 key
  • volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key
  • volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key
  • volatile-ttl:在设置了过期时间的 key 中,挑选 TTL(time to live,剩余时间)短的 key 淘汰
  • volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  • allkeys-lfu:从所有键中驱逐使用频率最少的键

18. Redis事务

  • Redis的单条命令是保证原子性的,但是redis事务不能保证原子性
  • Redis事务操作过程
    开启事务(multi)
    命令入队
    执行事务(exec)
  • 使用watch key监控指定数据,相当于乐观锁加锁。
  • 每次提交执行exec后都会自动释放锁,不管是否成功

19. Redis主从复制

  • 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点,后者称为从节点,数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
  • 默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点

主从复制的原理
1)全量复制

  1. 从服务器连接主服务器,发送SYNC命令; (从服务器主动发起同步请求)
  2. 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  3. 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  6. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
    在这里插入图片描述

2)增量复制
 Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

在这里插入图片描述

redis主从复制策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

作用

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外一种数据冗余的方式。
  • 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
  • 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。
  • 高可用基石:主从复制还是哨兵和集群能够实施的基础。

20. 哨兵模式

Sentinel 可以在被监视的主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

1)哨兵故障检测

  • 检查主观下线状态
    在默认情况下,Sentinel 会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他 Sentinel 在内)发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线。
    如果一个实例在 down-after-miliseconds 毫秒内,连续向 Sentinel 返回无效回复,那么 Sentinel 会修改这个实例所对应的实例结构,在结构的 flags 属性中设置 SRI_S_DOWN 标识,以此来表示这个实例已经进入主观下线状态。

  • 检查客观下线状态
    当 Sentinel 将一个主服务器判断为主观下线之后,为了确定这个主服务器是否真的下线了,它会向同样监视这一服务器的其他 Sentinel 进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。
    当 Sentinel 从其他 Sentinel 那里接收到足够数量(quorum,可配置)的已下线判断之后,Sentinel 就会将服务器置为客观下线,在 flags 上打上 SRI_O_DOWN 标识,并对主服务器执行故障转移操作。

2)哨兵故障转移流程
当哨兵监测到某个主节点客观下线之后,就会开始故障转
流程。核心流程如下

  1. 发起一次选举,选举出领头 Sentinel
  2. 领头 Sentinel 在已下线主服务器的所有从服务器里面,选出一个从服务器,并将其升级为新的主服务器。
  3. 领头 Sentinel 将剩余的所有从服务器改为复制新的主服务器。
  4. 领头 Sentinel 更新相关配置信息,当这个旧的主服务器重新上线时,将其设置为新的主服务器的从服务器。

21. 相比 memcached 有哪些优势?

  • Redis支持更丰富的数据类型:Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,而Memecache把数据全部存在内存之中。
  • 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
  • Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。

22. 缓存穿透(数据库没有这个数据)

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:

  1. 接口层增加校验。如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 缓存空值。从当访问缓存和DB都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间。这样可以防止攻击用户反复用同一个id暴力攻击
  3. 采用布隆过滤器,使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库

23. 缓存击穿(数据库有这个数据)

一个存在的key(数据库中存在这个数据),但是缓存过期,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增,这就是缓存被击穿
解决方案:

  • 设置热点数据永远不过期。(无需访问数据库,压力减小)
  • 加互斥锁,互斥锁(避免高并发导致数据库压力变大)

24. 缓存雪崩

大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案:

  • 让Key的失效时间分散开,防止同一时间大量数据过期现象发生。
  • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中
  • 限流降级,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量

1. redis的9种数据结构

在这里插入图片描述

2. 如何保障数据库与缓存的一致性?

由于缓存和数据库是分开的,无法做到原子性的同时进行数据修改,可能出现缓存更新失败,或者数据更新失败的情况,这时候会出现数据不一致,影响前端业务。

方案1: 先淘汰缓存,后更新数据库
正常情况

  • A请求进行写操作,先淘汰缓存,再更新数据库
  • B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中

异常情况1

  • A请求进行写操作,先淘汰缓存
  • B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中。注意,此时redis中被更新的依然是老数据,A请求的数据库更新操作尚未完成
  • A请求进行数据库更新操作。此时,数据库中是新数据,redis缓存中是老数据,产生了数据不一致的问题。且该不一致会一直持续到缓存自然失效或者下次的更新操作

对于该种异常情况,提供两种解决思路:

  1. 异步更新缓存。

    1. A请求进行写操作,先淘汰缓存
    2. B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据。注意,此时不向redis写入新的缓存策略
    3. A请求通过订阅数据库binlog,对redis缓存数据进行异步更新
    4. 该方案虽然解决了数据不一致的问题,但是在数据库更新操作完成前,所有的读请求都会直接打到数据库上,具有比较大的风险。
  2. 延时双删

    1. A请求进行写操作,先淘汰缓存
    2. B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中。注意,此时redis中被更新的依然是老数据,A请求的数据库更新操作尚未完成。假设该步骤耗时N秒
    3. A请求进行数据库更新操作。
    4. 由于此时redis中写入了老数据,因此A请求在休眠M秒后(M略大于N),再次对redis进行淘汰缓存操作
    5. 该方案虽然解决了数据不一致的问题,但是由于请求A在更新完数据库之后,需要休眠M秒再次淘汰缓存,一定程度上影响了数据更新操作的吞吐量。可以尝试将等待M秒更新redis的操作放到另一个单独的线程(比如消息队列 + 重试机制)。可以有效缓解吞吐量降低的问题。

异常情况2

  • A请求进行读操作,此时redis缓存中没有数据,因此直接从数据库中读取数据
  • B请求进行写操作,先淘汰缓存,再更新数据库
  • A请求进行将从数据库中读到的老数据,更新到redis。此时产生数据不一致问题。
  • 该种异常情况发生概率极低,一般读操作比写操作要快。如有担心,可以采用上述的延时删除策略

方案2: 先更新数据库,后更新淘汰缓存
正常情况

  • A请求进行写操作,先更新数据库,再淘汰缓存
  • B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中

异常情况1

  • A请求进行写操作,先更新数据库
  • B请求进行读操作,由于A请求尚未淘汰缓存,B请求在redis中发现所需数据,因此直接返回老数据,产生了数据不一致的问题
  • A请求淘汰缓存。
  • C请求进行读操作,发现redis中没有数据,因此从数据库中读取新数据,并更新至缓存。数据不一致的问题解决。
  • 该场景下,数据最终一致,只是在高并发下产生了一小段时间的数据不一致。

异常情况2

  • A请求进行读操作,此时redis缓存中没有数据,因此直接从数据库中读取数据
  • B请求进行写操作,更新数据库,并将redis中缓存进行了淘汰(虽然此时redis中并没有任何的缓存)
  • A请求将从数据库中读到的老数据,更新到redis。此时产生数据不一致问题。
  • 该种异常情况发生概率极低,一般读操作比写操作要快。如有担心,可以采用上述的延时删除策略

总结

  • 方案1,先淘汰缓存,后更新数据库的策略,有可能导致长时间的数据不一致问题,可以通过延时双删 or 异步更新缓存策略进行解决。
  • 方案2,先更新数据库,后更新缓存,有可能导致极短时间内的数据不一致,但是数据最终是一致的

为什么缓存是删除而不是更新?
删除更加轻量,延迟加载的一种实现,更新可能涉及到多个表,比较耗时。

3. redis有哪些数据结构?分别有哪些典型的应用场景?

  1. String字符串:可以缓存简单或者JSON格式的字符串。redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、session共享、分布式ID等等
  2. hash哈希表:可以用来存储一些key-value对,更适合用来存储对象
  3. list列表:redis的列表通过命令的组合,既可以当做栈来使用,也可以当做队列来使用,可以用来缓存类似公众号、微博等消息流数据
  4. set集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集等操作,从而可以实现我和某人的共同好友、朋友圈积攒等功能
  5. zset有序集合:集合是无序的,有序集合可以实现排序,可以用来实现排行榜功能。

4. redis集群方案

哨兵模式
在这里插入图片描述
Cluster模式(服务端分片)
在这里插入图片描述
在这里插入图片描述
Redis Sharding(客户端分片)
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值