文章
努力加载图片中...
Redis 面试题学习
  • 5452 字

  • 7 分钟

  • 6 次

  • 2025-09-28
标签:

1 讲一下Redis底层的数据结构

基本数据结构:String、Hash、List、Set、Zset。 特殊数据结构,还有:BitMap、HyperLogLog、GEO、Stream。

2 ZSet用过吗

使用 Zset 实现博文点赞排行榜

  1. ZADD key score member 把每篇文章的 ID 作为 member,点赞数作为 score 加入有序集合。
  2. 当某篇文章新增点赞时,用 ZINCREBY key increment member 原子性地增加分数。
  3. 查询 Top N 排行榜时,使用 ZREVRANGE key start stop WITHSCORES 从高到低获取。
  4. 如果要查询某个分数区间的文章,可以用 ZRANGEBYSCORE
  5. 查询某篇文章的具体排名或者分数,可以用 ZSCORE

3 Zset 底层是怎么实现的?

Zset 类型的底层数据结构是压缩列表跳表实现的

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于64字节时,Redis 就会使用压缩列表作为 Zset 类型的底层数据结构。
  • 如果有序集合不满足上述条件,Redis 就会使用跳表作为 Zset 类型的底层数据结构。

4 Redis 为什么使用跳表而不是用 B+ 树?

主要是因为 Redis 是内存数据库,不需要像传统数据库那样优化磁盘 I/O,而跳表在内存环境下的综合表现更优。主要有三点:

  1. 跳表通过随机层数实现概率性平衡,插入和删除操作只需修改相邻节点指针,没有递归上浮或结构调整的开销。不需要像 B+ 树那样处理复杂的节点分裂、合并、旋转操作。写入性能更好,出错概率低,也更容易维护。

  2. 跳表是链式结构,节点分配灵活,内存布局紧凑。而 B+ 树通常按页组织,存在内部碎片,在内存中反而不如跳表高效。

5 介绍一下 Redis 中的 listpack

Redis 的 listpack 是为了彻底替代压缩列表(ziplist),解决其长期存在的“连锁更新”问题而引入的新数据结构。

  1. listpack 借鉴了 ziplist 的优点:使用一块连续内存来紧凑存储数据,节省内存。但每个 listpack 节点只记录自己的总长度(len),不再记录前一个节点的长度。
  2. 节点结构包括三个部分
    • encoding:标明当前元素的数据类型和编码方式;
    • data:实际存储的数据(整数或字符串);
    • lenencoding + data 的总字节数。
  3. 整个 listpack 是从头部到尾部顺序排列节点,末尾有一个结束标记 EOF。解析时可以从头开始逐个读取,每个节点自包含长度信息,无需依赖前驱节点。以此避免了连锁更新。

6 哈希表是怎么扩容的?

  1. 触发扩容操作时,Redis 会为原哈希表创建一个更大的哈希表,通常是原哈希表的两倍大小
  2. Redis 哈希表的数据迁移使用了渐进式 rehash 的方法,在每次对字典的增删改查操作中,除了执行对应操作外,还会顺序将原哈希表中索引位置上的所有 key-value 迁移到新哈希表上
  3. 随着请求次数的增多,最终会将原哈希表的键值全部迁移到新哈希表,完成扩容。

7 哈希表扩容的时候,有读请求怎么查?

查找一个 key 的值的话,先会在哈希表 1里面进行查找,如果没找到,就会继续到哈希表 2 里面进行找到。

8 String 是使用什么存储的?

使用 SDS 的数据结构,主要包含四个部分

  1. len,记录字符串长度。
  2. alloc,分配给字符数组的空间长度。
  3. flags,用来表示不同类型的 SDS。
  4. buf[],字符数组,用来保存时机数据。

9 String 为什么不用 c 语言中的字符串?

Redis 中的 String 在 c 语言的字符串中,增加了 lenallocflags 三个元数据来解决 C 语言字符串的缺陷。主要有三点

  1. 增加 len 可以用 O(1) 的复杂度获取字符串长度,不需要像 C 语言那样遍历。
  2. 使用二进制安全的的字符数组保存字符串,使得 Redis 不仅可以保存文本数据,还可以保存其他格式的二进制数据。
  3. Redis 中的 String 通过 alloc - len 计算剩余可用空间大小,若发现空间不够用,就会自动扩大 SDS 的空间大小,以此避免出现缓冲区溢出

