Skip to content

一、数据结构与底层(R01–R04) ​

R01. 实现“排行榜(按分数排序、取 Top N、查询某用户排名)”应使用 Redis 的哪种数据结构? ​

考点:数据结构选型

A. String B. List C. Hash D. ZSet(有序集合)

答案:D

【考点】五种基本数据类型的适用场景。

【结论】 排行榜需要“按分数排序 + 取 Top N + 查某用户排名”,这是 ZSet(有序集合)的标准场景 —— 选 D。

【逐项辨析】

  • A String:错。String 只能存整块值,没有排序能力 —— 要取 Top N 必须把全部数据拉到应用层自行排序,且无法直接查询某成员的排名。
  • B List:错。List 是按插入顺序排列的链表,不是按分值排序;取 Top N 只能按插入位置取,“排名”也不是按分数的排名。
  • C Hash:错。Hash 适合按字段存取一个对象(如购物车),字段之间没有序关系,既不能排序也不能取 Top N。
  • D ZSet(有序集合):正确。 每个成员绑定一个 score,内部按 score 有序,ZADD / ZREVRANGE / ZREVRANK 直接覆盖“写入 / 取 Top N / 查排名”三个动作;复杂度分别是 ZADD O(log N)、ZREVRANK O(log N)、ZREVRANGE O(log N + M)(M 为返回成员数,取 Top N 时即 O(log N + N))。

【知识点】 Redis 五种基本类型的“能力矩阵”—— 选型的关键是看需要哪种访问模式:

类型底层结构核心能力典型场景
StringSDS单值读写、计数、位操作缓存对象、计数器、分布式锁(SET NX)
Listquicklist(双向链表 + listpack)两端插入 / 弹出、按位置访问消息队列、最新列表、时间线
Hash哈希表 / listpack按字段读写对象的属性购物车、对象字段局部更新
Set哈希表 / intset去重、交并差集标签、共同好友、抽奖去重
ZSet跳表 + 哈希表按分值排序、范围查询、排名排行榜、延时队列、热搜榜

排行榜的四个动作与 ZSet 命令的对应:

需求命令复杂度
写入某用户分数ZADDO(log N)
取 Top NZREVRANGE key 0 N-1 WITHSCORESO(log N + N)
查某用户排名ZREVRANK(降序)/ ZRANK(升序)O(log N)
取分数区间成员ZRANGEBYSCOREO(log N + M)

为什么 ZSet 能做到 O(log N):因为“排序”这件事被下沉到了数据结构内部 —— 写入时即维护有序性,所以查询排名不需要遍历全部数据。用 List / Hash 做排行榜,排序成本被推迟到应用层,必然是全量遍历 O(N)。

【记忆锚点】 「要排序、要排名、要 Top N —— 一律 ZSet」 —— 选型的判据是“数据需不需要按某个分值维持有序”。

【易混对比】

  • ZSet vs Set:Set 只去重、无序;ZSet 去重 + 按 score 有序。ZSet 的成员同样唯一,但多了一个 score。
  • ZSet vs List:List 按插入位置排序(谁先来谁靠前),ZSet 按分值排序(分数高者靠前)—— 排行榜要的是后者。
  • ZSet vs Hash:Hash 是“成员 → 多个字段”的映射(一个对象);ZSet 是“成员 → 一个分数”的映射(一个排序维度)。
  • 换问法:若题干改成“实现消息队列 / 最新 N 条评论”,答案是 List;改成“统计 UV / 去重”,答案是 Set;改成“存储商品多个属性”,答案是 Hash。

【自测】 要实时统计“直播间人气榜”,支持按人气值排序、查询主播当前排名,且要求“人气值相同时按主播加入时间先后排名”。用哪种结构最合适?同分如何处理?

答:ZSet。同分可把“加入时间戳”编码进成员名 —— 因为 ZSet 在 score 相同时按成员字典序排列(Redis 固有权衡:score 相同则按成员名升序,见【知识点】),把时间戳前置于成员名即可实现“同分按先后”的稳定排序。与 R02 连考;互联网面试高频。

【知识关联】

  • 补题关联:补-27(业务场景与数据结构匹配)、补-28(编码选型)。
  • 面试/工程:选型第一问是“访问模式”:要排序/范围 → ZSet;要精确去重计数 → HyperLogLog(有误差);要位运算/连续签到 → Bitmap;要树状关系 → 不要用原生 Redis 硬扛,可用邻接表+应用层或专门图库。结构选错的代价是:内存暴涨或复杂度从 O(1) 退化到 O(N)。
  • 面试追问:① 统计 UV 用 Set 还是 HLL?(精确用 Set,海量用 HLL) ② 延迟队列常用什么?(ZSet:score=到期时间戳,轮询 ZRANGEBYSCORE + Lua 取出)

【拓展延伸】

  • 变式问法:给出业务描述选结构;或反向“下列匹配不正确的是”。
  • 参数/命令:ZADD/ZRANGEBYSCORE、PFADD/PFCOUNT、SETBIT/GETBIT/BITCOUNT、GEOADD/GEOSEARCH;编码阈值 hash-max-listpack-entries 等见补-28。

R02. Redis 中 ZSet(有序集合)的底层实现是? ​

考点:ZSet 的底层实现

A. 双向链表 B. 红黑树 C. 跳表(skiplist)+ 哈希表(dict) D. 普通数组

答案:C

【考点】跳表与哈希表的组合设计。

【结论】 ZSet 采用跳表 + 哈希表双结构:跳表管“按 score 有序”,哈希表管“成员 → score 的 O(1) 查找” —— 选 C。

【逐项辨析】

  • A 双向链表:错。双向链表查询是 O(N),无法支撑 O(log N) 的排名与范围查询;Redis 的 List 用的才是双向链表(quicklist)。
  • B 红黑树:错。Redis 的 ZSet 并没有采用红黑树 —— 红黑树是 Java TreeMap、C++ std::map 的实现,属张冠李戴。
  • C 跳表(skiplist)+ 哈希表(dict):正确。 两个结构各司其职,缺一不可(见下)。
  • D 普通数组:错。数组插入 / 删除是 O(N),无法在动态增删下维持 O(log N) 的有序访问。

【知识点】 为什么 ZSet 必须是“两个结构”?因为 ZSet 同时提供两类接口,而这两类接口的最优结构恰好相反:

操作需要的能力只靠跳表只靠哈希表双结构
ZSCORE key member按成员定位O(log N)O(1)O(1)(哈希表)
ZRANGE / ZREVRANK按分值有序、范围与排名O(log N)无法做O(log N)(跳表)

推论:只用跳表 → 按成员查 score 要 O(log N),差一个量级;只用哈希表 → 无序,范围查询与排名根本无法实现。所以 Redis 让跳表与哈希表指向同一批成员,哈希表只存“成员 → score”映射,跳表负责排序与范围。

跳表的原理:在有序链表之上建立多级索引,每层按一定概率(Redis 中晋升概率为 1/4)抽取部分节点作为上层索引,查找时从最高层开始“跳跃”、逐层下降,平均查找长度为 O(log N)。相比平衡树,跳表不需要旋转,范围查询只需在底层链表顺序前进,实现更简单。

内存优化:当元素较少时(默认成员数 ≤ 128 且每个成员长度 ≤ 64 字节),ZSet 会改用 listpack(Redis 7.0 之前为 ziplist)压缩存储,此时不建跳表与哈希表;超过阈值才转换为跳表 + 哈希表。

【记忆锚点】 「跳表管排序与范围,哈希表管按成员查分」 —— 两个结构指向同一批成员,一个负责“有序”,一个负责“定位”。

【易混对比】

  • 跳表 vs 红黑树:都能 O(log N) 查找,但跳表实现更简单、范围查询更自然(底层本身就是有序链表)、无需旋转;Redis 选跳表,Java 的有序容器选红黑树。
  • ZSet 的两种编码:小数据量用 listpack,大数据量用跳表 + 哈希表。Set 小数据量则用 intset(纯整数)或 listpack + 哈希表。
  • listpack vs ziplist:Redis 7.0 起用 listpack 取代 ziplist 作为小对象压缩编码,解决了 ziplist 的连锁更新(cascade update)问题。
  • 换问法:若题干问“ZSet 小数据量时的编码”,答案是 listpack(旧版本 ziplist),而不是跳表。

【自测】 为什么 Redis 用跳表而不是红黑树来实现 ZSet?

答:三点 —— ① 范围查询更自然:跳表底层就是有序链表,找到起点后顺序前进即可;红黑树需中序遍历。② 实现更简单:跳表靠概率平衡,无需旋转与再平衡逻辑。③ 局部调整更容易:增删只影响相邻指针,无需全局重平衡。与 R01、R03 连考;互联网面试高频。

