Redis常见面试题与答案

x33g5p2x  于2021-11-21 转载在 Redis  
字(5.9k)|赞(0)|评价(0)|浏览(375)

Redis的基本数据类型

Redis有哪些常用的数据类型?

  • String:字符串(最常用的缓存)
  • Hash:哈希(保存对象)
  • List:有序列表(消息队列)
  • Set:无序集合(抽奖、点赞、求交集、并集、差集)
  • Zset:有序集合(热门排行榜)
  • BitMap:位图(统计用户在线状态、活跃用户数、签到等)
  • HyperLogLog:基数统计(统计网站UV)
  • Geospatial:地理空间(地理位置相关查询与计算)

String的底层结构是什么?

  • 是一种叫做sds的数据结构(simple dynamic string 简单动态字符串),本质上其实还是字符串数组。
  • sds又可以细分为:sdshdr5(2^5 byte)、sdshdr8(2^8 byte)、sdshdr16(2^16 byte)、sdshdr32(2^32 byte)、sdshdr64(2^64 byte),用于存储不同长度的字符串。

为什么Redis要用sds实现字符串?

  • c语言本身没有字符串类型,只有字符串数组,定义字符串数组时需要先分配足够内存,否则会发生内存溢出,而sds无需担心内存溢出问题,它支持动态扩容;

  • 字符串数组获取长度必须遍历数组,时间复杂度为O(n),而sds中定义了len属性,可以直接获取长度,时间复杂度为O(1);

  • 字符串数组在进行修改时会做内存重新分配,而sds可以通过空间预分配和惰性空间释放来防止多次重新分配内存;

  • 空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。

  • 惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。

  • c语言字符串通过第一个’\0’来标记字符串结束,因此不能保存图片、音频等二进制文件,而sds是通过len属性来确定长度,因此可以保存二进制文件。

String有几种编码类型?

  • int:存储8个字节的长整型(最大值 2^63 - 1),超出范围会升级为embstr;
  • embstr:存储小于44字节的字符串,embstr中的redisObject与sds存储连续,创建时只需分配一次内存空间,存取效率高,且embstr不可修改,修改后会升级为raw;
  • raw:存储大于44字节的字符串,raw中的redisObject与sds分开存储,创建时只需分配两次内存空间,适合存储较大或频繁修改的字符串。

Hash的底层实现是什么?

  • ziplist与hashtable

什么是ziplist(压缩列表)?

  • ziplist是一个经过特殊编码,由连续内存块组成的双向链表。为了达到压缩内存,节省空间的目的,它不存储上一个和下一个节点的指针,而是存储上一个节点的长度和当前节点的长度,因此可以通过计算长度来实现元素遍历。这样做虽然牺牲了一定的查询效率,但也节省了空间,(以时间换空间)因此适合元素不多的场景。

  • 在hash类型中,同时满足以下两个条件时才使用ziplist:

  • hash对象保存的键值数 < 512;

  • 所有键和值的字符串长度都 < 64 byte。

ziplist与数组的区别?

  • ziplist和数组都是连续内存,需要预分配,但是数组元素长度相同,ziplist的元素长度可以不同;
  • 数组的元素保存了索引下标,而ziplist的元素只存储了上一节点和当前节点的长度;
  • 数组通过下标来遍历,ziplist通过元素记录的长度遍历;
  • 数组的查询时间复杂度O(1),ziplist的查询时间复杂度O(n)。

hashtable的特点是什么?

  • hashtable与java中的hashtable类似,也是一个数组 + 链表的结构,同样采用拉链法解决hash冲突。

  • 在redis的hashtable的底层数据结构中有两个ht:

  • ht[0],是存放数据的table,作为非扩容时容器;

  • ht[1],只有正在进行扩容时才会使用,它也是存放数据的table,长度为ht[0]的两倍。

  • 当总元素个数与hash槽的比值大于dict_force_resize_ratio(1)时,会触发扩容,即触发ht[0] 到 ht[1] 的rehash过程。

hashtable的扩容原理

  • hashtable扩容采用渐进式rehash,避免数据量大时,一次性rehash产生阻塞;
  • 在rehash期间,会维护一个rehashIndex,用来表示rehash进度,初始值为0,代表rehash进行中;
  • 每次迁移完一个槽位,rehashIndex + 1;
  • 迁移过程中如果有请求进来,将请求路由到的index与rehashIndex比较,如果大于rehashIndex,代表这部分数据还没有迁移完成,访问ht[0],否则访问ht[1];
  • 全部迁移完成后rehashIndex更新为-1。

List的底层实现是什么?

  • List的底层实现是quicklist,quicklist是一个与linkedlist类似的双向链表,每个quicklist的节点又包含多个ziplist,这样就结合了两者的优点,既保证一定的查询效率,又节省了一定内存空间。