10 Redis 为什么快?

  1. Redis 大部分操作都在内存中完成,并且采用了高效的数据结构,所以性能瓶颈只能是内存和网络带宽,而非 CPU,所以采用单线程
  2. Redis 采用了 I/O 多路复用机制处理大量 Socket 请求,也就是允许 内核存在多个监听 Socket 和已连接 Socket。一旦有请求达到,就会交给 Redis 线程处理,实现一个线程处理多个 IO 流

11 Redis 哪些地方使用了多线程?

Redis 6.0 后,启动时会创建 7 个线程

  1. 主线程 Redis-server,主要负责执行命令。
  2. bio_close_filebio_aof_fsyncbio_lazy_free,三个后台线程,分别处理关闭文件任务、AOF 刷盘任务、释放内存任务。
  3. io_thd_1, io_thd_2io_thd_3,三个 I/O 线程,用于分摊 Redis 网络的 I/O 压力。

12 Redis 怎么实现的io多路复用?

首先 socket 客户端连接 Redis 时,会生成一个套接字描述符,也就是 fd,然后 Redis 会将 fd 注册到监听列表中,监听列表会同时监听多个 FD 是否有数据到来,一有数据到来就会通知事件处理器处理,不会阻塞整个服务。并且整个过程是单线程驱动的,没有锁竞争,既避免了多线程开销,同时也解决阻塞 I/O 的问题。

13 Redis的网络模型是怎样的?

在 Redis 6.0 之前,采用的是单 Reactor 单线程的模型,也就是一个线程来负责监听客户端连接,读写数据、解析和执行命令。这样做虽然简单高效,但是只能用一个 CPU 核心,无法充分利用多核性能,并且如果某个操作耗时很长,也会阻塞其他请求

Redis 6.0 后,使用了网络 I/O 多线程化,也就是网络读写放到了多个线程处理,但命令执行的核心逻辑还是单线程。这样既保证了单线程执行命令的原子性,避免锁竞争,又通过多线程提升了网络吞吐量

14 如何实现 redis 原子性?

Redis 执行一条命令的时候,是具备原子性的,因为 redis 执行命令的时候是单线程处理的,不存在多线程安全的问题。 如果要保证多条命令的原子性,可以使用 lua 脚本,将多个操作写到一个 Lua 脚本中,Redis 会将整个 Lua 脚本当作整体执行,执行过程不会被其他命令打断,以此保证 Lua 脚本中操作的原子性。

15 除了 Lua 脚本有没有什么也能保证 redis 的原子性?

Redis 事务也可以保证多个操作的原子性,可以使用 MULTIEXEC ,在事务没有发生任何错误的情况下,可以保证操作的原子性;但如果某个操作执行错误了,由于 Redis 中没有回滚机制,所以不会保证操作的原子性。

16 Redis 有哪 2 种持久化方式?分别的优缺点是什么?

  1. AOF 日志:每执行一条写操作,就把该命令以追加的方式写入到一个文件里。
  • 优点:
    1. 数据更安全,最多丢失最后一次写入前的数据。
    2. 支持多种写回策略,灵活平衡性能和安全。
    3. AOF 的文件可读,便于排查问题,也支持重写机制来减少体积。
  • 缺点:
    1. 文件通常比 RDB 大,占用磁盘空间多
    2. 恢复速度慢,因为要一条条执行命令。
    3. 高频写入时,可能影响性能
  1. RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘。
  • 优点:
    1. 文件紧凑,体积小,适合备份和快速恢复。
    2. 恢复速度快,直接加载进内存。
    3. 生成快照可以由子进程完成,不影响主线程性能
  • 缺点:
    1. 数据安全性相对较低,可能丢失最后一次快照的数据。
    2. 如果在创建快照到恢复期间由写操作,恢复后的数据可能于故障前不一致。

17 过期删除策略和内存淘汰策略有什么区别?

  • 内存淘汰策略是在内存满了的时候,redis 会触发内存淘汰策略,来淘汰一些不必要的内存资源,以腾出空间,来保存新的内容。
  • 过期键删除策略是将已过期的键值对进行删除,Redis 采用的删除策略是惰性删除+定期删除。

18 介绍一下 Redis 内存淘汰策略

主要分为不进行数据淘汰的策略和进行数据淘汰的策略:

  1. 不进行数据淘汰的策略
    • noeviction:当运行内存超过最大设置内存时,不淘汰任何数据,如果有新的数据写入,会报错通知禁止写入。
  2. 进行数据淘汰的策略
    • 在设置了过期时间的数据中进行淘汰
      1. volatile-random:随机淘汰设置了过期时间的任意键值;
      2. volatile-ttl:优先淘汰更早过期的键值。
      3. volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
      4. volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
    • 在所有数据范围内进行淘汰:
      1. allkeys-random:随机淘汰任意键值;
      2. allkeys-lru:淘汰整个键值中最久未使用的键值;
      3. allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。