【知识关联】

  • 补题关联:补-27(ZSet 适用场景)、补-28(listpack 小对象优化的同一思想)。
  • 面试/工程:排行榜 ZINCRBY/ZREVRANGE、延时队列、滑动窗口限流(ZSet 时间戳)都建立在“有序 + 按 member 查 score O(1)”上。面试常追问“为什么不是只用跳表”——按 member 查会退化到 O(log N),批量校验 member 是否存在也不再是 O(1)。
  • 面试追问:① 跳表与平衡树比优势?(实现简单、范围扫描友好、无需旋转) ② 小 ZSet 会退化成什么?(listpack/ziplist 连续内存,牺牲 O(log N) 换更小内存)

【拓展延伸】

  • 变式问法:问“ZSCORE 依赖哪个结构”→ 哈希表;问“ZRANGE 依赖哪个”→ 跳表;问“元素很少时编码是什么”→ listpack。
  • 参数/命令:zset-max-listpack-entries(旧名 zset-max-ziplist-entries)、zset-max-listpack-value;OBJECT ENCODING 查看当前编码;ZADD GT/LT/NX/XX 条件更新。

R03. Redis 的 String 底层使用 SDS(简单动态字符串)而非 C 原生字符串,其好处不包括? ​

考点:SDS 简单动态字符串

A. 记录字符串长度,获取长度是 O(1)(C 字符串需遍历,O(N)) B. 二进制安全,可存储图片、序列化对象等含 \0 的数据 C. 自动扩容并预留空间,避免缓冲区溢出、减少内存重分配次数 D. 支持事务的 ACID 特性

答案:D

【考点】SDS 相对 C 字符串的四项改进。

【结论】 题干问的是“不包括”,正确答案是 D —— SDS 与事务的 ACID 毫无关系;A、B、C 三项都是 SDS 的真实好处。

【逐项辨析】

  • A 记录字符串长度、取长度为 O(1):确是 SDS 的真实好处,故不选。SDS 结构中直接存 len 字段,取长度无需遍历;C 字符串要一直扫到 \0,是 O(N)。
  • B 二进制安全、可存含 \0 的数据:确是 SDS 的真实好处,故不选。SDS 用 len 判断结尾,而不是靠 \0,因此图片、序列化对象等含 \0 的字节流可以原样保存。
  • C 自动扩容并预留空间:确是 SDS 的真实好处,故不选。扩容前先检查空间,杜绝缓冲区溢出;扩容采用空间预分配、缩容采用惰性释放,减少内存重分配次数。
  • D 支持事务的 ACID 特性:正确(“不包括”的就是这一项)。 事务是 Redis 的命令组合机制,属于命令层特性;字符串的底层结构不参与事务语义,且 Redis 事务本身也不支持回滚,并不具备完整 ACID。

【知识点】 SDS(Simple Dynamic String,简单动态字符串)的定义:Redis 自己实现的字符串结构,用长度字段代替 \0 作为结尾判据。其结构大致为 len + alloc + flags + buf[],并按长度选择不同的头类型:

头类型长度范围len / alloc 占用
sdshdr5≤ 31 字节不存 alloc(长度编码进 flags),不能原地扩容
sdshdr8≤ 255 字节各 1 字节
sdshdr16≤ 65535 字节各 2 字节
sdshdr32≤ 4 GB各 4 字节
sdshdr64更大各 8 字节

按长度分档选头的目的是省内存 —— 短字符串不必背负 8 字节的长度字段。

SDS 与 C 字符串的逐项对比:

维度C 字符串SDS
取长度O(N)(遍历到 \0)O(1)(读 len)
二进制安全❌ 遇 \0 即截断✅ 用 len 判界,可存任意字节
缓冲区溢出易发生(strcat 不检查容量)扩容前检查 alloc,杜绝溢出
内存重分配每次修改都可能 realloc空间预分配 + 惰性释放,减少次数

推导:C 字符串的一切缺陷都源于“只用 \0 表示结尾”。因为结尾信息与内容耦合 → 存不了 \0(非二进制安全);因为没有独立长度 → 取长度要遍历(O(N));因为不知道剩余空间 → 拼接可能越界(缓冲区溢出)。SDS 把“长度”从内容里抽出来单独存,三个问题一次性解决,这就是“长度与内容解耦”。

空间预分配策略:修改后 len < 1 MB 时,alloc = 2 × len(即再多分配一倍);len ≥ 1 MB 时,每次多分配 1 MB。惰性释放:缩短字符串时不立即 realloc,而是更新 len 并保留 alloc,等后续需要时再复用。

【记忆锚点】 「SDS 把长度抽出来单独存」 —— 一句话记住四项好处:O(1) 取长、二进制安全、不溢出、少重分配。

【易混对比】

  • SDS vs C 字符串:核心差异是“结尾判据”—— C 靠 \0,SDS 靠 len。
  • 惰性释放 vs 内存泄漏:惰性释放是有意的缓存(保留空间以备复用),不是泄漏;空间仍由 alloc 记录,后续写入可直接复用。
  • SDS 与 listpack / ziplist 的关系:listpack 内部的字符串元素同样可用 SDS 或整型编码;三者都是 Redis 的基础存储单元,但层级不同(SDS 是字符串,listpack 是紧凑列表)。
  • 换问法:若题干改成“下列哪项是 SDS 的好处”,答案就落在 A / B / C 任一项上;若问“SDS 有哪些头类型”,答案是 sdshdr5/8/16/32/64。

【自测】 一个 len = 10 字节的字符串(小于 1 MB),执行 APPEND key "abc" 后 len 变为 13,此时 SDS 的 alloc(可用空间)是多少?缩短字符串时为什么不一定立刻释放内存?

答:alloc = 26(13 × 2 —— 因为 len < 1 MB 时按“再多分配一倍”预分配)。缩短时不立刻释放,是因为惰性释放策略:保留已分配空间供后续 APPEND 直接复用,避免频繁 realloc。与 R02、R04 连考;互联网面试高频。

【知识关联】

  • 补题关联:补-28(底层编码,SDS 是字符串层基础)。
  • 面试/工程:SDS 二进制安全 → 可存图片/序列化对象;预分配与惰性释放减少 realloc;len 字段使 STRLEN O(1)、杜绝 C 字符串截断。工程含义:不要把超大 JSON 整段塞进一个 string(应 Hash/分片);APPEND 高频场景 SDS 扩容策略更友好。
  • 面试追问:① SDS 如何兼容 C 字符串函数?(仍以 \0 结尾,但二进制数据中间可含 \0) ② 为什么要预分配?(摊还扩容,减少反复申请内存)

【拓展延伸】

  • 变式问法:对比 C 字符串 vs SDS(长度获取、缓冲区溢出、二进制安全、内存重分配)。
  • 参数/命令:OBJECT ENCODING key(int/embstr/raw);STRLEN;APPEND;SETRANGE;大 value 用 MEMORY USAGE 评估;proto-max-bulk-len 限制单值大小。

R04. 关于“Redis 为什么快”,下列原因不正确的是? ​

考点:Redis 的线程模型

A. 数据全部存放在内存中,读写无需磁盘 IO B. 单线程处理命令,避免了多线程的锁竞争与上下文切换开销 C. 使用 IO 多路复用(epoll)处理海量连接 D. Redis 的所有命令都是 O(1) 复杂度,不存在慢命令

答案:D

【考点】Redis 高性能的真实原因与“单线程”的准确含义。

【结论】 不正确的是 D —— “所有命令都是 O(1)”是明显错误的说法,Redis 存在 KEYS *、SORT 等 O(N) 甚至更慢的命令,正是生产中的阻塞风险点。

【逐项辨析】

  • A 数据全在内存、读写无需磁盘 IO:说法成立,不选。这是 Redis 快的根本原因 —— 内存访问比磁盘快约 4~5 个数量级。
  • B 单线程处理命令、避免锁竞争与上下文切换:说法成立,不选。但需注意“单线程”指的是命令执行单线程,不是“整个 Redis 只有一个线程”。
  • C 使用 IO 多路复用(epoll)处理海量连接:说法成立,不选。epoll 让单线程能以极低开销管理大量连接,避免了“一连接一线程”的模型开销。
  • D Redis 的所有命令都是 O(1)、不存在慢命令:正确(这是“不正确”的那一项)。 错在“所有”二字 —— KEYS * 是 O(N),SORT、HGETALL、SMEMBERS、ZRANGE 在大数据量下都可能耗时数十毫秒甚至秒级,而阻塞单线程 = 阻塞全部请求。