Set的底层实现是什么?

  • set的底层实现为intset或hashtable,如果都是整型数字,则用intset存储,否则用hashtable。

Zset的底层实现是什么?

  • 默认使用ziplist存储,如果元素数量大于128或任意元素长度大于等于64byte,则使用skiplist + dict 存储。

什么是skiplist(跳跃表)?

  • 每相邻的两个节点都增加了一个指针,让指针指向下下个节点,这样所有新增加的指针连成了一个新的链表,但包含的节点数只有原来的一半。这样当我们查找数据时,先沿着新链表查找,类似于二分查找的思想。
  • 例如查找23的位置:

  1. 23首先和7比较,再和19比较,直到发现比23大的值;
  2. 当处于19节点时,发现26比23大,则与19的下一节点22比较;
  3. 22比23要小,下一节点26比23要大,说明要查找的23元素并不在链表内。
  • 因为level的值是随机的,skiplist可能不只是跳一层,也可能跳两层、三层等,如下图:

  • skiplist查找的时间复杂度为O(Log n);
  • 可以在O(1)的时间复杂度下获得skiplist的头节点、尾节点、长度和高度。

持久化

Redis有哪几种持久化方式?

  • Redis支持两种方式的持久化,一种是RDB方式、另一种是AOF(append-only-file)方式。前者会根据指定的规则“定时”将内存中的数据存储在硬盘上,而后者在每次执行命令后将命令本身记录下来。两种持久化方式可以单独使用其中一种,也可以将这两种方式结合使用。

AOF文件过大怎么办?

  • Redis提供了AOF文件的重写机制,优化掉冗余的命令;
  • 重写过程在AOF文件大小达到指定阈值时触发,由子进程执行;
  • 子进程并不是基于原有AOF文件进行重新,而是全量遍历当前内存中的数据,写入新的AOF文件,在此期间新的写请求命令会先写到缓冲区aof_rewrite_buf中,最后合并到一起。

内存回收策略

LRU和LFU的区别?

  • LRU是最近最少使用页面置换算法(Least Recently Used),也就是首先淘汰最长时间未被使用的页面(按使用时间排序);
  • LFU是最近最不常用页面置换算法(Least Frequently Used),也就是淘汰一定时期内被访问次数最少的页(按使用次数排序)。

Redis有哪些种内存回收策略?

  1. noeviction:默认的策略,如果内存使用达到阈值,所有引起申请内存的命令会报错;
  2. allkeys-random:添加数据时,如果内存使用达到阈值,就会扫描所有的key,随机淘汰一些key;
  3. volatile-random:添加数据时,如果内存使用达到阈值,扫描那些设置里过期时间的key,随机淘汰一些key;
  4. volatile-ttl:添加数据时,如果内存使用达到阈值,扫描那些设置里过期时间的key,淘汰一些即将过期的key(按失效时间排序);
  5. allkeys-lru:添加数据时,如果内存使用达到阈值,就会扫描所有的key,淘汰一些最近未使用的key(按最近使用时间排序);
  6. volatile-lru:添加数据时,如果内存使用达到阈值,扫描那些设置里过期时间的key,淘汰一些最近未使用的key(按最近使用时间排序);
  7. allkeys-lfu:添加数据时,如果内存使用达到阈值,就会扫描所有的key,淘汰一些最近最少使用的key(按最近使用次数排序);
  8. volatile-lfu:添加数据时,如果内存使用达到阈值,就会淘汰一些设置了过期时间的,并且最近最少使用的key(按最近使用次数排序)。

高可用

Redis有哪些高可用方案?

  • 主从、哨兵、集群。

哨兵节点的最少数量是多少?

  • 三个,一主两从。

哨兵Leader选举的原理是什么?

  • 哨兵的Leader选举是基于共识算法Raft;

  • Raft协议动画演示

  • Raft

  • 参与选举的三种角色:

  • Leader:负责事务请求的处理与数据同步;

  • Follower:可以处理非事务请求,以及Leader选举;

  • Candidate:候选者,在Leader选举期间,Follower会成为候选者;

  • 集群总节点数需要满足 2n + 1(n >= 1);

  • Leader选举的触发时机:

  • 集群初始化;

  • Leader宕机;

  • 成为Leader的条件:

  • 每个Candidate会在一个随机时间后进行广播发起投票,默认会投自己;

  • 其它节点接收到投票请求后会判断该节点的term(任期)是否优于自己,如果满足则会同意;

  • 当超过半数节点投票同意后该Candidate会成为新的Leader,同时term + 1;

  • 如果平票,则在一个超时时间后重新开启投票流程;

  • Leader的数据同步:

  • Leader 基于2pc实现数据同步,超过半数的Follower 响应成功后数据才算成功写入;

  • 如何处理脑裂或网络分区;

  • 以节点多的分区数据为准。