19 介绍一下 Redis 过期删除策略

Redis 的过期删除策略采用的是 “惰性删除 + 定期删除” 两种方式配合使用,目的是在 CPU 性能内存利用率 之间取得一个平衡。

  • 惰性删除,具体是当客户端尝试获取一个 key 时,Redis 会先调用 expireIfNeeded 这个函数检查是 否已过期,如果过期了,立即删除,然后返回 null;如果没过期,就正常返回数据。
  • 定期删除,默认是每秒执行 10 次,每次随机抽取 20 个设置了过期时间的 key,删除其中已经过期的,如果发现超过 25% 的 key 都过期了,那就再随机抽取一批继续删除。但是整个过程有 25 ms 的限制,避免阻塞主线程。

20 Redis的缓存失效会不会立即删除?

不会,Redis 的过期删除策略是选择「惰性删除+定期删除」这两种策略配和使用。

  • 惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
  • 定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。

21 那为什么不过期立即删除?

Redis 没有在 key 过期后立即删除,主要是为了避免对 CPU 性能造成过大压力。如果每个过期 key 都要精确地在到期时立刻删除,就需要一个高精度的定时器来追踪每一个 key,这会带来大量的系统调用和 CPU 开销。使用惰性删除+定期删除的策略,在内存利用率CPU 性能 之间取得了良好的平衡,在保证服务器响应速度和稳定性的情况下,允许一定程度的内存冗余。

22 为什么使用 redis ?

主要是因为 Redis 具备「高性能」和「高并发」两种特性

  1. 高性能是因为 Redis 是基于内存操作的,所有数据都存在内存中,读写速度非常快。
  2. 高并发是因为 Redis 单机就能达到 10 万 QPS 以上,而 MySQL 单机一般很难超过 1万 QPS。

23 为什么 redis 比 mysql 要快?

  1. Redis 是基于内存存储的 NoSQL 数据库,MySQL 是基于磁盘存储的关系型数据库。由于内存存储速度很快,Redis 读写速度也久很快,无需像 MySQL 那样进行磁盘 I/O 操作。
  2. Redis 是基于键值对存储数据的,支持简单的数据结构。而 MySQL 需要定义表结构,索引等复杂的关系型数据结构,因此在某些场景下,Redis 的数据操作相比 MySQL 更为简单高效。
  3. Redis 采用单线程模型避免了多线程之间的竞争,省去了多线程切换带来的开销,也不会导致死锁问题。

24 本地缓存和 Redis 缓存的区别?

本地缓存:

  • 优点:
    1. 访问速度极快,没有网络开销,延迟低。
  • 缺点:
    1. 数据只存储在当前节点,集群部署时数据会不一致。
    2. 本机重启或者宕机了,数据就会丢失
    3. 存储量受到单机内存限制,不适合缓存大量数据。

Redis 缓存:

  • 优点:
    1. 多个服务实例共享同一份缓存,保证一致性;
    2. 支持持久化、主从复制、集群,可扩展性强,能存大量数据;
    3. 使某个应用节点重启,缓存还在。
  • 缺点:
    1. 要走网络请求,相比本地缓存有一定网络延迟和带宽开销
    2. 应用重启或者宕机了,数据就会丢失。
    3. 多了一层外部依赖,Redis 如果挂了,可能影响实际业务。

25 redis 应用场景是什么?

  1. 缓存,将热点数据存储在内存中,可以提高访问速度,减轻数据库负载。
  2. 排行榜,使用 Zset 可以很方便的进行数据排序和排名。
  3. 分布式锁,利用多个服务实例共享同一份缓存特性实现分布式锁。
  4. 计数器,由于 Redis 的原子操作和高性能,可以实现数据统计或计数功能。
  5. 消息队列,利用 Redis 的发布订阅功能可以实现轻量级的消息队列。

26 Redis 除了缓存,还有哪些应用?

  1. 排行榜,使用 Zset 可以很方便的进行数据排序和排名。
  2. 分布式锁,利用多个服务实例共享同一份缓存特性实现分布式锁。
  3. 计数器,由于 Redis 的原子操作和高性能,可以实现数据统计或计数功能。
  4. 消息队列,利用 Redis 的发布订阅功能可以实现轻量级的消息队列。

27 Redis 支持并发操作吗?

支持,主要原因有两点:

  1. 单个 Redis 命令是原子性的,一个命令要么执行成功,要么完全不执行。
  2. Redis 支持事务,可以将一系列操作放在一个事务中执行,使用 MULTIEXECDISCARDWATCH等命令执行任务,确保一系列操作的原子性。