【知识点】 “Redis 为什么快”可拆成四个原因,其中“单线程”最容易被误读:

原因说明易错点
纯内存操作数据在内存,省去磁盘 IO根本原因,但持久化时仍有磁盘开销
单线程命令执行无锁竞争、无上下文切换、无并发 bug不是“全进程单线程”
IO 多路复用(epoll)单线程高效管理海量连接连接多 ≠ 线程多
高效的数据结构SDS、跳表、listpack 等定制结构结构效率高,但不能改变命令复杂度

“单线程”的准确边界:

部分线程模型
命令执行(读写数据结构)单线程(4.0 之前为全流程单线程)
网络 IO6.0 起引入多线程 IO(读写 socket、解析协议)
持久化(RDB / AOF)独立后台线程 / 子进程
异步删除(UNLINK、FLUSHALL ASYNC)后台线程
集群同步独立线程

推导:为什么单线程反而快?① 纯内存操作使 CPU 不是瓶颈,瓶颈在网络 IO,多线程带来的并行收益有限;② 单线程省掉了锁竞争与上下文切换;③ 单线程天然保证命令的原子性,无需加锁就能实现 INCR、SETNX 这类复合语义。代价是:任何一个慢命令都会阻塞其后所有请求 —— 这就是“慢命令”问题的由来。

常见慢命令清单(生产必须规避):

命令复杂度替代方案
KEYS *O(N)SCAN 游标分批
HGETALL 大 HashO(N)HSCAN,或按字段 HGET
SMEMBERS 大 SetO(N)SSCAN
ZRANGE 全量取O(N)限定范围或 ZSCAN
SORTO(N log N)尽量用 ZSet 维护有序

【记忆锚点】 「内存快、单线程省、epoll 多路复用、结构巧 —— 但慢命令一样能卡死全场」 —— 前三条是优点,第四条是必须警惕的代价。

【易混对比】

  • “单线程”的两种理解:命令执行单线程 ✅ vs 整个 Redis 单线程 ❌(6.0 起网络 IO 已多线程,持久化 / 异步删除也由后台线程完成)。
  • 单线程 vs 原子性:单线程是 Redis 命令级原子性的来源,但不提供事务级原子性(Redis 事务不支持回滚)。
  • 慢命令 vs 大 key:二者互为因果 —— 大 key 上执行 O(N) 命令就是典型的阻塞源;治理手段包括拆分 key、用 SCAN 系列替代全量命令、用 UNLINK 异步删除。
  • 换问法:若题干改成“Redis 6.0 的线程模型变化”,答案是“网络 IO 引入多线程,命令执行仍是单线程”。

【自测】 生产环境误执行 KEYS user:*,Redis 出现数百毫秒卡顿,其他请求全部超时。请说明原因,并给出两种替代写法。

答:原因是 KEYS 为 O(N) 全量遍历,且命令执行是单线程 —— 遍历期间其他命令全部排队等待。替代方案:① 用 SCAN 游标分批遍历(每次只返回少量 key,不阻塞);② 用 Set / Hash 维护索引,把“按前缀查 key”变成一次集合读取。与 R02、R26 连考;生产事故类面试高频。

【知识关联】

  • 补题关联:补-28(底层编码与内存布局,命令复杂度与编码相关)、补-26(单线程下的限流实现)。
  • 面试/工程:单线程避免锁与上下文切换,IO 多路复用(epoll)支撑 C10K;Redis 6.0 的多线程只加速网络读写,命令执行仍是单线程,所以 CPU 密集命令(大 SORT、大 SINTER)依然阻塞。理解这点才能解释“为什么加了 io-threads 还是慢”。
  • 面试追问:① 哪些操作会阻塞主线程?(KEYS、大集合全量遍历、SAVE、大 Lua、慢 O(N) 命令) ② 多线程为何不解决 CPU 密集命令?(执行路径仍是单线程串行,数据结构也非线程安全)

【拓展延伸】

  • 变式问法:问“Redis 6/7 是不是多线程”→ 网络 IO 多线程、命令执行单线程;问“为什么不用多核跑命令”→ 复杂度与数据结构安全,且单线程已能打满网卡/内存带宽。
  • 参数/命令:io-threads、io-threads-do-reads;压测 redis-benchmark;慢日志 SLOWLOG GET/LEN/RESET、CONFIG SET slowlog-log-slower-than;INFO commandstats 看各命令调用耗时。

R05. 大量请求查询数据库中根本不存在的 key(如伪造的 id),导致请求全部打到数据库,这属于? ​

考点:缓存穿透

A. 缓存穿透 B. 缓存击穿 C. 缓存雪崩 D. 缓存预热

答案:A

【考点】缓存穿透的定义与三大缓存问题的区分。

【结论】 请求查的是数据库中根本不存在的 key,缓存永远不命中、每次都要打到库 —— 这叫缓存穿透,选 A。

【逐项辨析】

  • A 缓存穿透:正确。 判定标志是“数据不存在” —— 缓存与数据库都没有该 key,因此每一次请求都穿透到数据库。
  • B 缓存击穿:错。击穿是单个热点 key 过期瞬间的并发回源,该 key 在数据库里是存在的。
  • C 缓存雪崩:错。雪崩是大批 key 同时失效(或 Redis 整体宕机),是“范围”问题,不是“数据不存在”问题。
  • D 缓存预热:错。预热是系统上线前主动把热点数据加载进缓存,属优化手段,不是故障现象。

【知识点】 三大缓存问题只需一把尺子:“出问题的是什么数据”。

问题触发对象数据是否存在典型成因后果
穿透不存在的 key❌ 缓存与库都没有恶意攻击、参数错误每次请求都打到库
击穿单个热点 key✅ 存在,只是刚过期热点 key 过期瞬间并发回源瞬时大量请求打到库
雪崩大批 key✅ 存在,只是集中失效TTL 对齐、Redis 宕机数据库被整体压垮

穿透的本质是缓存对“不存在的 key”无法形成拦截:查询 miss → 回源数据库 → 数据库也为空 → “空”这个结果不会被写入缓存 → 下次同样的请求再次穿透。这是一个“负结果不被缓存”导致的漏洞。

推导:正常的缓存模式是“cache aside”—— 读时先查缓存,miss 则查库并回填。当数据存在时,回填动作会填补缓存,请求被挡住;当数据不存在时,没有东西可以回填,缓存永远是空的,于是每一次请求都走完整链路。因此穿透的解法必然围绕“让不存在的 key 也能被拦下”(缓存空值 / 布隆过滤器,见 R06)。

【记忆锚点】 「穿透查的是没有的东西,击穿是一个热点过期,雪崩是一片同时过期」 —— 先看“数据在不在”,再看“影响是一个点还是一片”。

【易混对比】

  • 穿透 vs 击穿:穿透的数据根本不存在;击穿的数据存在,只是过期了。一字之差,解法完全不同(穿透用布隆过滤器 / 空值缓存;击穿用互斥锁 / 逻辑过期)。
  • 击穿 vs 雪崩:击穿是一个点(单 key),雪崩是一大片(批量 key)。
  • 穿透 vs 恶意攻击:穿透现象常由攻击引发(用不存在的 id 刷接口),但参数错误、脏数据同样会造成穿透 —— 不能一律归因于攻击。
  • 换问法:若题干改成“大量请求查询已被删除的商品 id”,仍是穿透;若改成“首页大促 key 到期瞬间并发激增”,则是击穿。

【自测】 攻击者用 id = -1 这类必然不存在的参数高频刷接口,缓存和数据库都查不到,流量全部打到 MySQL。这属于三大缓存问题中的哪一种?最省内存的解法是什么?

答:缓存穿透;最省内存的解法是布隆过滤器(把存在的 key 预加载,判定“不存在”直接返回,内存开销极小),代价是有假阳性、不支持删除元素。与 R06、R07、R09 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-30(Cache Aside 与兜底补偿)、补-26(限流可挡住恶意扫描型穿透)。
  • 面试/工程:穿透的典型形态是恶意扫不存在的 ID、爬虫打未命中接口,或删数据后缓存未清。防御分层:① 参数校验/网关黑名单(最便宜) ② 布隆过滤器拦“肯定不存在” ③ 空值缓存给“查库也无”的 key 短 TTL ④ 熔断限流保住 DB。只靠加机器扛穿透是错误方案。
  • 面试追问:① 空值缓存 TTL 该设多长?(短,如 30s–2min,避免数据后来真的写入后长期读不到) ② 布隆过滤器误判方向是什么?(可能把不存在误判为存在 → 仍有少量漏到 DB;绝不会把存在误判为不存在)

