这 20 道 Redis 经典面试题你还不会,就别去面试了!

本系列会系统的整理 MySQL,Redis,SSM 框架,算法,计网等面试常问技术栈的面试题,本文主要是整理分享了 Redis 相关的面试题,MySQL、Spring、JVM 之前已经更新了,需要的同学也可以去看一下,希望对正在准备秋招的你们有所帮助!

1. 什么是 Redis?它主要用来什么的?

2.说说 Redis 的基本数据结构类型

3. Redis 为什么这么快?

4. 什么是缓存击穿、缓存穿透、缓存雪崩?

5. 什么是热 Key 问题,如何解决热 key 问题

6. Redis 过期策略和内存淘汰策略

7.说说 Redis 的常用应用场景

8. Redis 的持久化机制有哪些?优缺点说说

9.怎么实现 Redis 的高可用?

10. 使用过 Redis 分布式锁嘛?有哪些注意点呢?

11. 使用过 Redisson 嘛?说说它的原理

12. 什么是 Redlock 算法

13. Redis 的跳跃表

14. MySQL 与 Redis 如何保证双写一致性

15. 为什么 Redis 6.0 之后改多线程呢?

16. 聊聊 Redis 事务机制

17. Redis 的 Hash 冲突怎么办

18. 在生成 RDB 期间,Redis 可以同时处理写请求么?

19. Redis 底层,使用的什么协议?

20. 布隆过滤器

当然个人整理的所有面试题都无偿分享,只求大伙一个点赞关注转发三连,这些文档都放在文末了,需要的同学可以自取

1. 什么是 Redis?它主要用来什么的?

Redis,英文全称是 Remote Dictionary Server(远程字典服务),是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。

与 MySQL 数据库不同的是,Redis 的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过 10 万次读写操作。因此 redis 被广泛应用于缓存,另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

2.说说 Redis 的基本数据结构类型

大多数小伙伴都知道,Redis 有以下这五种基本类型:

  • String(字符串)

  • Hash(哈希)

  • List(列表)

  • Set(集合)

  • zset(有序集合)

它还有三种特殊的数据结构类型

  • Geospatial

  • Hyperloglog

  • Bitmap

2.1 Redis 的五种基本数据类型

String(字符串)

  • 简介:String 是 Redis 最基础的数据结构类型,它是二进制安全的,可以存储图片或者序列化的对象,值最大存储为 512M

  • 简单使用举例: set key value、get key 等

  • 应用场景:共享 session、分布式锁,计数器、限流。

  • 内部编码有 3 种,int(8 字节长整型)/embstr(小于等于 39 字节字符串)/raw(大于 39 个字节字符串)

C 语言的字符串是 char[]实现的,而 Redis 使用 SDS(simple dynamic string) 封装,sds 源码如下:

struct sdshdr{
  unsigned int len; // 标记buf的长度
  unsigned int free; //标记buf中未使用的元素个数
  char buf[]; // 存放元素的坑
}

SDS 结构图如下:

Redis 为什么选择 SDS 结构,而 C 语言原生的 char[]不香吗?

举例其中一点,SDS 中,O(1)时间复杂度,就可以获取字符串长度;而 C 字符串,需要遍历整个字符串,时间复杂度为 O(n)

Hash(哈希)

  • 简介:在 Redis 中,哈希类型是指 v(值)本身又是一个键值对(k-v)结构

  • 简单使用举例:hset key field value 、hget key field

  • 内部编码:ziplist(压缩列表) 、hashtable(哈希表)

  • 应用场景:缓存用户信息等。

  • 注意点:如果开发使用 hgetall,哈希元素比较多的话,可能导致 Redis 阻塞,可以使用 hscan。而如果只是获取部分 field,建议使用 hmget。

字符串和哈希类型对比如下图:

List(列表)

  • 简介:列表(list)类型是用来存储多个有序的字符串,一个列表最多可以存储 2^32-1 个元素。

  • 简单实用举例:lpush key value [value ...] 、lrange key start end

  • 内部编码:ziplist(压缩列表)、linkedlist(链表)

  • 应用场景:消息队列,文章列表,

一图看懂 list 类型的插入与弹出:

list 应用场景参考以下:

lpush+lpop=Stack(栈)

lpush+rpop=Queue(队列)

lpsh+ltrim=Capped Collection(有限集合)

lpush+brpop=Message Queue(消息队列)

Set(集合)

  • 简介:集合(set)类型也是用来保存多个的字符串元素,但是不允许重复元素

  • 简单使用举例:sadd key element [element ...]、smembers key

  • 内部编码:intset(整数集合)、hashtable(哈希表)

  • 注意点:smembers 和 lrange、hgetall 都属于比较重的命令,如果元素过多存在阻塞 Redis 的可能性,可以使用 sscan 来完成。

  • 应用场景:用户标签,生成随机数抽奖、社交需求。