28 Redis 的大 Key 问题是什么?

Redis 大 Key 问题 指的是某个 key 对应的 value 值所占空间比较大,带来的问题是,Redis 的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。

29 大 Key 问题的缺点?

  1. 内存占用过高,消耗大量 CPU 时间,导致系统性能下降。
  2. 对大 Key 的操作可能会导致 Redis 实例阻塞,导致其他请求无法得到响应。
  3. 获取大 Key 时,产生的网络流量较大,可能造成服务器带宽被打满。
  4. 主从同步时,由于需要传输大量数据,可能导致主从同步延迟
  5. 在集群模式中,大 Key 还可能导致无法使某个数据分片的内存资源达到均衡

30 Redis 大 key 如何解决?

  1. 可以把一个大 Key 拆分成多个小 Key,例如把包含几万个字段的 Hash,按业务维度拆成多个小 Hash。
  2. 将大 Key 保存至其他存储服务
  3. 可以通过监控系统设置合理的 Redis 内存报警阈值进行提醒,例如Redis内存使用率超过70% 等。

31 什么是热 key?

  • QPS集中在特定的Key:Redis实例的总QPS(每秒查询率)为10,000,而其中一个Key的每秒访问量达到了7,000。
  • 带宽使用率集中在特定的Key:对一个拥有上千个成员且总大小为1 MB的HASH Key每秒发送大量的HGETALL 操作请求。
  • CPU使用时间占比集中在特定的Key:对一个拥有数万个成员的Key(ZSET类型)每秒发送大量的ZRANGE 操作请求。

32 如何保证 redis 和 mysql 数据缓存一致性问题?

  • 对于读数据,可以选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。
  • 对于写数据,可以选择更新 db 后,再删除缓存。
  • 对于一些需要在 Redis 中长期存在数据,可以使用定时任务定期同步到 MySQL。

33 缓存雪崩、击穿、穿透是什么?怎么解决?

  1. 缓存雪崩指的是大量缓存数据在同一时间过期,或者 Redis 突然宕机,导致所有请求都打到数据库,数据库瞬间压力飙升,可能直接挂掉。

    • 解决办法
      1. 对数据的过期时间,添加一定范围内的随机数,确保数据不会在同一时间过期。
      2. 用户请求时,若数据不在 Redis 里,就添加一个互斥锁,保证只有一个请求来构建缓存。
  2. 缓存击穿指的是某个特别热门的 key过期了,瞬间大量请求涌进来,全都绕过缓存查数据库,把数据库打崩。

    • 解决办法
      1. 用户请求时,若数据不在 Redis 里,就添加一个互斥锁,保证只有一个请求来构建缓存。
      2. 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备过期时,通知后台线程更新。
  3. 缓存穿透指的是请求一个既不在缓存也不在数据库的数据,比如 id = -1 或乱写 ID,每次缓存没命中,还得查数据库,导致恶意请求过多。

    • 解决办法
      1. 可以对非法请求进行校验,例如校验参数是否存在,是否是非法值,直接返回错误,避免进一步访问缓存和数据库。
      2. 缓存空值或者默认值,后续请求就可以通过缓存读取到空值或默认值,不会继续查询数据库。
      3. 可以在写入数据库数据时,使用布隆过滤器做个标记,在确认缓存失效后,使用布隆过滤器判断数据是否存在,不存在就查询数据库来判断,这样大量请求只会查询 Redis 和布隆过滤器。

34 布隆过滤器原理介绍一下

它由两部分组成:

  • 一个初始值全为 0 的位图数组(也就是一串二进制的 0 和 1);
  • 和 N 个不同的哈希函数

主要工作流程分为两步:

  1. 添加一个元素(比如把数据写入数据库时)

    • 用那 N 个哈希函数,对这个元素做哈希,得到 N 个哈希值;
    • 每个哈希值对位图长度取模,算出在位图中的位置;
    • 把这些位置上的值都设为 1。
  2. 查询一个元素是否存在

    • 同样用这 N 个哈希函数算出 N 个位置;
    • 然后去看位图中这些位置是不是全为 1
      • 如果有任意一个是 0,说明这个元素一定不存在
      • 如果全为 1,说明这个元素可能存在(但不一定,可能是因为别的元素“污染”了这些位置)。
作者: Xigrut发布时间: 2025-09-28 19:00:38上次编辑时间: 2026-06-18 19:25:58 许可协议: CC BY-NC-SA 4.0
留言区