【拓展延伸】

  • 变式问法:对比穿透(key 本不存在)/击穿(热点 key 过期瞬间)/雪崩(大批 key 同时失效或 Redis 宕机);给出场景选类型。
  • 参数/命令:空值:SET key "" EX 60 或专用前缀 null:;布隆:RedisBloom BF.RESERVE key 0.01 1000000、BF.ADD/BF.EXISTS;也可用本地 Caffeine/Guava 布隆;网关 QPS 限流与 IP 黑名单。

R06. 解决缓存穿透最常用的两种方案是? ​

考点:缓存穿透的解决方案

A. 缓存空值(设置较短过期时间)+ 布隆过滤器 B. 延长缓存过期时间 C. 给热点 key 加互斥锁 D. 给过期时间加随机扰动

答案:A

【考点】缓存穿透的两种标准解法及其取舍。

【结论】 穿透的两种标准解法是缓存空值(设较短过期时间)与布隆过滤器 —— 选 A。

【逐项辨析】

  • A 缓存空值(设置较短过期时间)+ 布隆过滤器:正确。 前者让“不存在的 key”也能被缓存拦住,后者在入口处直接判定“不存在”并返回,二者都针对“查的是不存在的数据”这一根因。
  • B 延长缓存过期时间:错。错在方向反了 —— 穿透的问题是“数据不存在、根本没有东西可缓存”,延长 TTL 对不存在的 key 无效;延长 TTL 是缓解雪崩的思路之一。
  • C 给热点 key 加互斥锁:错。互斥锁针对的是“热点 key 过期瞬间的并发回源”,那是击穿的解法,与“数据不存在”无关。
  • D 给过期时间加随机扰动:错。随机扰动的目的是打散批量 key 的失效时刻,解决的是雪崩,对穿透无效。

【知识点】 两种方案的完整对照:

方案原理优点缺点
缓存空值查库为空时,也向 Redis 写入空值 / 特殊标记,并设较短 TTL(如 30~60 秒)实现简单、无需额外组件占用内存(大量不同 key 会撑爆缓存);存在短暂不一致窗口(期间数据库新增了该数据也读不到)
布隆过滤器把存在的 key 预加载进位数组,请求先过过滤器,判定“不存在”则直接返回内存极省(每元素约 10 bit 即可把误判率压到约 1%)、性能高有假阳性(误判“存在”)、无假阴性;不支持删除元素

布隆过滤器的原理推导:用 k 个独立哈希函数把每个元素映射到位数组的 k 个位上,全部置 1。查询时若任一位为 0 → 元素一定不存在(无假阴性);若全部为 1 → 元素可能存在(可能是别人的位被撞上,即假阳性)。这就是“判不存在可信、判存在不可信”的由来。

为什么两种方案要配合使用:布隆过滤器负责挡住绝大多数不存在的请求(内存极省),缓存空值负责兜住过滤器漏过的假阳性请求(避免它们反复查库)。工程上还常加第三种补充手段 —— 接口层参数校验(如 id 必须为正整数、格式校验),把明显非法的请求挡在最外层。

【记忆锚点】 「穿透两招:空值缓存兜底、布隆过滤器拦截」 —— 一个用空间换简单,一个用极小空间换高拦截率。

【易混对比】

  • 三大问题的解法一一对应:穿透 → 空值缓存 / 布隆过滤器;击穿 → 互斥锁 / 逻辑过期;雪崩 → TTL 随机扰动 / 多级缓存 / 熔断降级。三套解法不可混用。
  • 缓存空值 vs 布隆过滤器:空值缓存占内存但精确;布隆过滤器省内存但有误判。二者常组合使用。
  • 布隆过滤器 vs Counting Bloom Filter:后者把位换成计数器,支持删除,代价是内存成倍增加(约为普通布隆的 4 倍)。注意 Redis 8 起概率型数据结构已内置于 Redis Open Source(本机 redis:8 容器 8.10.2 直接可用 BF.*/CF.*,更早版本才要挂 RedisBloom 模块),其中 BF.* 不提供删除单个元素的命令 —— BF.ADD / BF.EXISTS 只能加与查,要清只能 DEL 整个过滤器;模块内能按元素删除的是 Cuckoo Filter 的 CF.DEL(配 CF.ADD / CF.EXISTS,空间通常比 Counting Bloom 更省)。
  • 换问法:若题干问“布隆过滤器能否删除元素”,答案是不能(需 Counting Bloom Filter 或定期重建)。

【自测】 某商品查询接口用布隆过滤器拦截穿透,某商品下架后从数据库删除,但布隆过滤器仍判定“存在”,请求继续查库并穿透。这说明布隆过滤器的哪个特性?

答:说明布隆过滤器不支持删除元素 —— 元素一旦置位就无法撤销,删除数据后过滤器仍判“存在”(这也是假阳性的一种表现)。解法是使用 Counting Bloom Filter,或定期重建过滤器。与 R05、R07、R08 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-30(一致性与兜底)、补-26(令牌桶限流作最后防线)。
  • 面试/工程:布隆过滤器适合“只判有无、可容忍极低误判”的海量集合(商品 ID、用户 ID);空值方案实现最简单,但要防“恶意大量不同 key”把空值缓存撑大(需上限与 LRU)。接口层校验 ID 合法性(位数、前缀、校验位)成本最低,应最先做。布隆删除困难(标准布隆不支持 delete,可用 Counting Bloom / Cuckoo Filter)。
  • 面试追问:① 布隆过滤器能删除吗?(标准实现不能安全删;Counting Bloom 可,但内存更大) ② 空值会不会造成“数据永久看不到”?(若 TTL 过长且期间数据已写入 DB,会;故 TTL 要短,或写路径主动删空值)

【拓展延伸】

  • 变式问法:题干强调“最常用两种”→ 布隆 + 空值;若选项有“缓存 null 并设短 TTL”也算空值方案;“全表扫校验”是错误项。
  • 参数/命令:BF.RESERVE key error capacity(error=误判率);误判率↓ → 内存↑;业务侧 UUID/雪花 ID 格式校验;布隆更新时机:新增商品时 BF.ADD,可接受短暂漏放行。

R07. 某个热点 key 在过期的那一瞬间,大量并发请求同时打到数据库,这属于? ​

考点:缓存击穿

A. 缓存穿透 B. 缓存击穿 C. 缓存雪崩 D. 缓存一致性

答案:B

【考点】缓存击穿的判定标志:单个热点 key + 过期瞬间 + 高并发。

【结论】 “单个热点 key 在过期瞬间被大量并发请求同时回源” —— 判定标志齐全,属于缓存击穿,选 B。

【逐项辨析】

  • A 缓存穿透:错。穿透查的是数据库中不存在的数据,而本题的热点 key 在数据库里是存在的,只是刚过期。
  • B 缓存击穿:正确。 题干两个关键词 —— “某个”(单个 key)与“过期的那一瞬间”(TTL 到期)—— 正是击穿的定义特征。
  • C 缓存雪崩:错。雪崩是一大批 key 同时失效,是“范围”层面的失效,而本题只涉及一个 key。
  • D 缓存一致性:错。缓存一致性指缓存与数据库之间的数据同步问题(如更新时序导致的脏数据),与“并发回源”不是同一类问题。

【知识点】 击穿的本质是“单点热点 + 失效瞬间 + 高并发”三者叠加:

要素含义缺了会怎样
单点热点该 key 承载极高 QPS非热点 key 过期,回源请求量小,数据库扛得住
失效瞬间TTL 到期,缓存出现“空洞”key 永不过期则不存在空洞,天然不击穿
高并发大量请求同时发现 miss并发低则只有零星请求回源

推导:击穿的伤害来自“同时性”。缓存失效的那一刻,N 个并发请求同时发现缓存 miss,由于没有互斥机制,它们会同时去查数据库 —— 数据库承受的不是 1 次查询,而是 N 次相同的查询。这就是“缓存本应起到的削峰作用,在失效瞬间完全失效”。

与雪崩的区别在范围:

维度击穿雪崩
失效对象单个热点 key一大批 key(或整个 Redis)
触发原因热点 key 的 TTL 到期TTL 集中对齐 / Redis 宕机
数据库压力一个 key 的 N 次重复查询全量 key 的回源潮

注意一个常见误解:“热点 key 永不过期就天然不击穿” 是成立的,但这会带来两个新问题 —— 数据长期不更新(业务上可能读到陈旧值)与内存长期占用。因此工程上更多采用“逻辑过期”(key 不过期,value 里存逻辑过期时间)来兼顾二者,见 R08。

【记忆锚点】 「击穿是“一个点”炸了,雪崩是“一整片”塌了」 —— 判据是“失效的是单个热点 key,还是一批 key”。