哨兵模式与集群模式的区别?

  • 哨兵模式是主从模式的升级版,主节点和从节点都保存了全量数据,而集群模式是对整个数据集进行了横向的分片,每个分片节点只保存一部分数据集;
  • 集群模式相比哨兵模式而言部署和运维成本更高,适合数据量更大的场景。

Redis集群方案不可用的情况有哪些?

  • 集群master半数以上宕机,无论是否有从库存活(无法进行选举);
  • 集群某一节点的主从全部宕机,集群进入fail状态。也可以理解成进群的slot映射[0-16383]不完整时进入fail状态。

缓存的经典七大问题

缓存失效

  • 在过期时间基础上加上随机时间,避免同时失效。

缓存穿透

  • 对于某些DB不存在的值,可以缓存一个特殊值,避免频繁访问DB;
  • 构建BloomFilter记录全量数据,这样可以过滤掉不存在的key。

缓存雪崩

  • 对DB的访问增加读写开关,当发现 DB 请求变慢、阻塞,慢请求超过阈值时,就会关闭读开关,部分或所有读 DB 的请求进行 failfast 立即返回,待 DB 恢复后再打开读开关。
  • 对缓存增加多个副本,缓存异常或请求 miss 后,再读取其他缓存副本,而且多个缓存副本尽量部署在不同机架,从而确保在任何情况下,缓存系统都会正常对外提供服务。
  • 对缓存体系进行实时监控,当请求访问的慢速比超过阈值时,及时报警,通过机器替换、服务替换进行及时恢复;也可以通过各种自动故障转移策略,自动关闭异常接口、停止边缘服务、停止部分非核心功能措施,确保在极端场景下,核心功能的正常运行。

缓存一致性问题

  • cache 更新失败后,可以进行重试,如果重试失败,则将失败的 key 写入队列机服务,待缓存访问恢复后,将这些 key 从缓存删除。这些 key 在再次被查询时,重新从 DB 加载,从而保证数据的一致性。
  • 缓存时间适当调短,让缓存数据及早过期后,然后从 DB 重新加载,确保数据的最终一致性。
  • 缓存的更新方案:缓存的三种读写方式_学无止境-CSDN博客_只读缓存 读写缓存

数据并发竞争

  • 使用全局锁。即当缓存请求 miss 后,先尝试加全局锁,只有加全局锁成功的线程,才可以到 DB 去加载数据。其他进程/线程在读取缓存数据 miss 时,如果发现这个 key 有全局锁,就进行等待,待之前的线程将数据从 DB 回种到缓存后,再从缓存获取。
  • 对缓存数据保持多个备份,即便其中一个备份中的数据过期或被剔除了,还可以访问其他备份,从而减少数据并发竞争的情况。

Hot key

  • 对于特殊的业务场景,提前分析收集热门的key;
  • 如果热门的key的名称为hotkey,可以将它分散为 hotkey#1、hotkey#2、hotkey#3,……hotkey#n,这 n 个 key 分散存在多个缓存节点,然后 client 端请求时,随机访问其中某个后缀的 hotkey,这样就可以把热 key 的请求打散,避免一个缓存节点过载。

Big key

  • 对于value超过阈值的key进行序列化压缩等。

分布式锁

Redisson如何实现可重入锁?

  • Redisson是通过lua脚本实现的lock和unlock,底层使用的数据结构是hash;
  • hash的key为锁实际的key,hash中会有一个entry,entry的key为传入的唯一threadId,value为重入次数,重入一次会+1,释放一次会-1;
  • 每次加锁都会更新失效时间,同时还有定时任务进行失效时间的延长(看门狗);
  • lock和unlock是基于Redis的Pub Sub模式实现锁的等待和唤醒的,lock过程如果无法获得锁,当前线程执行subscribe(threadId),订阅该threadId的锁事件,当对应threadId的锁完全释放后,会执行publish发布锁释放事件,这样订阅该事件的所有线程便可以重新竞争锁。

Redis线程模型

Redis 采用单线程为什么还这么快?

  • 首先,Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,比如哈希表和跳表;

  • 其次,因为是单线程模型避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题;

  • 另外,Redis并不是完全的单线程应用,除了核心的读写操作由专门的线程完成之外,还有很多辅助线程在后台执行工作,例如:

  • 主从同步;

  • 持久化;

  • 大key删除:直接通过del删除大key会导致Redis阻塞,例如数量较大的map、list或set等。Redis4.0引入了unlink命令,先标记再异步删除,避免阻塞;

  • 除了unlink之外,flushall、flushdb也都是异步操作;

  • 最后, Redis 采用了 I/O 多路复用机制,在Linux环境的epoll模式下,可以高效地进行网络通信,因为基于非阻塞的 I/O 模型,就意味着 I/O 的读写流程不再阻塞。

相关文章

微信公众号

最新文章

更多