有序集合(zset)

  • 简介:已排序的字符串集合,同时元素不能重复

  • 简单格式举例:zadd key score member [score member ...],zrank key member

  • 底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)

  • 应用场景:排行榜,社交需求(如用户点赞)。

2.2 Redis 的三种特殊数据类型

  • Geo:Redis3.2 推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。

  • HyperLogLog:用来做基数统计算法的数据结构,如统计网站的 UV。

  • Bitmaps :用一个比特位来映射某个元素的状态,在 Redis 中,它的底层是基于字符串类型实现的,可以把 bitmaps 成作一个以比特位为单位的数组

3. Redis 为什么这么快?

3.1 基于内存存储实现

我们都知道内存读写是比在磁盘快很多的,Redis 基于内存存储实现的数据库,相对于数据存在磁盘的 MySQL 数据库,省去磁盘 I/O 的消耗。

3.2 高效的数据结构

我们知道,Mysql 索引为了提高效率,选择了 B+树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。先看下 Redis 的数据结构 &内部编码图:

SDS 简单动态字符串

符串长度处理:Redis 获取字符串长度,时间复杂度为 O(1),而 C 语言中,需要从头开始遍历,复杂度为 O(n);

空间预分配:字符串修改越频繁的话,内存分配越频繁,就会消耗性能,而 SDS 修改和空间扩充,会额外分配未使用的空间,减少性能损耗。

惰性空间释放:SDS 缩短时,不是回收多余的内存空间,而是 free 记录下多余的空间,后续有变更,直接使用 free 中记录的空间,减少分配。

二进制安全:Redis 可以存储一些二进制数据,在 C 语言中字符串遇到'\0'会结束,而 SDS 中标志字符串结束的是 len 属性。

字典

Redis 作为 K-V 型内存数据库,所有的键值就是用字典来存储。字典就是哈希表,比如 HashMap,通过 key 就可以直接获取到对应的 value。而哈希表的特性,在 O(1)时间复杂度就可以获得对应的值。

跳跃表

跳跃表是 Redis 特有的数据结构,就是在链表的基础上,增加多级索引提升查找效率。

跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。

3.3 合理的数据编码

Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是 redis 设计者总结优化的结果。

String:如果存储数字的话,是用 int 类型的编码;如果存储非数字,小于等于 39 字节的字符串,是 embstr;大于 39 个字节,则是 raw 编码。

List:如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节(默认),使用 ziplist 编码,否则使用 linkedlist 编码

Hash:哈希类型元素个数小于 512 个,所有值小于 64 字节的话,使用 ziplist 编码,否则使用 hashtable 编码。

Set:如果集合中的元素都是整数且元素个数小于 512 个,使用 intset 编码,否则使用 hashtable 编码。

Zset:当有序集合的元素个数小于 128 个,每个元素的值小于 64 字节时,使用 ziplist 编码,否则使用 skiplist(跳跃表)编码

3.4 合理的线程模型

I/O 多路复用

I/O 多路复用

多路 I/O 复用技术可以让单个线程高效的处理多个连接请求,而 Redis 使用用 epoll 作为 I/O 多路复用技术的实现。并且,Redis 自身的事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间。

什么是 I/O 多路复用?

I/O :网络 I/O

多路 :多个网络连接

复用:复用同一个线程。

IO 多路复用其实就是一种同步 IO 模型,它实现了一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;而没有文件句柄就绪时,就会阻塞应用程序,交出 cpu。

单线程模型

  • Redis 是单线程模型的,而单线程避免了 CPU 不必要的上下文切换和竞争锁的消耗。也正因为是单线程,如果某个命令执行过长(如 hgetall 命令),会造成阻塞。Redis 是面向快速执行场景的数据库。,所以要慎用如 smembers 和 lrange、hgetall 等命令。

  • Redis 6.0 引入了多线程提速,它的执行命令操作内存的仍然是个单线程。

3.5 虚拟内存机制

Redis 直接自己构建了 VM 机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。

Redis 的虚拟内存机制是啥呢?

虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过 VM 功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。

4. 什么是缓存击穿、缓存穿透、缓存雪崩?

4.1 缓存穿透问题

先来看一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。

 

缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。

通俗点说,读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。

缓存穿透一般都是这几种情况产生的:

  • 业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个 userid 查询有没有守护。

  • 业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。

  • 黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。