【易混对比】

  • 击穿 vs 穿透:击穿的数据存在(刚过期),穿透的数据不存在。击穿是“并发回源同一个 key”,穿透是“每次都查不存在的 key”。
  • 击穿 vs 雪崩:一个点 vs 一大片(见上表)。
  • 热点 key vs 大 key:热点 key 是访问频率高(击穿风险),大 key 是单个 value 体积大(网络与阻塞风险)—— 两个不同的治理维度,但都需专项处理。
  • 换问法:若题干改成“缓存中大批 key 在同一时刻过期”,答案就变成雪崩;若改成“查询的 id 在数据库中不存在”,答案是穿透。

【自测】 某系统给所有商品缓存统一设置 TTL = 30 分钟,每天 10:00 批量预热后,10:30 左右数据库负载出现规律性尖峰。这属于三大缓存问题中的哪一种?为什么?

答:属于缓存雪崩 —— 因为所有 key 的 TTL 同时对齐,在 10:30 集体失效,请求潮涌向数据库,形成规律性尖峰。解法是给过期时间加随机扰动(如 TTL = 30 分钟 + 随机 0~5 分钟)打散失效时刻。与 R05、R06、R08 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-21(互斥锁/乐观思想直接用于重建)、补-30(一致性策略背景)。
  • 面试/工程:商品详情、直播开播、微博热点在热点 key 过期瞬间最易击穿:并发线程同时 miss 并打穿 DB。与热 key 的区别:热 key 是访问频率过高(未过期也会打挂节点),击穿强调“过期瞬间的重建风暴”。与穿透的区别:key 是存在的,只是刚好失效。
  • 面试追问:① 击穿与热 key 有什么区别?(击穿=失效瞬间重建风暴;热 key=访问热点导致单节点/单分片过热) ② 为什么击穿要互斥而不是都去查库?(把 N 次并行查库压成 1 次查库 + N-1 次等待/读旧值,保护 DB)

【拓展延伸】

  • 变式问法:问“单个热点 key 失效”选击穿;问“大批 key 失效或缓存宕机”选雪崩;问“查不存在的数据”选穿透。
  • 参数/命令:互斥:SET lock:rebuild:hot 1 NX PX 3000;逻辑过期:value 内嵌 expireAt,过期后先返回旧值、异步重建;热点可设更长 TTL 或本地缓存(Caffeine)二级。

R08. 解决缓存击穿(热点 key 过期)的常用方案是? ​

考点:缓存击穿的解决方案

A. 互斥锁(只让一个线程回源重建缓存)+ 热点 key 逻辑过期或永不过期 B. 缓存空值 C. 给所有 key 的过期时间加随机扰动 D. 部署布隆过滤器

答案:A

【考点】击穿的两种解法:串行化回源 vs 逻辑过期。

【结论】 击穿的两大解法是互斥锁(只让一个线程回源重建)+ 热点 key 逻辑过期或永不过期 —— 选 A。

【逐项辨析】

  • A 互斥锁(只让一个线程回源重建缓存)+ 热点 key 逻辑过期或永不过期:正确。 前者把并发回源串行化(只查库一次),后者让热点 key 不产生失效空洞,两条路线正好覆盖击穿的两个成因。
  • B 缓存空值:错。缓存空值解决的是“数据不存在”的穿透问题,而击穿的数据是存在的,缓存空值没有意义。
  • C 给所有 key 的过期时间加随机扰动:错。随机扰动的目的是打散批量失效时刻,解决的是雪崩;击穿只有一个 key,打散无从谈起。
  • D 部署布隆过滤器:错。布隆过滤器用于穿透拦截(判定 key 是否存在),与“热点 key 过期瞬间的并发回源”无关。

【知识点】 两种方案的完整对照:

维度互斥锁方案逻辑过期方案
做法缓存 miss 时先抢分布式锁(SET key value NX EX),只有拿到锁的线程查库并回填,其余线程短暂等待后重试key 不设 Redis 过期时间(永不过期),value 内额外存 expireTime;读到时若逻辑已过期,则异步开线程重建,当前请求仍返回旧数据
优点保证数据库只被查一次;数据强一致(返回最新值)完全不阻塞请求,可用性最高
缺点其他线程需等待,吞吐下降;有锁超时 / 死锁风险存在数据不一致窗口,返回旧值;需额外线程池

互斥锁方案的关键实现要点(以 Redis 分布式锁为例):SET lock_key unique_value NX EX 10。三个要点:① NX 保证“只有 key 不存在时才设置成功”,即只有一个人能拿到锁;② EX 10 给锁设过期时间,避免持锁者崩溃后死锁;③ unique_value(如 UUID)用于释放锁时校验归属 —— 必须用 Lua 脚本“先比对 value 再删除”,否则可能误删别人刚拿到的锁。

推导:击穿的成因是“失效瞬间的并发”,因此解法只有两条路 —— 要么让并发不进来(互斥锁,把 N 个并发回源压成 1 次),要么让失效不存在(逻辑过期 / 永不过期,热点 key 永远不产生空洞)。前者牺牲吞吐换一致性,后者牺牲一致性换可用性,是典型的“一致性与可用性的取舍”。

补充工程口径:热点 key 若确实选择“永不过期”,必须配套主动更新机制(数据变更时同步刷新缓存),否则会长期返回陈旧数据。

【记忆锚点】 「击穿两条路:抢锁串行回源,逻辑过期不阻塞」 —— 一个保数据新,一个保响应快。

【易混对比】

  • 互斥锁 vs 逻辑过期:前者阻塞等待、数据新;后者不阻塞、数据旧。选型看业务能否容忍短暂脏读(如商品详情可容忍,库存不可容忍)。
  • 互斥锁 vs 分布式锁:互斥锁是目的(只让一个线程回源),分布式锁是手段(SET NX EX + Lua 释放)。
  • 三大问题的解法对照:穿透 → 空值缓存 / 布隆过滤器;击穿 → 互斥锁 / 逻辑过期;雪崩 → TTL 随机扰动 / 多级缓存 / 熔断降级。题干问哪种问题,就先想对应的那一组解法。
  • 换问法:若题干问“击穿方案中哪个能保证请求不阻塞”,答案是逻辑过期;问“哪个能保证数据库只被查一次”,答案是互斥锁。

【自测】 某大促商品详情缓存采用“互斥锁”方案,大促开始瞬间 QPS 极高,大量线程在等锁,接口平均耗时飙升。换成哪种方案可以缓解?代价是什么?

答:换成逻辑过期方案(key 永不过期,value 存 expireTime,过期时异步重建、当前请求返回旧值)—— 因为没有任何线程需要等待锁,接口耗时稳定。代价是存在数据不一致窗口,短时间返回旧数据。与 R05、R06、R07 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-21(悲观互斥 vs 乐观无锁)、R21(Lua 保证「查空再占锁」的原子性);补-26 讲的是令牌桶限流脚本,别混、补-30(一致性取舍)。
  • 面试/工程:互斥锁实现简单、强一致,但重建期间其他线程阻塞或重试,适合重建快、可等待的场景。逻辑过期不阻塞读(可立刻返回旧值),但会有秒级陈旧,适合容忍最终一致的展示类数据。两者可组合:先逻辑过期保可用,后台互斥重建防重复打库。失败线程通常短退避重试或直接读旧值,不建议长自旋占 CPU。
  • 面试追问:① 互斥锁失败线程该自旋还是休眠?(短退避休眠/重试;长自旋浪费 CPU 且惊群) ② 逻辑过期如何避免击穿打到库?(只有一个线程能抢到重建锁,其余继续读旧值)

【拓展延伸】

  • 变式问法:把“热点永不过期”也列为可选,但要指出一致性代价;把“所有线程都去查库”设为错误项。
  • 参数/命令:SET ... NX PX 占锁;Lua 保证 GET-判断-DEL/占锁原子;信号量/令牌桶限制同时重建数;SET hot:data '{"v":1,"exp":...}' 结构化逻辑过期字段。

R09. 大量 key 在同一时间集中过期,导致数据库压力骤增,这属于? ​

考点:缓存雪崩

A. 缓存穿透 B. 缓存击穿 C. 缓存雪崩 D. 缓存预热

答案:C

【考点】缓存雪崩的定义(含 Redis 整体宕机的情形)。

【结论】 大量 key 在同一时间集中过期,请求瞬间全部涌向数据库 —— 这叫缓存雪崩,选 C。判定词是“大批 key 同时失效”。

