5452 字
7 分钟
6 次
- 2025-09-28
1 讲一下Redis底层的数据结构
基本数据结构:String、Hash、List、Set、Zset。 特殊数据结构,还有:BitMap、HyperLogLog、GEO、Stream。
2 ZSet用过吗
使用 Zset 实现博文点赞排行榜
- 用
ZADD key score member把每篇文章的 ID 作为 member,点赞数作为 score 加入有序集合。 - 当某篇文章新增点赞时,用
ZINCREBY key increment member原子性地增加分数。 - 查询 Top N 排行榜时,使用
ZREVRANGE key start stop WITHSCORES从高到低获取。 - 如果要查询某个分数区间的文章,可以用
ZRANGEBYSCORE - 查询某篇文章的具体排名或者分数,可以用
ZSCORE
3 Zset 底层是怎么实现的?
Zset 类型的底层数据结构是压缩列表或跳表实现的
- 如果有序集合的元素个数小于 128 个,并且每个元素的值小于64字节时,Redis 就会使用压缩列表作为 Zset 类型的底层数据结构。
- 如果有序集合不满足上述条件,Redis 就会使用跳表作为 Zset 类型的底层数据结构。
4 Redis 为什么使用跳表而不是用 B+ 树?
主要是因为 Redis 是内存数据库,不需要像传统数据库那样优化磁盘 I/O,而跳表在内存环境下的综合表现更优。主要有三点:
-
跳表通过随机层数实现概率性平衡,插入和删除操作只需修改相邻节点指针,没有递归上浮或结构调整的开销。不需要像 B+ 树那样处理复杂的节点分裂、合并、旋转操作。写入性能更好,出错概率低,也更容易维护。
-
跳表是链式结构,节点分配灵活,内存布局紧凑。而 B+ 树通常按页组织,存在内部碎片,在内存中反而不如跳表高效。
5 介绍一下 Redis 中的 listpack
Redis 的 listpack 是为了彻底替代压缩列表(ziplist),解决其长期存在的“连锁更新”问题而引入的新数据结构。
listpack借鉴了 ziplist 的优点:使用一块连续内存来紧凑存储数据,节省内存。但每个 listpack 节点只记录自己的总长度(len),不再记录前一个节点的长度。- 节点结构包括三个部分
encoding:标明当前元素的数据类型和编码方式;data:实际存储的数据(整数或字符串);len:encoding + data的总字节数。
- 整个 listpack 是从头部到尾部顺序排列节点,末尾有一个结束标记
EOF。解析时可以从头开始逐个读取,每个节点自包含长度信息,无需依赖前驱节点。以此避免了连锁更新。
6 哈希表是怎么扩容的?
- 触发扩容操作时,Redis 会为原哈希表创建一个更大的哈希表,通常是原哈希表的两倍大小。
- Redis 哈希表的数据迁移使用了渐进式 rehash 的方法,在每次对字典的增删改查操作中,除了执行对应操作外,还会顺序将原哈希表中索引位置上的所有 key-value 迁移到新哈希表上 。
- 随着请求次数的增多,最终会将原哈希表的键值全部迁移到新哈希表,完成扩容。
7 哈希表扩容的时候,有读请求怎么查?
查找一个 key 的值的话,先会在哈希表 1里面进行查找,如果没找到,就会继续到哈希表 2 里面进行找到。
8 String 是使用什么存储的?
使用 SDS 的数据结构,主要包含四个部分
len,记录字符串长度。alloc,分配给字符数组的空间长度。flags,用来表示不同类型的 SDS。buf[],字符数组,用来保存时机数据。
9 String 为什么不用 c 语言中的字符串?
Redis 中的 String 在 c 语言的字符串中,增加了 len、alloc、flags 三个元数据来解决 C 语言字符串的缺陷。主要有三点
- 增加
len可以用 O(1) 的复杂度获取字符串长度,不需要像 C 语言那样遍历。 - 使用二进制安全的的字符数组保存字符串,使得 Redis 不仅可以保存文本数据,还可以保存其他格式的二进制数据。
- Redis 中的 String 通过
alloc - len计算剩余可用空间大小,若发现空间不够用,就会自动扩大 SDS 的空间大小,以此避免出现缓冲区溢出。
10 Redis 为什么快?
- Redis 大部分操作都在内存中完成,并且采用了高效的数据结构,所以性能瓶颈只能是内存和网络带宽,而非 CPU,所以采用单线程。
- Redis 采用了 I/O 多路复用机制处理大量 Socket 请求,也就是允许 内核存在多个监听 Socket 和已连接 Socket。一旦有请求达到,就会交给 Redis 线程处理,实现一个线程处理多个 IO 流。
11 Redis 哪些地方使用了多线程?
Redis 6.0 后,启动时会创建 7 个线程
- 主线程
Redis-server,主要负责执行命令。 bio_close_file、bio_aof_fsync、bio_lazy_free,三个后台线程,分别处理关闭文件任务、AOF 刷盘任务、释放内存任务。io_thd_1,io_thd_2、io_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 事务也可以保证多个操作的原子性,可以使用 MULTI 和 EXEC ,在事务没有发生任何错误的情况下,可以保证操作的原子性;但如果某个操作执行错误了,由于 Redis 中没有回滚机制,所以不会保证操作的原子性。
16 Redis 有哪 2 种持久化方式?分别的优缺点是什么?
- AOF 日志:每执行一条写操作,就把该命令以追加的方式写入到一个文件里。
- 优点:
- 数据更安全,最多丢失最后一次写入前的数据。
- 支持多种写回策略,灵活平衡性能和安全。
- AOF 的文件可读,便于排查问题,也支持重写机制来减少体积。
- 缺点:
- 文件通常比 RDB 大,占用磁盘空间多。
- 恢复速度慢,因为要一条条执行命令。
- 高频写入时,可能影响性能。
- RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘。
- 优点:
- 文件紧凑,体积小,适合备份和快速恢复。
- 恢复速度快,直接加载进内存。
- 生成快照可以由子进程完成,不影响主线程性能。
- 缺点:
- 数据安全性相对较低,可能丢失最后一次快照的数据。
- 如果在创建快照到恢复期间由写操作,恢复后的数据可能于故障前不一致。
17 过期删除策略和内存淘汰策略有什么区别?
- 内存淘汰策略是在内存满了的时候,redis 会触发内存淘汰策略,来淘汰一些不必要的内存资源,以腾出空间,来保存新的内容。
- 过期键删除策略是将已过期的键值对进行删除,Redis 采用的删除策略是惰性删除+定期删除。
18 介绍一下 Redis 内存淘汰策略
主要分为不进行数据淘汰的策略和进行数据淘汰的策略:
- 不进行数据淘汰的策略
- noeviction:当运行内存超过最大设置内存时,不淘汰任何数据,如果有新的数据写入,会报错通知禁止写入。
- 进行数据淘汰的策略
- 在设置了过期时间的数据中进行淘汰
- volatile-random:随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰更早过期的键值。
- volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
- volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
- 在所有数据范围内进行淘汰:
- allkeys-random:随机淘汰任意键值;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- 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 具备「高性能」和「高并发」两种特性。
- 高性能是因为 Redis 是基于内存操作的,所有数据都存在内存中,读写速度非常快。
- 高并发是因为 Redis 单机就能达到 10 万 QPS 以上,而 MySQL 单机一般很难超过 1万 QPS。
23 为什么 redis 比 mysql 要快?
- Redis 是基于内存存储的 NoSQL 数据库,MySQL 是基于磁盘存储的关系型数据库。由于内存存储速度很快,Redis 读写速度也久很快,无需像 MySQL 那样进行磁盘 I/O 操作。
- Redis 是基于键值对存储数据的,支持简单的数据结构。而 MySQL 需要定义表结构,索引等复杂的关系型数据结构,因此在某些场景下,Redis 的数据操作相比 MySQL 更为简单高效。
- Redis 采用单线程模型避免了多线程之间的竞争,省去了多线程切换带来的开销,也不会导致死锁问题。
24 本地缓存和 Redis 缓存的区别?
本地缓存:
- 优点:
- 访问速度极快,没有网络开销,延迟低。
- 缺点:
- 数据只存储在当前节点,集群部署时数据会不一致。
- 本机重启或者宕机了,数据就会丢失。
- 存储量受到单机内存限制,不适合缓存大量数据。
Redis 缓存:
- 优点:
- 多个服务实例共享同一份缓存,保证一致性;
- 支持持久化、主从复制、集群,可扩展性强,能存大量数据;
- 使某个应用节点重启,缓存还在。
- 缺点:
- 要走网络请求,相比本地缓存有一定网络延迟和带宽开销;
- 应用重启或者宕机了,数据就会丢失。
- 多了一层外部依赖,Redis 如果挂了,可能影响实际业务。
25 redis 应用场景是什么?
- 缓存,将热点数据存储在内存中,可以提高访问速度,减轻数据库负载。
- 排行榜,使用 Zset 可以很方便的进行数据排序和排名。
- 分布式锁,利用多个服务实例共享同一份缓存特性实现分布式锁。
- 计数器,由于 Redis 的原子操作和高性能,可以实现数据统计或计数功能。
- 消息队列,利用 Redis 的发布订阅功能可以实现轻量级的消息队列。
26 Redis 除了缓存,还有哪些应用?
- 排行榜,使用 Zset 可以很方便的进行数据排序和排名。
- 分布式锁,利用多个服务实例共享同一份缓存特性实现分布式锁。
- 计数器,由于 Redis 的原子操作和高性能,可以实现数据统计或计数功能。
- 消息队列,利用 Redis 的发布订阅功能可以实现轻量级的消息队列。
27 Redis 支持并发操作吗?
支持,主要原因有两点:
- 单个 Redis 命令是原子性的,一个命令要么执行成功,要么完全不执行。
- Redis 支持事务,可以将一系列操作放在一个事务中执行,使用
MULTI、EXEC、DISCARD、WATCH等命令执行任务,确保一系列操作的原子性。
28 Redis 的大 Key 问题是什么?
Redis 大 Key 问题 指的是某个 key 对应的 value 值所占空间比较大,带来的问题是,Redis 的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。
29 大 Key 问题的缺点?
- 内存占用过高,消耗大量 CPU 时间,导致系统性能下降。
- 对大 Key 的操作可能会导致 Redis 实例阻塞,导致其他请求无法得到响应。
- 获取大 Key 时,产生的网络流量较大,可能造成服务器带宽被打满。
- 主从同步时,由于需要传输大量数据,可能导致主从同步延迟。
- 在集群模式中,大 Key 还可能导致无法使某个数据分片的内存资源达到均衡。
30 Redis 大 key 如何解决?
- 可以把一个大 Key 拆分成多个小 Key,例如把包含几万个字段的 Hash,按业务维度拆成多个小 Hash。
- 将大 Key 保存至其他存储服务。
- 可以通过监控系统设置合理的 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 缓存雪崩、击穿、穿透是什么?怎么解决?
-
缓存雪崩指的是大量缓存数据在同一时间过期,或者 Redis 突然宕机,导致所有请求都打到数据库,数据库瞬间压力飙升,可能直接挂掉。
- 解决办法
- 对数据的过期时间,添加一定范围内的随机数,确保数据不会在同一时间过期。
- 用户请求时,若数据不在 Redis 里,就添加一个互斥锁,保证只有一个请求来构建缓存。
- 解决办法
-
缓存击穿指的是某个特别热门的 key过期了,瞬间大量请求涌进来,全都绕过缓存查数据库,把数据库打崩。
- 解决办法
- 用户请求时,若数据不在 Redis 里,就添加一个互斥锁,保证只有一个请求来构建缓存。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备过期时,通知后台线程更新。
- 解决办法
-
缓存穿透指的是请求一个既不在缓存也不在数据库的数据,比如 id = -1 或乱写 ID,每次缓存没命中,还得查数据库,导致恶意请求过多。
- 解决办法
- 可以对非法请求进行校验,例如校验参数是否存在,是否是非法值,直接返回错误,避免进一步访问缓存和数据库。
- 缓存空值或者默认值,后续请求就可以通过缓存读取到空值或默认值,不会继续查询数据库。
- 可以在写入数据库数据时,使用布隆过滤器做个标记,在确认缓存失效后,使用布隆过滤器判断数据是否存在,不存在就查询数据库来判断,这样大量请求只会查询 Redis 和布隆过滤器。
- 解决办法
34 布隆过滤器原理介绍一下
它由两部分组成:
- 一个初始值全为 0 的位图数组(也就是一串二进制的 0 和 1);
- 和 N 个不同的哈希函数。
主要工作流程分为两步:
-
添加一个元素(比如把数据写入数据库时)
- 用那 N 个哈希函数,对这个元素做哈希,得到 N 个哈希值;
- 每个哈希值对位图长度取模,算出在位图中的位置;
- 把这些位置上的值都设为 1。
-
查询一个元素是否存在
- 同样用这 N 个哈希函数算出 N 个位置;
- 然后去看位图中这些位置是不是全为 1;
- 如果有任意一个是 0,说明这个元素一定不存在;
- 如果全为 1,说明这个元素可能存在(但不一定,可能是因为别的元素“污染”了这些位置)。