如何避免缓存穿透呢? 一般有三种方法。

  • 1.如果是非法请求,我们在 API 入口,对参数进行校验,过滤非法值。

  • 2.如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)

  • 3.使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。

布隆过滤器原理:它由初始值为 0 的位图数组和 N 个哈希函数组成。一个对一个 key 进行 N 个 hash 算法获取 N 个值,在比特数组中将这 N 个值散列后设定为 1,然后查的时候如果特定的这几个位置都为 1,那么布隆过滤器判断该 key 存在。

4.2 缓存雪奔问题

缓存雪奔: 指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至 down 机。

  • 缓存雪奔一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5 小时+0 到 1800 秒酱紫。

  • Redis 故障宕机也可能引起缓存雪奔。这就需要构造 Redis 高可用集群啦。

4.3 缓存击穿问题

缓存击穿: 指热点 key 在某个时间点过期的时候,而恰好在这个时间点对这个 Key 有大量的并发请求过来,从而大量的请求打到 db。

缓存击穿看着有点像,其实它两区别是,缓存雪奔是指数据库压力过大甚至 down 机,缓存击穿只是大量并发请求到了 DB 数据库层面。可以认为击穿是缓存雪奔的一个子集吧。有些文章认为它俩区别,是区别在于击穿针对某一热点 key 缓存,雪奔则是很多 key。

解决方案就有两种:

  • 1.使用互斥锁方案。缓存失效时,不是立即去加载 db 数据,而是先使用某些带成功返回的原子操作命令,如(Redis 的 setnx)去操作,成功的时候,再去加载 db 数据库数据和设置缓存。否则就去重试获取缓存。

  • 2. “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。

5. 什么是热 Key 问题,如何解决热 key 问题

什么是热 Key 呢?在 Redis 中,我们把访问频率高的 key,称为热点 key。

如果某一热点 key 的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。

 

而热点 Key 是怎么产生的呢?主要原因有两个:

用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。

请求分片集中,超过单 Redi 服务器的性能,比如固定名称 key,Hash 落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点 Key 问题。

那么在日常开发中,如何识别到热点 key 呢?

凭经验判断哪些是热 Key;

客户端统计上报;

服务代理层上报

如何解决热 key 问题?

Redis 集群扩容:增加分片副本,均衡读流量;

将热 key 分散到不同的服务器中;

使用二级缓存,即 JVM 本地缓存,减少 Redis 的读请求。

6. Redis 过期策略和内存淘汰策略

 

6.1 Redis 的过期策略

我们在 set key 的时候,可以给它设置一个过期时间,比如 expire key 60。指定这 key60s 后过期,60s 后,redis 是如何处理的嘛?我们先来介绍几种过期策略:

定时过期

每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即对 key 进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

惰性过期

只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。

定期过期

每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。

expires 字典会保存所有设置了过期时间的 key 的过期时间数据,其中,key 是指向键空间中的某个键的指针,value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。

Redis 中同时使用了惰性过期和定期过期两种过期策略。

  • 假设 Redis 当前存放 30 万个 key,并且都设置了过期时间,如果你每隔 100ms 就去检查这全部的 key,CPU 负载会特别高,最后可能会挂掉。

  • 因此,redis 采取的是定期过期,每隔 100ms 就随机抽取一定数量的 key 来检查和删除的。

  • 但是呢,最后可能会有很多已经过期的 key 没被删除。这时候,redis 采用惰性删除。在你获取某个 key 的时候,redis 会检查一下,这个 key 如果设置了过期时间并且已经过期了,此时就会删除。

但是呀,如果定期删除漏掉了很多过期的 key,然后也没走惰性删除。就会有很多过期 key 积在内存内存,直接会导致内存爆的。或者有些时候,业务量大起来了,redis 的 key 被大量使用,内存直接不够了,运维小哥哥也忘记加大内存了。难道 redis 直接这样挂掉?不会的!Redis 用 8 种内存淘汰策略保护自己~

6.2 Redis 内存淘汰策略

volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的 key 中使用 LRU(最近最少使用)算法进行淘汰;

allkeys-lru:当内存不足以容纳新写入数据时,从所有 key 中使用 LRU(最近最少使用)算法进行淘汰。

volatile-lfu:4.0 版本新增,当内存不足以容纳新写入数据时,在过期的 key 中,使用 LFU 算法进行删除 key。

allkeys-lfu:4.0 版本新增,当内存不足以容纳新写入数据时,从所有 key 中使用 LFU 算法进行淘汰;

volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的 key 中,随机淘汰数据;。

allkeys-random:当内存不足以容纳新写入数据时,从所有 key 中随机淘汰数据。

volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的 key 中,根据过期时间进行淘汰,越早过期的优先被淘汰;

noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。

7.说说 Redis 的常用应用场景

  • 缓存

  • 排行榜

  • 计数器应用

  • 共享 Session

  • 分布式锁

  • 社交网络

  • 消息队列

  • 位操作

7.1 缓存

我们一提到 redis,自然而然就想到缓存,国内外中大型的网站都离不开缓存。合理的利用缓存,比如缓存热点数据,不仅可以提升网站的访问速度,还可以降低数据库 DB 的压力。并且,Redis 相比于 memcached,还提供了丰富的数据结构,并且提供 RDB 和 AOF 等持久化机制,强的一批。

7.2 排行榜

当今互联网应用,有各种各样的排行榜,如电商网站的月度销量排行榜、社交 APP 的礼物排行榜、小程序的投票排行榜等等。Redis 提供的 zset 数据类型能够实现这些复杂的排行榜。

比如,用户每天上传视频,获得点赞的排行榜可以这样设计:

  • 1.用户 Jay 上传一个视频,获得 6 个赞,可以酱紫:

zadd user:ranking:2021-03-03 Jay 3
  • 2.过了一段时间,再获得一个赞,可以这样:

  • zincrby user:ranking:2021-03-03 Jay 1
    

  • 3.如果某个用户 John 作弊,需要删除该用户:

  • zrem user:ranking:2021-03-03 John
    

Redis 是基于内存的非关系型 K-V 数据库,既然它是基于内存的,如果 Redis 服务器挂了,数据就会丢失。为了避免数据丢失了,Redis 提供了持久化,即把数据保存到磁盘。

Redis 提供了 RDB 和 AOF 两种持久化机制,它持久化文件加载流程如下:

7.6 社交网络

赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适保存 这种类型的数据,Redis 提供的数据结构可以相对比较容易地实现这些功能。

7.7 消息队列

消息队列是大型网站必用中间件,如 ActiveMQ、RabbitMQ、Kafka 等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis 提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。

7.8 位操作

用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。腾讯 10 亿用户,要几个毫秒内查询到某个用户是否在线,能怎么做?千万别说给每个用户建立一个 key,然后挨个记(你可以算一下需要的内存会很恐怖,而且这种类似的需求很多。这里要用到位操作——使用 setbit、getbit、bitcount 命令。原理是:redis 内构建一个足够长的数组,每个数组元素只能是 0 和 1 两个值,然后这个数组的下标 index 用来表示用户 id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0 和 1)来构建一个记忆系统。

8. Redis 的持久化机制有哪些?优缺点说说

  • 4.展示获取赞数最多的 3 个用户

  • zrevrangebyrank user:ranking:2021-03-03 0 2
    

    7.3 计数器应用

    各大网站、APP 应用经常需要计数器的功能,如短视频的播放数、电商网站的浏览数。这些播放数、浏览数一般要求实时的,每一次播放和浏览都要做加 1 的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis 天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。

    7.4 共享 Session

    如果一个分布式 Web 服务将用户的 Session 信息保存在各自服务器,用户刷新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用 Redis 将用户的 Session 进行集中管理,每次用户更新或者查询登录信息都直接从 Redis 中集中获取。

    7.5 分布式锁

    几乎每个互联网公司中都使用了分布式部署,分布式服务下,就会遇到对同一个资源的并发访问的技术难题,如秒杀、下单减库存等场景。

  • 用 synchronize 或者 reentrantlock 本地锁肯定是不行的。

  • 如果是并发量不大话,使用数据库的悲观锁、乐观锁来实现没啥问题。

  • 但是在并发量高的场合中,利用数据库锁来控制资源的并发访问,会影响数据库的性能。

  • 实际上,可以用 Redis 的 setnx 来实现分布式的锁。

8.1 RDB

RDB,就是把内存数据以快照的形式保存到磁盘上。

什么是快照?可以这样理解,给当前时刻的数据,拍一张照片,然后保存下来。

RDB 持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是 Redis 默认的持久化方式。执行完操作后,在指定目录下会生成一个 dump.rdb 文件,Redis 重启的时候,通过加载 dump.rdb 文件来恢复数据。RDB 触发机制主要有以下几种:

RDB 的优点

  • 适合大规模的数据恢复场景,如备份,全量复制等

RDB 缺点

  • 没办法做到实时持久化/秒级持久化。

  • 新老版本存在 RDB 格式兼容问题

AOF

AOF(append only file) 持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行 AOF 文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。

AOF 的工作流程如下:


....博主太懒了字数太多了,不想写了....文章已经做成PDF,有需要的朋友可以私信我免费获取!

猜你喜欢

转载自blog.csdn.net/weixin_70730532/article/details/125871034