【逐项辨析】

  • A 缓存穿透:错。关键字眼是“数据不存在” —— 穿透是缓存与数据库都查不到;本题的 key 存在,只是过期了。
  • B 缓存击穿:错。关键字眼是“单个热点 key” —— 击穿的量级是“一个点”,而题干说的是“大量 key”。
  • C 缓存雪崩:正确。 “同一时间集中过期”正是雪崩的判定标志:大批 key 同时失效,原本被缓存挡住的流量在极短时间内全落到数据库。
  • D 缓存预热:错。预热是上线前主动把热点数据加载进缓存的优化手段,属于“制造缓存”,与题干描述的失效现象方向相反。

【知识点】 雪崩有两种触发形态,后果完全相同:

形态成因后果
大批 key 同时过期启动时批量写入、统一设置相同 TTL同一秒大量 miss,请求潮涌向数据库
Redis 整体不可用宕机、网络分区、主从切换期间所有请求直接落到数据库

推导:设缓存命中率为 h、QPS 为 Q,数据库实际承压约为 Q × (1 − h)。大批 key 失效时 h 从 0.95 瞬间跌到接近 0,数据库承压从 5% 陡增到接近 100% —— 数据库被瞬间打满,可能引发连锁故障(库挂 → 上层超时 → 重试放大 → 彻底雪崩)。所以雪崩防护必然是多层次的:既要在 TTL 上打散,也要在架构上做高可用与兜底(见 R10)。

【记忆锚点】 “穿透是查不存在的,击穿是一个热点过期,雪崩是一片同时过期” —— 先看数据在不在,再看影响是一个点还是一片。

【易混对比】

  • 雪崩 vs 击穿:雪崩是一大片 key,击穿是一个热点 key。
  • 雪崩 vs 穿透:雪崩的数据存在(只是失效),穿透的数据根本不存在。
  • 换问法:改成“Redis 主从切换期间所有请求打到数据库”仍是雪崩;改成“爆款商品缓存到期的瞬间并发激增”则变成击穿。与 R05、R06、R10 连考。

【自测】 定时任务凌晨 2 点批量刷新 10 万个商品缓存,TTL 统一设为 3600 秒;3 点监控显示数据库 QPS 突增 20 倍。属于哪种缓存问题?

答:缓存雪崩。10 万个 key 的 TTL 完全对齐 → 一小时后集中失效。与 R05、R10 连考。

【知识关联】

  • 补题关联:补-30(缓存与库一致性/兜底)、补-26(限流在雪崩时保命)。
  • 面试/工程:上线大批写入且 TTL 设成同一固定值(如都 24h)是经典雪崩事故:某时刻集体过期,请求齐刷刷打穿 DB。Redis 宕机/主从切换导致全量缓存不可用,后果同级。治理要“防过期扎堆 + 防节点单点 + 保命限流”三层一起上,不能只加机器。
  • 面试追问:① 为什么加随机 TTL 能防雪崩?(把过期时刻打散,避免同一毫秒集体失效) ② 雪崩与 Redis 宕机有何异同?(都表现为缓存层整体失效;雪崩还可因 TTL 扎堆,不必宕机)

【拓展延伸】

  • 变式问法:问“缓存服务器重启导致全量失效”也归入雪崩类风险;把“单个 key 过期”排除为击穿。
  • 参数/命令:EXPIRE key (base + random(0,N));集群/多副本分担;多级缓存(本地+Redis);maxmemory-policy 防止因内存满导致大量逐出;熔断组件与 DB 连接池上限。

R10. 缓解缓存雪崩的措施不包括? ​

考点:缓存雪崩的解决方案

A. 给缓存过期时间加随机扰动,避免集中失效 B. 搭建 Redis 高可用架构(哨兵 / Cluster),配合多级缓存与服务降级熔断 C. 对数据库做限流、并准备缓存预热与热点数据永不过期 D. 把所有 key 的过期时间统一设为同一时刻,便于集中管理

答案:D

【考点】雪崩的多层防护体系。

【结论】 把所有 key 的过期时间统一设为同一时刻恰恰是雪崩的成因,而不是缓解措施 —— 所以选 D。

【逐项辨析】

  • A 加随机扰动避免集中失效:属有效缓解措施,不是本题答案。关键字眼是“随机” —— 基础 TTL 叠加随机值即可打散失效时刻。
  • B 搭建 Redis 高可用(哨兵 / Cluster),配合多级缓存与降级熔断:属有效缓解措施。关键字眼是“高可用” —— 针对雪崩的第二种形态(Redis 整体不可用)。
  • C 对数据库限流、并准备缓存预热与热点数据永不过期:属有效缓解措施。关键字眼是“兜底” —— 限流是最后的保险丝。
  • D 把所有 key 的过期时间统一设为同一时刻,便于集中管理:正确(本题选它)。 统一过期时间 = 人为制造集中失效,等于亲手把“大批 key 同时过期”这个成因做出来,与 A 的“随机扰动”背道而驰。

【知识点】 雪崩防护是四层体系,每层挡的是不同的失败路径:

层次手段挡什么风险关键配置 / 工具
① TTL 打散基础过期时间 + 随机值大批 key 同时过期如 3600 + rand(0, 300) 秒
② 高可用哨兵 / ClusterRedis 自身单点故障主从 + 自动故障转移
③ 多级缓存本地缓存 + RedisRedis 整体不可用Caffeine / Guava + Redis
④ 服务治理限流、熔断降级、预热、永不过期前三层都失效后的兜底数据库限流、返回兜底数据

推导:雪崩的根因是“缓存挡不住流量了”。要挡住,要么让缓存不集体失效(①),要么让 Redis 不挂(②),要么挂了还有别的缓存(③),要么挡不住时保护数据库不被打死(④)—— 四层分别对应四种“挡不住”的路径,所以这是典型的“不能只靠一招”的架构题。

【记忆锚点】 “打散 TTL、Redis 高可用、本地兜底、库前限流” —— 四层防线,少一层都会漏。

【易混对比】

  • 雪崩 vs 击穿的解法:雪崩用“打散 + 高可用 + 多级缓存”(面上的措施);击穿用“互斥锁 / 逻辑过期”(点上的措施)。拿互斥锁防雪崩基本无效 —— 十万个 key 各锁各的,锁本身反成瓶颈。
  • 换问法:若 D 改成“把热点数据的 TTL 设为永久”,则它是合理措施(配合预热使用)。与 R05、R09 连考。

【自测】 某团队把全部缓存 key 的 TTL 统一设为 24 小时、不设随机值,同时给数据库加了限流。这套方案能防住雪崩吗?

答:不能完全防住。TTL 完全对齐本身就是雪崩的典型成因,限流只能减轻后果。与 R09 连考。

【知识关联】

  • 补题关联:补-30(删除失败补偿与最终一致)、补-26(限流)、补-21(互斥思想)。
  • 面试/工程:互斥锁防击穿、随机 TTL 防雪崩、熔断限流保命、多级缓存降压,生产里是组合拳而不是单选。集群多副本(主从+读写分离)能降低“单节点宕机=全站缓存丢”的概率,但主从切换瞬间仍可能雪崩,需结合客户端重试与本地缓存。只做随机 TTL 不够:扛不住节点级故障。
  • 面试追问:① 只做随机 TTL 够不够?(不够,防不了宕机与容量击穿) ② 集群多副本如何降低同时失效概率?(故障域分散;但仍要本地缓存与限流兜底)

【拓展延伸】

  • 变式问法:题干换成“不包括”时,把“故意让所有 key 同时过期”“缓存宕机后不做限流直接打库”设为错误项。
  • 参数/命令:过期通知 notify-keyspace-events Ex(仅辅助,不能当可靠补偿);监控 INFO stats 的 expired_keys、evicted_keys;熔断限流(Sentinel/Hystrix 思想、网关令牌桶);本地缓存 Caffeine 设置短暂 TTL。

R11. 关于 RDB 持久化,下列说法正确的是? ​

考点:RDB 持久化

A. 记录写命令日志,文件体积小、恢复快 B. 是某一时刻的数据快照,二进制文件,体积小、恢复快,但可能丢失最后一次快照之后的数据 C. 实时持久化,完全不丢数据 D. 只能通过命令手动触发,无法配置自动触发

答案:B

【考点】RDB 的机制、优缺点与触发方式。

【结论】 RDB 是某一时刻的全量数据快照,二进制文件、体积小、恢复快,但两次快照之间的写入会丢失 —— 选 B。

【逐项辨析】

  • A 记录写命令日志,文件体积小、恢复快:错。关键字眼是“写命令日志” —— 记录写命令的是 AOF;且 AOF 恰恰“文件大、恢复慢”。
  • B 是某一时刻的数据快照,二进制文件,体积小、恢复快,但可能丢失最后一次快照之后的数据:正确。 快照、紧凑二进制、有丢失窗口,三个特征全中。
  • C 实时持久化,完全不丢数据:错。关键字眼是“完全不丢” —— RDB 是周期性快照,快照点之间就是空档。
  • D 只能通过命令手动触发,无法配置自动触发:错。关键字眼是“只能手动” —— 自动触发(save 规则、主从全量同步)才是常规方式。

【知识点】 RDB 的核心是“把内存中的全量数据在某一瞬间序列化成紧凑二进制”(默认文件 dump.rdb)。

维度RDB 的表现原因
存储内容某一时刻的全量数据快照存数据本身,不存命令
文件体积小二进制紧凑编码,无命令历史冗余
恢复速度快直接加载进内存,无需重放命令
数据安全性弱,两次快照间的写入会丢快照周期性产生,之间有空档

触发方式分两类:

方式命令 / 配置特点
手动SAVE阻塞主进程,生产禁用
手动BGSAVEfork 子进程后台执行
自动save 900 1 等规则N 秒内至少 M 次修改即触发 BGSAVE
自动主从全量同步主节点生成 RDB 发给从节点

推导:RDB 存的是“某一时刻的整张内存快照”,因此天然“小、快”(无命令冗余),也天然带有“快照点之间有丢失窗口”的缺点 —— 优点与缺点来自同一个设计决策,这正是它常与 AOF 搭配使用的原因(见 R12)。

【记忆锚点】 “RDB 存数据、AOF 存命令” —— 快照小、恢复快,代价是快照之间会丢。

【易混对比】

  • RDB vs AOF:RDB 存数据快照(小、快、可能丢);AOF 存写命令日志(大、慢、丢得少)。
  • SAVE vs BGSAVE:前者阻塞主进程,后者 fork 子进程后台执行 —— 生产只用 BGSAVE。
  • 换问法:问“哪个更适合做冷备份与灾难恢复”,答案仍是 RDB(紧凑、易传输、加载快)。与 R12、R13 连考。

【自测】 实例只开 RDB、配置 save 900 1,10:00 触发一次 BGSAVE,10:12 宕机。10:00—10:12 之间的写入会怎样?

答:全部丢失。触发条件是“900 秒内至少 1 次修改”,10:00 之后 15 分钟内不会再触发快照。与 R12 连考。

【知识关联】

  • 补题关联:无直接补题条目;可对照主库 redo/binlog 的持久化取舍(补-20 恢复思想迁移:都是“落盘节奏 vs 性能”)。
  • 面试/工程:RDB 是某个时间点的全量快照,恢复快、适合备份与容灾,但两次快照之间的写会丢;AOF 追加写命令/RESP,丢数据窗口更小。生产常见 RDB+AOF 同时开:AOF 作主恢复源,RDB 作快速冷备。fork 依赖 COW,大内存实例 fork 时要警惕延迟尖刺。
  • 面试追问:① RDB 为什么可能丢最近写入?(快照间隔内的写未落盘) ② BGSAVE 与 SAVE 区别?(BGSAVE 后台 fork,不阻塞;SAVE 阻塞主进程,生产禁用)

【拓展延伸】

  • 变式问法:问“fork 后 COW 会不会导致内存翻倍”(不会立刻翻倍,只复制被修改页,但写入密集时内存峰值可能接近翻倍);问“什么业务适合 RDB”(可容忍分钟级丢失、要快速重启)。
  • 参数/命令:save 900 1 / save 300 10 等触发策略;dbfilename、dir;SHUTDOWN SAVE;BGSAVE;LASTSAVE;INFO persistence 看 rdb_last_bgsave_status、aof_last_write_status。

R12. 关于 AOF 持久化,下列说法正确的是? ​

考点:AOF 持久化

A. 以快照形式保存全量数据 B. 默认开启且无法关闭 C. 追加写命令日志,可配置 appendfsync 为 always/everysec/no;文件更大、恢复较慢,但丢数据更少 D. 恢复速度比 RDB 更快

答案:C

【考点】AOF 的写入流程与 appendfsync 三种策略。

【结论】 AOF 追加写写命令日志,appendfsync 可配 always / everysec / no;文件比 RDB 大、恢复更慢,但丢数据更少 —— 选 C。

【逐项辨析】

  • A 以快照形式保存全量数据:错。关键字眼是“快照” —— 那是 RDB;AOF 保存的是写命令,是“日志”而非“快照”。
  • B 默认开启且无法关闭:错。关键字眼是“默认开启” —— AOF 默认是关闭的(appendonly no),必须显式开启。
  • C 追加写命令日志,可配置 appendfsync 为 always/everysec/no;文件更大、恢复较慢,但丢数据更少:正确。 机制、配置项、优缺点三处全对。
  • D 恢复速度比 RDB 更快:错。关键字眼是“更快” —— 说反了。AOF 要逐条重放命令,远慢于 RDB 的直接加载快照。

【知识点】 AOF 的写入流程分三步,三档 appendfsync 的差别只在第三步的时机:

客户端写命令 → ① 写入 aof_buf 缓冲区 → ② write 到 AOF 文件(OS 页缓存)
             → ③ fsync 刷盘(真正落盘)
appendfsync何时 fsync最多丢多少数据性能适用
always每条命令都刷几乎不丢最差数据零容忍场景
everysec每秒刷一次(默认)最多 1 秒好生产主流选择
no由操作系统决定不可控(可能几十秒)最好不推荐

与 RDB 的三点对照:文件体积 RDB 小 / AOF 大(命令有历史冗余);恢复速度 RDB 快(直接加载)/ AOF 慢(逐条重放);默认状态 RDB 开启 / AOF 关闭。

推导:everysec 成为默认值,是性能与安全的平衡点 —— always 每条命令一次 fsync,磁盘 IO 成为吞吐瓶颈;no 把刷盘交给操作系统,丢失量无法预期。生产常见做法是两者同时开启:RDB 做备份与快速恢复,AOF 保证数据完整性(重启时优先用 AOF 恢复,因为它更完整)。

【记忆锚点】 “RDB 存数据、AOF 存命令;always 最安全、everysec 是默认、no 最不可控”。

【易混对比】

  • always vs everysec:安全性差一档,性能差一大截;除非要求零丢失,一律 everysec。
  • AOF 默认关闭 vs RDB 默认开启:最常被记反的一对,按“日志要额外开、快照默认开”来记。
  • 换问法:问“重启后优先用哪个文件恢复”,答“优先 AOF”(更完整);AOF 关闭时才用 RDB。与 R11、R13 连考。

【自测】 实例同时开启 RDB 与 AOF(everysec),运行中 AOF 被误删、RDB 完好,重启后数据状态如何?

答:回到最近一次 RDB 快照的状态,快照之后的写入全部丢失。与 R11、R13 连考。

【知识关联】

  • 补题关联:补-20(日志+检查点恢复思想迁移到 AOF rewrite/重放)。
  • 面试/工程:always 最安全但性能差;everysec 丢约 1 秒,是默认推荐;no 依赖 OS,几乎不用。AOF 文件膨胀必须 rewrite:重放等价命令集、用临时文件+原子替换。重写期间新写要同时进旧 AOF 与 rewrite 缓冲,故重写时有额外内存与 CPU 压力,大实例要在低峰做。
  • 面试追问:① everysec 到底丢多少?(通常 ≤1s 写入,取决于 fsync 调度与负载) ② AOF 膨胀不 rewrite 会怎样?(恢复变慢、磁盘打满、重放时间不可控)

【拓展延伸】

  • 变式问法:对比 always/everysec/no;问“AOF 与 binlog 的异同”(都是追加逻辑日志,但 AOF 面向本机恢复,binlog 面向复制/PITR)。
  • 参数/命令:appendonly yes、appendfsync always|everysec|no、auto-aof-rewrite-percentage、auto-aof-rewrite-min-size、BGREWRITEAOF;INFO persistence。

R13. AOF 重写(rewrite)的目的是? ​

考点:AOF 重写

A. 压缩 AOF 文件体积——用“重建当前数据集所需的最少命令”替换历史全部命令,加快恢复速度 B. 备份数据到另一个文件 C. 提升读请求的性能 D. 实现主从同步

答案:A

【考点】AOF 重写机制与 auto-aof-rewrite-percentage 触发条件。

【结论】 AOF 重写的目的就是压缩 AOF 文件体积 —— 用“重建当前数据集所需的最少命令”替换历史全部命令,顺带加快重启恢复,选 A。

【逐项辨析】

  • A 压缩 AOF 文件体积 —— 用“重建当前数据集所需的最少命令”替换历史全部命令,加快恢复速度:正确。 目的与手段都描述准确。
  • B 备份数据到另一个文件:错。关键字眼是“备份” —— 重写产物是新的 AOF 文件,用于替换旧文件,目的不是留一份备份。
  • C 提升读请求的性能:错。关键字眼是“读请求” —— 重写发生在持久化链路上,不改变数据结构也不建索引,对读性能无直接提升。
  • D 实现主从同步:错。关键字眼是“主从同步” —— 全量同步走 RDB,增量同步走 repl_backlog;重写是本地文件整理。

【知识点】 AOF 是追加写,这是它需要“重写”的根本原因:

INCR counter  × 10000 次  →  AOF 里留下 10000 条 INCR 命令
                             而当前数据集只需 1 条 SET counter 10000

即:AOF 记录的是“过程”,而恢复只需要“结果”。两者差距随时间单调增长,文件越来越大、恢复越来越慢 —— 重写就是把这个差距抹掉。流程是 fork + 双缓冲,不阻塞主进程:

阶段主进程子进程
期间继续处理命令,同时写入 AOF 缓冲区与 AOF 重写缓冲区用最少命令生成新 AOF 文件
完成把重写缓冲区内容追加到新文件退出
收尾用新文件替换旧文件(原子 rename)—

触发条件(redis.conf)需同时满足:

配置项含义默认值
auto-aof-rewrite-percentage相对上次重写后体积的增长百分比100(翻倍即触发)
auto-aof-rewrite-min-size触发重写的文件最小体积门槛64mb

后者(min-size)防止小文件频繁重写(体积没到门槛就不触发),前者(percentage)保证大文件在持续增长后会重写;也可手动执行 BGREWRITEAOF。

【记忆锚点】 “AOF 记过程、恢复要结果;重写就是把过程压成结果” —— 一万条 INCR 压成一条 SET。

【易混对比】

  • BGSAVE vs BGREWRITEAOF:前者生成 RDB 快照(存数据),后者生成新 AOF(存最少命令);两者都用 fork。
  • AOF 重写 vs AOF 刷盘(appendfsync):重写解决“文件体积”,刷盘解决“落盘时机”,是两件事。
  • 换问法:问“重写期间主进程新写入的命令会不会丢”,答“不会” —— 它们进入 AOF 重写缓冲区,完成后追加进新文件。与 R11、R12、R14 连考。

【自测】 某 key 被 INCR 了 10 万次。重写后这个 key 会变成几条命令?触发自动重写至少要满足什么条件?

答:1 条(写成 SET key 100000 之类的最终值);触发需同时满足 auto-aof-rewrite-percentage 与 auto-aof-rewrite-min-size。与 R12 连考。

【知识关联】

  • 补题关联:补-20(恢复时间与重放成本)。
  • 面试/工程:重写用“读当前内存数据状态 → 生成等价最小命令集”,而不是重放历史 AOF,因此文件会显著变小。重写期间父进程继续服务,子进程写临时文件;新写命令同时进 rewrite buffer,子进程结束后追加,再原子 rename 替换。大实例重写高峰可能造成 CPU/内存尖刺,应低峰触发或调阈值。
  • 面试追问:① 为什么重写后文件会变小?(历史反复 SET/DEL 被折叠成最终状态) ② 重写会不会丢数据?(不会:旧 AOF 继续写,结束后合并;异常则保留旧文件)

【拓展延伸】

  • 变式问法:问“何时自动重写”→ 增长百分比+最小体积双条件;问“重写与 RDB 快照关系”→ 都是子进程+COW 思想,但输出物不同。
  • 参数/命令:BGREWRITEAOF、auto-aof-rewrite-percentage 100、auto-aof-rewrite-min-size 64mb、aof-use-rdb-preamble(混合持久化,4.0+);INFO persistence 看 aof_current_size/rewrite。

R14. Redis 对已过期的键采用什么删除策略? ​

考点:过期键删除策略

A. 只使用惰性删除(访问时才判断过期) B. 只使用定时删除(为每个键设置定时器) C. 惰性删除 + 定期删除(定期任务随机抽查部分键删除) D. 不删除,等内存淘汰时统一处理

答案:C

【考点】惰性删除与定期删除的组合设计。

【结论】 Redis 采用惰性删除 + 定期删除的组合:访问时才判断过期,同时后台定期随机抽查部分键删除 —— 选 C。

【逐项辨析】

  • A 只使用惰性删除:错。关键字眼是“只” —— 过期后长期不被访问的 key 会一直占着内存不被回收,最终撑满内存。
  • B 只使用定时删除:错。关键字眼是“为每个键设置定时器” —— 定时删除虽最及时,但定时器数量巨大、CPU 开销过高,Redis 没有采用。
  • C 惰性删除 + 定期删除:正确。 两种策略互为补充,正是 Redis 实际采用的方案。
  • D 不删除,等内存淘汰时统一处理:错。关键字眼是“不删除” —— 过期键在访问时就会被惰性删除;内存淘汰(见 R15)是额外兜底,不是过期键的主要处理方式。

【知识点】 过期键的删除有三种候选策略,Redis 选了“折中组合”:

策略做法优点缺点采用
定时删除为每个 key 建定时器,到点即删最及时,内存友好定时器多、CPU 开销大✗
惰性删除访问 key 时才检查是否过期不浪费 CPU冷 key 长期占内存✓
定期删除后台任务随机抽查并删除兼顾 CPU 与内存抽查不保证全覆盖✓

惰性删除(expireIfNeeded)的判定链:读命令遇到已过期的 key → 删除并返回空(视同不存在);写命令遇到已过期的 key → 先删除再正常执行。即过期键一旦被碰到就必然被清掉。

定期删除(activeExpireCycle)的四条规则:① 频率由 hz 控制,默认 hz = 10,即每秒 10 次;② 只从设置了过期时间的 key(expires 字典)中随机抽查,不是全量扫描;③ 若某次抽查中过期 key 占比超过 25%,则继续抽查;④ 单次执行有时间预算——每轮最多占用约 25% 的 CPU 时间片,按 hz=10 推算即 100ms × 25% ≈ 25ms,超时立即退出等下一轮,避免长时间阻塞单线程。注意这个 25ms 是推导值,Redis 并没有名为"单次上限 25ms"的配置项;改 hz 会同时改变每轮的时间预算。

推导:组合仍然不够 —— 惰性删除覆盖不到“冷数据”,定期删除是随机抽查、不保证某个具体 key 被抽到。两者叠加仍不能保证及时清理,必须再加内存淘汰策略兜底(见 R15)。这就是 Redis 的过期键处理是“三件套”而非两件套的原因。

【记忆锚点】 “惰性删除省 CPU,定期删除省内存;两者都不保及时,淘汰策略来兜底” —— 定期删除只查 expires 字典、只随机、只删已过期的。

【易混对比】

  • 惰性删除 vs 定期删除:前者被动(访问触发)、不费 CPU 但漏内存;后者主动(后台任务),恰好相反。
  • 过期键删除 vs 内存淘汰:前者针对“已过期的 key”(时间维度),后者针对“内存不足时的 key”(空间维度)。所有 key 都没过期,内存满了照样淘汰。
  • 换问法:问“定期删除是否保证所有过期键都被删掉”,答“不保证” —— 随机抽查 + 25% 放大 + 25ms 上限。

【自测】 某实例 hz = 10。定期删除任务每秒执行几次?若一次抽查中发现过期 key 占比 60%,会怎样?

答:每秒 10 次;占比超过 25% → 继续抽查,直到占比降下来或达到单次 25ms 上限。与 R09、R15 连考。

【知识关联】

  • 补题关联:无直接补题;与 MySQL 补-20 的“恢复后一致性”对照:过期键策略影响读到的数据是否“陈旧”。
  • 面试/工程:惰性+定期是默认折中:不为每个 key 起定时器,又避免过期键长期占内存。内存紧时仅靠惰性可能堆积大量已逻辑过期 key,导致真需要内存时才暴发式访问磁盘/淘汰。TTL 过短会增加回源,过长则陈旧;业务常“基础 TTL + 随机抖动”。
  • 面试追问:① 只有惰性删除会怎样?(过期键占内存直到再次被访问) ② 从库如何处理主库过期?(主库 DEL 后通过复制传播;从库也可按本地时钟惰性判过期,但以主库删除为准)

【拓展延伸】

  • 变式问法:对比惰性删除 vs 定期删除 vs 内存淘汰(淘汰是“为腾内存主动删”,过期是“到期失效”)。
  • 参数/命令:EXPIRE/PEXPIRE/EXPIREAT、TTL/PTTL、PERSIST;active-expire-cycle 相关行为(每秒采样);INFO stats 的 expired_keys;notify-keyspace-events Ex。

持续学习,持续积累。