← 学习门户 每日面试题 学习看板

2026-10-15

Redis 深入 —— 从架构/原子性/持久化到 Zset/一致性/排行榜/高并发

10 题 · 平均 3.1/5 · low 3.33 · medium 2.75 · high 3.33 · 第 1 轮

#1 什么是Redis的Pipeline,和事务有什么区别? low redis

题目

什么是Redis的Pipeline,和事务有什么区别?

我的回答

redis的pipeline和事务二者都是用来一次性执行一批命令的;二者的目的是不一样的。管道是为了提升一批命令的效率,降低多次网络开销,一次性把多个命令传递过去,但是它不保证原子性,比如说传递过去 3 个命令,哪怕中间某个命令出问题,后续的还是会执行。而事务是为了保证原子性,一次性传过去 3 个命令,如果中间一个命令失败了,后续的命令是不执行的。但是 Redis 本身的事务并不是完全保证原子性,它不会把原来的做回滚。如果想要做回滚,需要在 Lua 脚本中自己去实现。实现的也是类似于 TCC 的补偿,并不是真正的原子性结果。

点评

优点:- 目的区分准确- Pipeline 效率/事务原子性 抓住- Redis 事务无回滚认知到位- Lua 补偿 TCC 类比
不足:- 未提 MULTI/EXEC 具体命令- 未提 WATCH + CAS 乐观锁- 未提 Pipeline 阻塞主线程风险

完整讲解

第一部分:知识重点剖析

**核心考点**:考察对 Redis Pipeline 和事务本质区别的理解,能否区分"性能优化"和"原子性保证"两个不同维度。

**知识点拆解**:
1. **Pipeline(管道)**:客户端批量发送命令,减少网络 RTT 开销。命令按顺序执行,但不保证原子性——中间某个命令失败,后续命令继续执行。
2. **MULTI/EXEC(事务)**:BEGIN 到 EXEC 之间的命令打包执行,EXEC 后按顺序执行,中间命令失败不会回滚之前的成功命令(Redis 事务无回滚语义)。
3. **WATCH + CAS(乐观锁)**:WATCH 监控 key 变更,EXEC 时若被监控 key 在事务期间被其他客户端修改,事务自动失败,不执行任何命令。
4. **Lua 脚本(EVAL)**:原子执行整段脚本,脚本内可基于 Redis 事务语义做补偿(类似 TCC),但无原生回滚。

**关键差异**:

| 维度 | Pipeline | MULTI/EXEC | WATCH+EXEC | Lua |
|------|---------|-----------|------------|-----|
| 网络开销 | 1 次 RTT | N 次 RTT | N+1 次 RTT | 1 次 RTT |
| 原子性 | 无 | 命令顺序执行,无回滚 | 失败则全部不执行 | 整段脚本原子 |
| 适用场景 | 批量写入、减少 RTT | 简单批处理 | 需要乐观锁 | 复杂业务逻辑 |

**Pipeline 风险**:阻塞主线程(一次性发太多命令)、网络缓冲溢出、命令间无依赖保证。

第二部分:考察方向与延伸递进

1. **Pipeline 和事务能不能结合使用?**
- 解答:不能直接结合。Pipeline 是客户端发送机制,事务是服务端执行机制,两者是正交的。想原子执行批量命令,用 MULTI/EXEC 或 Lua。

2. **Pipeline 阻塞主线程怎么排查?**
- 解答:SLOWLOG 看慢命令、INFO commandstats 看命令耗时分布、检查客户端是否一次性发太多命令。

3. **Lua 脚本和 MULTI/EXEC 怎么选?**
- 解答:简单批处理用 MULTI/EXEC,复杂业务逻辑(含 if/for)用 Lua。Lua 性能更好(减少 RTT),MULTI/EXEC 更简单。

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Redis Pipeline 和事务的目的完全不同——Pipeline 是为了减少网络 RTT 开销,事务是为了保证命令按顺序执行。

Pipeline 是客户端批量发送机制,一次 RTT 把多个命令打包发给服务端,服务端按顺序执行但不保证原子性——中间某个命令失败,后续命令继续执行。适合批量写入、减少网络开销的场景。

MULTI/EXEC 是服务端事务,BEGIN 到 EXEC 之间的命令打包,EXEC 后按顺序执行。但 Redis 事务无回滚——中间某个命令失败,之前的成功命令不回滚。需要乐观锁时用 WATCH + EXEC,被监控 key 在事务期间被修改则事务失败。

想要真正的原子性和复杂业务逻辑,用 Lua 脚本。Lua 整段原子执行,脚本内可以做类似 TCC 的补偿,但本质不是 ACID 事务。

Pipeline 有风险:一次性发太多命令会阻塞主线程,SLOWLOG 里能看到慢命令。生产环境建议客户端控制批量大小,比如每次 100 条命令。

#2 Redis为什么被设计成是单线程的? low redis

题目

Redis为什么被设计成是单线程的?

我的回答

Redis 使用单线程最核心的原因还是它自身整个链路中消耗的资源的成本来决定的。Redis 整个链路请求中,它并不是说在线程切换、线程业务上,Redis 自身进行计算方面使用 CPU 消耗的成本比较大,而是在网络层面开销相对更高。它的存储使用全都在内存中,然后获取数据也都是 KV 的形式,效率都极高,并不需要做线程切换之类的去提高并行的效率。最终选择了单线程的另一个原因,其实是多线程的引入会带来更多复杂的相关实现,比如多线程的竞争,然后多线程对同一内容的修改产生的锁如何处理。这些东西引入之后都会影响实际的效率,反而不如单线程对内存直接操作,快速拿快速结束,实现的效率也让其能达到 10 万 QPS 的这个能力,就是大部分多线程系统无法实现的。

点评

优点:- 瓶颈在网络 IO 判断到位- 多线程成本分析清晰- 对比 10 万 QPS 量级
不足:- 未提 I/O 多路复用 epoll/kqueue- 未提 Redis 6.0 多线程边界(socket IO vs 命令执行)- 未提对比 MySQL 为何必须多线程 > ℹ️ 完整讲解见上方首次记录(2026-09-20)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Redis 单线程设计的架构权衡深度理解,能否跳出"单线程 = 慢"的直觉误区。

**知识点拆解**:

1. **核心逻辑——瓶颈在哪?**
- Redis 是内存 KV 存储,命令执行极其轻量:网络接收 → 解析 → 内存查找 O(1) → 网络返回
- 瓶颈在网络 I/O,不在 CPU;单核 CPU 就能处理数万 QPS
- 对比 MySQL:SQL 解析/优化器/执行器(CPU 密集)+ 事务隔离 + 锁竞争 + 磁盘 I/O → 必须多线程

2. **单线程的收益**:无锁(无 CAS/死锁/内存屏障)、无上下文切换、天然原子性、稳定性、可预测性、GC 无关

3. **技术支撑——I/O 多路复用**:epoll/kqueue 事件循环,一个线程 hold 上万连接,时间复杂度 O(1)

4. **Redis 6.0 多线程边界**:
- Socket 读写、协议解析:多线程(可配 io-threads 2-4)
- 命令执行:主线程(processCommandAndResetClient)
- 持久化 fork:子进程 + COW
- AOF fsync:子线程
- 为什么命令执行不多线程?引入锁开销抵消收益;命令本身轻;单线程保证原子性

5. **单线程的瓶颈与扩展**:瓶颈是单核 CPU 打满;正确解法是 Cluster 分片,不要"让单节点变多线程"

6. **何时绕过单线程瓶颈**:Cluster 分片 / 6.0 多线程 / 慢命令优化(UNLINK/SCAN)/ Lua 拆分

7. **与其他 KV 存储对比**:Redis(单线程 + epoll)vs Memcached(一连接一线程)vs RocksDB(多线程 LSM)vs etcd(单线程 Raft)

8. **工程实践**:避免慢命令(KEYS→SCAN,大 DEL→UNLINK);拆分大 key;Cluster 分片;监控 SLOWLOG;6.0 参数 io-threads 2-4

第二部分:考察方向与延伸递进

**考察方向**:
- 深度:aeMain 事件循环源码?为什么 6.0 命令执行不多线程?
- 广度:与 Netty / Nginx / Kafka 的 I/O 模型对比
- 应用:单核 CPU 打满如何排查?大 key 阻塞如何规避?

**延伸递进面试题**:

1. **Redis 6.0 引入多线程为什么命令执行仍是单线程?**
- 解答要点:引入锁的开销抵消收益;命令本身轻;单线程保证原子性
2. **Redis 的事件循环(aeMain)如何调度?**
- 解答要点:epoll 监听 I/O 事件 → 时间事件 serverCron → 命令队列处理;命令执行同步阻塞
3. **单核 CPU 打满如何排查?**
- 解答要点:SLOWLOG 看慢命令 → INFO commandstats 看命令耗时分布 → 拆大 key / Cluster 分片
4. **Redis 的慢命令有哪些?**
- 解答要点:KEYS、SORT、SRANDMEMBER(大集合)、大 HGETALL/SMEMBERS、LPOP 大列表、Lua 脚本
5. **为什么 Memcached 用多线程而 Redis 不用?**
- 解答要点:Memcached 早期设计,一连接一线程;Redis 用 epoll 单线程更优
6. **如果让你设计一个类似 Redis 的系统,会怎么选?**
- 解答要点:命令轻 → 单线程 + I/O 多路复用;命令重 → 多线程 + 锁;磁盘密集 → 异步 IO / io_uring

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Redis 单线程的设计本质是架构权衡,核心判断是瓶颈在哪。

Redis 是内存 KV 存储,命令执行极其轻量——一次 GET 就是网络接收 → 解析 → 内存 O(1) 查找 → 网络返回,瓶颈在网络 I/O,不在 CPU。单核 CPU 就能处理数万 QPS,多核收益很低。

而多线程带来的成本很高:锁竞争、上下文切换、并发 bug、延迟抖动。单线程天然无锁、无死锁、无内存屏障、无 GC 暂停,稳定性极高,代码也简单。

技术支撑是关键——Redis 用 epoll/kqueue 做 I/O 多路复用,一个线程通过事件循环就能 hold 住上万连接,事件就绪才处理,时间复杂度 O(1)。这是单线程高并发的唯一基础。

Redis 6.0 引入多线程的边界要分清:多线程只做 socket 读写 + 协议解析,命令执行 processCommandAndResetClient 仍是主线程。因为命令本身轻,多线程收益低,引入锁开销抵消收益,而且单线程保证原子性语义不变。

对比 MySQL:MySQL 有 SQL 解析、优化器、执行器(CPU 密集)+ 事务隔离 + 锁竞争 + 磁盘 I/O(并发等待),所以必须多线程。Redis 命令简单,单线程足够。

单线程的瓶颈是单核 CPU 打满,正确解法是 Cluster 分片水平扩展,而不是让单节点变多线程。工程上我做过两个坑:一是 KEYS 命令阻塞主线程,改用 SCAN;二是大 key DEL 卡住,换 UNLINK 异步删除。

所以总结:Redis 单线程是基于"命令轻 + 网络密集"的最优架构选择,简单、稳定、可预测;瓶颈用分片扩展,而不是破坏单线程模型。
---

#3 RDB和AOF的写回策略分别是什么? low redis

题目

RDB和AOF的写回策略分别是什么?

我的回答

RDB 是一次性把整个的快照直接加载到内存中,一次性直接就完成了。而 AOF 本身存储的是大量的命令,它需要逐个去执行才能完成整个写回。所以在备份的时候,RDB 加载的会更快,AOF 在执行时间长了加载的会更慢。这个原因,Redis 后期推出了混合的策略。历史的数据使用 RDB,最新的数据使用 AOF,后续的逐步的进行替换,把 AOF 内容放到 RDB 中。RDB 来实现远期数据的存储,AOF 实行实时短期的几秒内数据的展现,这样可以更快的加载,也更全面的加载,保证系统的数据稳定性、系统数据安全。

点评

优点:- RDB/AOF 加载速度差异抓到了- Redis 4.0 混合持久化提到
不足:- ❌ 答非所问:题目问写回策略(fsync),答成了加载策略- ❌ 遗漏 fsync.always / fsync.everysec / no 三种策略- ❌ 遗漏 appendfsync 参数- ❌ 遗漏 RDB fork + COW 子进程机制- ❌ 遗漏 AOF rewrite (bgrewriteaof)

完整讲解

第一部分:知识重点剖析

**核心考点**:考察对 Redis 持久化写回策略(fsync)的理解,区分"写回"和"加载"两个不同方向。

**知识点拆解**:
1. **RDB 写回策略**:RDB 是快照,fork 子进程 + COW 机制。触发时机:手动(SAVE/BGSAVE)、配置(save 60 10000 / save 300 1000 / save 900 1)、子进程完成后原子替换。
2. **AOF 写回策略(appendfsync)**:
- **always**:每条命令立即 fsync 到磁盘,最安全但性能最低(QPS 降到几百)
- **everysec**(默认):每秒 fsync 一次,性能和安全折中,最多丢 1 秒数据
- **no**:由 OS 决定何时 fsync(通常 30 秒),性能最高但可能丢大量数据
3. **AOF rewrite**:AOF 文件过大时,BGREWRITEAOF 触发,fork 子进程重写压缩 AOF。
4. **混合持久化(Redis 4.0+)**:AOF rewrite 时前半段用 RDB 格式(历史数据),后半段用 AOF 格式(rewrite 期间的增量),兼顾启动速度和数据安全性。

**关键参数**:
- appendfsync(always/everysec/no)
- auto-aof-rewrite-percentage(默认 100)
- auto-aof-rewrite-min-size(默认 64MB)

第二部分:考察方向与延伸递进

1. **RDB 和 AOF 同时开启,恢复优先级?**
- 解答:默认优先 AOF,因为 AOF 数据更完整。可通过 aof-use-rdb-preamble 调整。

2. **AOF 文件过大怎么办?**
- 解答:BGREWRITEAOF 自动触发(达到阈值时),或手动执行。rewrite 期间新命令写入临时 AOF,完成后合并。

3. **fsync.always 为什么生产不用?**
- 解答:每次命令都要 fsync 到磁盘,QPS 降到几百,性能损失巨大。生产一般用 everysec,最多丢 1 秒数据。

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Redis 写回策略主要是 AOF 的 appendfsync 参数,分三档:always、everysec、no。

always 是每条命令立即 fsync 到磁盘,最安全但性能最低,QPS 降到几百,生产一般不用。

everysec 是默认值,每秒 fsync 一次,性能和安全性折中,最多丢 1 秒数据。生产环境推荐这个。

no 是让 OS 决定何时 fsync(通常 30 秒),性能最高但可能丢大量数据,一般不用。

RDB 是快照,fork 子进程 + COW 机制,触发时机是手动 SAVE/BGSAVE 或配置(save 60 10000 等)。RDB 写回后原子替换旧文件。

Redis 4.0 引入了混合持久化,AOF rewrite 时前半段用 RDB 格式存历史数据,后半段用 AOF 格式存增量,兼顾启动速度和数据安全性。

参数方面,appendfsync 控制 AOF 写回频率,auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 控制 AOF rewrite 触发阈值。

#4 Redis中的Zset是怎么实现的? medium redis

题目

Redis中的Zset是怎么实现的?

我的回答

redis 中 zset 是有序集合,在底层有两组实现一个是小批量数据的压缩列表,一个是大量数据后的跳跃表;小批量数据到时候,直接按照分数信息进行集合顺序插入到压缩列表中,读取数据的时候遍历压缩列表;当数据量比较大了之后,使用跳跃表实现 zset 有序集合,基于分数构建多层级大跳跃表,每一层级都类似二分的方式去找到下一层级中的一个位置,逐层的快速找到最底层目标数据

点评

优点:- 两种底层编码抓到- 跳跃表多层级定位机制描述基本对
不足:- ❌ 编码是二选一非递进:ziplist/listpack vs skiplist 是编码切换- ❌ 5.0+ ziplist 已被 listpack 完全替代- ❌ 跳跃表节点还挂哈希表(O(1) member→score)- ❌ 遗漏编码切换阈值:zset-max-listpack-entries=128 / zset-max-listpack-value=64- ⚠️ 跳跃表描述"二分"不准确,是跳级定位不是二分

完整讲解

第一部分:知识重点剖析

**核心考点**:考察对 Zset 底层编码切换机制的理解,区分"编码二选一"和"递进升级"。

**知识点拆解**:
1. **Zset 两种编码**:
- **listpack**(Redis 5.0+,替代 ziplist):连续内存块,小数据高效
- **skiplist + hashtable**:跳跃表 + 哈希表组合,大数据高效
2. **编码切换阈值**(二选一,非递进):
- zset-max-listpack-entries(默认 128):元素数超过则切换 skiplist
- zset-max-listpack-value(默认 64 字节):最大元素值超过则切换 skiplist
3. **skiplist 结构**:
- 多层级跳跃表,底层 O(n),顶层 O(1)
- 每层节点挂指针到下一层,跳级定位
- 每个节点还挂哈希表(O(1) member→score 查找)
4. **ziplist vs listpack**:5.0+ ziplist 已被 listpack 完全替代,性能更好、内存更安全

**关键命令复杂度**:
- ZADD/ZSCORE/ZREMRANGEBYSCORE:listpack O(n) / skiplist O(log n)
- ZRANGE:listpack O(n) / skiplist O(log n + m)
- ZRANK:skiplist O(log n) / listpack O(n)

第二部分:考察方向与延伸递进

1. **为什么 Zset 不用 B+ 树?**
- 解答:B+ 树节点大、内存浪费、实现复杂;跳跃表内存紧凑、实现简单、插入删除效率高。Redis 用跳跃表平衡了性能和复杂度。

2. **Zset 编码切换后能回退吗?**
- 解答:不能。一旦切换到 skiplist,即使元素数减少也不会回退 listpack,避免频繁切换开销。

3. **Zset 和 SortedSet 接口区别?**
- 解答:Redis 中 Zset 就是 SortedSet,Java 客户端用 Jedis 的 zadd/zrange 等命令。

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Zset 有两种底层编码,是二选一关系,不是递进升级。

小数据用 listpack(Redis 5.0+ 替代了 ziplist),连续内存块,内存紧凑。大数据用 skiplist + hashtable 组合。

编码切换阈值是两个:zset-max-listpack-entries(默认 128 个元素)和 zset-max-listpack-value(默认 64 字节)。任一超过就切换到 skiplist。

skiplist 是多层级跳跃表,底层 O(n),顶层 O(1),跳级定位到目标节点。每个节点还挂一个哈希表,通过 member 查找 score 是 O(1)。

ZADD/ZSCORE/ZREMRANGEBYSCORE 在 listpack 上 O(n),在 skiplist 上 O(log n)。ZRANGE 在 skiplist 上 O(log n + m),m 是返回元素数。

编码切换不可逆——一旦切换到 skiplist,即使元素数减少也不会回退 listpack,避免频繁切换的开销。

为什么不用 B+ 树?B+ 树节点大、内存浪费、实现复杂;跳跃表内存紧凑、实现简单、插入删除效率高。Redis 选跳跃表是性能和复杂度的平衡。

#5 为什么需要延迟双删,两次删除的原因是什么? medium redis

题目

为什么需要延迟双删,两次删除的原因是什么?

我的回答

延迟双删是为了保证多线程下,并行进行对数据进行更新时缓存的准确性的;因为多线程同时进行操作的情况下,A 线程更新了数据库,然后删除缓存,这个阶段如果 B 线程读取数据库,数据,由于 A 数据库的事务未执行完或者主从数据库的主从延迟导致 B 读取到了老的数据,并写回到缓存中,导致缓存错误,这时候延迟进行二次删除可以有效规避这种问题;另外可以通过 mq 监控数据库的 binlog 变更来对缓存进行删除处理,不过需要对应依赖和实现

点评

优点:- 并发场景抓到了- A/B 线程时序问题描述清楚- Binlog+MQ 备选方案提到
不足:- ❌ 遗漏延迟时间如何确定(覆盖主从同步+事务提交时间)- ❌ 遗漏延迟双删的本质是 Cache Aside + 二次删除- ❌ 遗漏先更DB vs 先删缓存的决策- ⚠️ "主从延迟"表述不精准,应是"并发读请求读到未提交或旧数据"

完整讲解

第一部分:知识重点剖析

**核心考点**:考察对缓存一致性策略的理解,区分"延迟双删"和"Cache Aside"的层次关系。

**知识点拆解**:
1. **Cache Aside 模式**:读时先读缓存,缓存未命中则读 DB 并写回缓存;写时先更 DB,再删缓存(不是更新缓存,避免并发写导致缓存不一致)。
2. **延迟双删的本质**:Cache Aside + 二次删除,解决并发读写时序问题。
3. **并发时序问题**:
- T1:A 线程更新 DB(事务未提交)
- T2:B 线程读 DB(读到旧数据)
- T3:A 线程删除缓存
- T4:B 线程把旧数据写回缓存 → 缓存错误
4. **延迟双删方案**:更新 DB → 删除缓存 → 延迟 N ms → 再删除缓存。第二次删除清掉 B 线程写回的旧数据。
5. **延迟时间确定**:覆盖"事务提交时间 + 主从同步时间 + 读请求耗时",一般 500ms-2s。
6. **其他方案**:
- **Binlog + MQ(Canal)**:监控 DB binlog,异步删缓存,彻底解耦
- **订阅 DB 变更**:类似 Canal,但 DB 原生支持
- **先删缓存再更 DB**:风险更大(删缓存后 DB 更新失败,缓存永久缺失)

**局限性**:延迟双删无法解决所有并发问题,仍需最终一致性;延迟时间不好精确确定;网络抖动可能放大问题。

第二部分:考察方向与延伸递进

1. **先更 DB 还是先删缓存?**
- 解答:Cache Aside 推荐先更 DB 再删缓存。先删缓存再更 DB 风险更大(删缓存后 DB 更新失败,缓存永久缺失)。

2. **延迟双删和 Canal 怎么选?**
- 解答:简单场景用延迟双删,复杂/高一致性要求场景用 Canal(解耦、可靠、可追溯)。

3. **延迟双删的延迟时间怎么定?**
- 解答:覆盖事务提交时间 + 主从同步时间 + 读请求耗时,一般 500ms-2s。可通过压测确定。

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,延迟双删的本质是 Cache Aside + 二次删除,解决并发读写时序问题。

Cache Aside 模式下,写时先更 DB 再删缓存。但并发时序会导致缓存不一致:A 线程更新 DB(事务未提交),B 线程读 DB 读到旧数据,A 线程删除缓存,B 线程把旧数据写回缓存——缓存就错了。

延迟双删方案:更新 DB → 删除缓存 → 延迟 N ms → 再删除缓存。第二次删除清掉 B 线程写回的旧数据。

延迟时间需要覆盖"事务提交时间 + 主从同步时间 + 读请求耗时",一般 500ms-2s。可通过压测确定具体值。

更彻底的方案是 Canal:监控 DB binlog,异步删缓存。Canal 解耦、可靠、可追溯,适合高一致性要求的场景。

延迟双删有局限:无法解决所有并发问题,仍需最终一致性;延迟时间不好精确确定;网络抖动可能放大问题。生产环境建议用 Canal,简单场景用延迟双删。

#6 Redis的Key和Value的设计原则有哪些? medium redis

题目

Redis的Key和Value的设计原则有哪些?

我的回答

Redis 中 key 和 value 都需要一定选择,比如 key 的设计需要考虑到业务识别性,一般我设计 key 的时候,会考虑 key 所在的服务,key 对应的业务表,key 所对应的业务名,一层层去明确一个key 的所述,方便快速定位 key,比如 用户服务的用户信息缓存,单个用户就是 user:userTable:info:{id},如果总列表就是 user:userTable:list:all; value 方面主要是选择合适的类型,以及选择合适的序列化方式,比如 list 数据就可以放到 redis 的 list 中,而不是直接用 str 去存储,对象信息安字段存储到 hashMap 中,而不是整个存储成 str

点评

优点:- Key 分层命名规范抓到- 举例 user:userTable:info:{id} 具体- Value 类型选择思路对
不足:- ❌ 遗漏 key 长度控制(<128 字节)- ❌ 遗漏避免通配符(* ? 避免 SCAN/KEYS 慢查询)- ❌ 遗漏大 key 限制(<10KB 为佳,<100MB 绝对禁止)- ❌ 遗漏 value 大小控制(字符串 <1MB,集合 <5000 元素)- ❌ 遗漏过期时间设置原则(热点短、冷长)- ❌ 遗漏避免特殊字符(空格、换行)

完整讲解

第一部分:知识重点剖析

**核心考点**:考察 Redis 工程化设计能力,能否从业务语义、性能、可维护性三个维度设计 key/value。

**知识点拆解**:
1. **Key 设计原则**:
- **业务语义**:服务:业务表:业务名:{ID} 分层,如 user:profile:info:{id}
- **长度控制**:<128 字节,避免过长浪费内存和带宽
- **避免通配符**:不用 * ? 等,避免 SCAN/KEYS 慢查询
- **避免特殊字符**:空格、换行、冒号等慎用,冒号用于分层
- **大小写敏感**:Key 区分大小写,统一用小写
2. **Value 设计原则**:
- **类型选择**:list/hash/zset/set/string 按业务选择
- **序列化**:JSON(可读性)vs Protobuf(性能)vs 原生类型
- **大小控制**:字符串 <1MB,集合 <5000 元素,避免大 key
- **大 key 拆分**:单 key >10KB 拆分,>100MB 绝对禁止
3. **过期时间原则**:
- 热点数据短(分钟级)、冷数据长(天级)
- 不缓存则不设过期(永久)
- 过期时间加随机偏移(避免雪崩)
4. **性能优化**:
- 避免 KEYS 命令(O(N) 阻塞),用 SCAN
- 避免大 key 操作(HGETALL 大 hash、LRANGE 大 list)
- 批量操作用 Pipeline 减少 RTT

**常见错误**:
- 用 DB 表名做 key(无业务语义)
- 单 key 存百万元素(大 key)
- key 含空格(解析问题)
- 不设过期时间(缓存雪崩)

第二部分:考察方向与延伸递进

1. **如何发现大 key?**
- 解答:redis-cli --bigkeys 扫描,或 INFO memory 看 used_memory,或 RDB 分析工具。

2. **Hash 和 String 存对象怎么选?**
- 解答:字段多、需单字段访问用 Hash;字段少、整体读写用 String+序列化。Hash 单字段 O(1),String 整体读写更快。

3. **过期时间如何避免雪崩?**
- 解答:过期时间加随机偏移(如 base + random(0, 300s)),避免大量 key 同时过期。

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Key 和 Value 设计是 Redis 工程化的核心,我从业务语义、性能、可维护性三个维度说。

Key 设计:业务语义分层,如 user:profile:info:{id},服务:业务表:业务名:{ID}。长度控制在 128 字节内,避免过长浪费内存和带宽。避免通配符(* ?),避免 SCAN/KEYS 慢查询。避免特殊字符(空格、换行),冒号用于分层。统一小写,Key 区分大小写。

Value 设计:类型按业务选择,list/hash/zset/set/string。序列化 JSON 可读性好,Protobuf 性能好。大小控制:字符串 <1MB,集合 <5000 元素,单 key >10KB 拆分,>100MB 绝对禁止。

过期时间:热点数据短(分钟级)、冷数据长(天级),过期时间加随机偏移避免雪崩。不缓存则不设过期。

性能优化:避免 KEYS 命令(O(N) 阻塞),用 SCAN。避免大 key 操作(HGETALL 大 hash、LRANGE 大 list)。批量操作用 Pipeline 减少 RTT。

常见错误:用 DB 表名做 key(无业务语义)、单 key 存百万元素(大 key)、key 含空格(解析问题)、不设过期时间(缓存雪崩)。

发现大 key 用 redis-cli --bigkeys 扫描,或 INFO memory 看 used_memory。Hash 和 String 存对象:字段多、需单字段访问用 Hash;字段少、整体读写用 String+序列化。

#7 如何实现缓存的预热? medium practice

题目

如何实现缓存的预热?

我的回答

所谓缓存预热就是对有感知知道会被大批量访问的内容进行提前访问,将内容加载到缓存中,防止因为缓存过期导致读取缓存时出现无缓存的问题并发去访问数据库,或者造成阻塞;预热一般是通过脚本或者计划任务执行的,另外对于长期热度的 key 可能直接会设置不过期,只有手动删除更新到时候才会将对应到缓存更新

点评

优点:- 核心目的抓到- 计划任务/脚本方法提到- 长期热点不过期思路对
不足:- ❌ 遗漏预热时机(服务启动 vs 大促前)- ❌ 遗漏预热方法分类(全量/增量/按需)- ❌ 遗漏预热数据量控制(分批避免雪崩)- ❌ 遗漏预热失败处理(重试/降级/监控)- ❌ 遗漏预热顺序(依赖关系、热点优先)- ❌ 遗漏预热后监控(命中率、加载耗时)

完整讲解

第一部分:知识重点剖析

**核心考点**:考察缓存预热的工程化能力,区分"时机、方法、失败处理、监控"四个维度。

**知识点拆解**:
1. **预热时机**:
- **服务启动时**:应用启动后异步加载热点数据(如商品列表、用户信息)
- **大促前**:手动触发预热脚本,提前加载促销商品数据
- **缓存过期后**:定时任务提前刷新,避免过期瞬间空缓存
2. **预热方法**:
- **全量加载**:从 DB 批量查询,Pipeline 写入缓存(适合数据量小)
- **增量加载**:只加载变化的数据(如按更新时间增量)
- **按需加载**:首次访问时加载,后续命中缓存(冷启动慢但省资源)
- **分布式预热**:多实例分摊预热任务,避免单点瓶颈
3. **预热数据量控制**:
- **分批加载**:每批 100-1000 条,避免一次性打满 DB 和 Redis
- **限流**:预热速率限制(如 1000 QPS),避免影响线上业务
- **优先级**:热点优先(Top N 商品先加载)
4. **预热失败处理**:
- **重试**:失败重试 3 次,指数退避
- **降级**:预热失败不影响线上,缓存未命中走 DB
- **告警**:预热失败率 >5% 告警
5. **预热后监控**:
- **命中率**:预热后命中率应 >90%
- **加载耗时**:监控预热任务耗时
- **缓存大小**:监控 used_memory,避免预热导致 OOM

**与缓存击穿/雪崩的区别**:
- **预热**:主动加载,避免冷启动
- **击穿**:热点 key 过期瞬间并发穿透,用互斥锁或永不过期
- **雪崩**:大量 key 同时过期,用过期时间随机偏移

第二部分:考察方向与延伸递进

1. **预热和缓存击穿的区别?**
- 解答:预热是主动加载(事前),击穿是被动防护(事中)。预热解决冷启动,击穿解决热点 key 过期瞬间并发穿透。

2. **预热导致 DB 压力太大怎么办?**
- 解答:分批加载、限流、优先级排序。预热速率 < 线上 QPS 的 10%。

3. **预热后缓存命中率低怎么办?**
- 解答:检查预热数据是否热点、过期时间是否过短、是否有数据变更导致缓存失效。

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,缓存预热我从时机、方法、失败处理、监控四个维度说。

时机:服务启动时异步加载热点数据,大促前手动触发预热脚本,缓存过期前定时刷新避免空缓存。

方法:全量加载(数据量小)、增量加载(只加载变化的)、按需加载(首次访问加载)、分布式预热(多实例分摊)。分批加载每批 100-1000 条,限流避免影响线上业务,热点优先。

数据量控制:预热速率限制在 1000 QPS 以内,避免打满 DB 和 Redis。按优先级加载,Top N 商品先加载。

失败处理:失败重试 3 次(指数退避),预热失败不影响线上(缓存未命中走 DB),失败率 >5% 告警。

预热后监控:命中率应 >90%,加载耗时、缓存大小(used_memory)都要监控,避免预热导致 OOM。

和缓存击穿/雪崩的区别:预热是主动加载(事前),击穿是被动防护(事中),击穿用互斥锁或永不过期,雪崩用过期时间随机偏移。

生产实践:大促前 1 小时启动预热脚本,分批加载 Top 1000 商品,预热完成后监控命中率。预热失败降级走 DB,不影响用户体验。

#8 Redis的zset实现排行榜,实现分数相同按照时间顺序排序,怎么做? high practice

题目

Redis的zset实现排行榜,实现分数相同按照时间顺序排序,怎么做?

我的回答

zset的排序规则只支持分数排序,如果要实现相同分数按照时间排序有两种思路,一个是在分数的后面跟上时间戳,作为分数的补充内容一起进行排序,另外就是在 zset 的 value 中保存时间,读取相同分数的内容后,在业务端进行二次排序,都能实现同分数后按照其他规则排序

点评

优点:- 两种思路都提到- 分数拼接方案抓到
不足:- ❌ 遗漏分数拼接具体实现(如 score * 1e13 + timestamp)- ❌ 遗漏时间戳精度选择(毫秒 vs 微秒)- ❌ 遗漏同毫秒内排序边界情况- ❌ 遗漏业务端二次排序在分页场景的代价- ❌ 遗漏排行榜扩展(Top N、我的排名、实时推送)

完整讲解

第一部分:知识重点剖析

**核心考点**:考察 Zset 排行榜的工程化实现能力,理解分数拼接技巧。

**知识点拆解**:
1. **问题背景**:Zset 按分数排序,分数相同时按 member 字典序排序(不稳定)。业务需要分数相同时按时间排序。
2. **方案一:分数拼接**
- **原理**:分数 = 业务分数 × 1e13 + 时间戳(毫秒)
- **示例**:分数 100,时间戳 1690000000000 → 10016900000000000
- **优点**:Zset 原生支持,查询高效
- **缺点**:时间戳精度限制(毫秒内无法区分)、分数精度限制
3. **方案二:业务端二次排序**
- **原理**:Zset 存 score + member,member 中包含时间戳,读取后在业务端排序
- **优点**:灵活,支持任意排序规则
- **缺点**:分页场景困难(需加载全部数据)、性能差
4. **方案三:分数分段**
- **原理**:分数高位存业务分数,低位存时间戳
- **示例**:分数 1000000 + 时间戳毫秒(取后 6 位)
- **优点**:精确到秒
- **缺点**:分数范围有限
5. **排行榜常见扩展**:
- **Top N**:ZREVRANGE 0 N-1
- **我的排名**:ZREVRANK
- **分数变化推送**:Pub/Sub 或 Redis Stream
- **分维度**:实时榜/小时榜/日榜/周榜(多个 Zset)

**关键命令**:
- ZADD:添加/更新分数
- ZREVRANGE:倒序范围查询
- ZREVRANK:获取排名 O(log n)
- ZSCORE:获取分数 O(1)

第二部分:考察方向与延伸递进

1. **分数拼接的时间戳精度怎么选?**
- 解答:毫秒(13 位)够用,微秒(16 位)精度更高但分数范围受限。一般用毫秒。

2. **同毫秒内多个用户分数相同怎么排?**
- 解答:按 member 字典序(默认),或业务端二次排序。同毫秒概率极低,一般不影响业务。

3. **排行榜如何支持实时更新?**
- 解答:ZADD 更新分数,Pub/Sub 或 Redis Stream 推送变化,客户端订阅实时更新。

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Zset 排行榜分数相同按时间排序有两种方案。

方案一是分数拼接:分数 = 业务分数 × 1e13 + 时间戳(毫秒)。例如分数 100,时间戳 1690000000000,拼接后是 10016900000000000。Zset 原生支持,查询高效。缺点是时间戳精度限制,毫秒内无法区分。

方案二是业务端二次排序:Zset 存 score + member,member 中包含时间戳,读取后在业务端排序。灵活,支持任意排序规则。缺点是分页场景困难(需加载全部数据)、性能差。

我推荐方案一,分数拼接。时间戳用毫秒够用,同毫秒概率极低,不影响业务。

排行榜常见扩展:Top N 用 ZREVRANGE 0 N-1,我的排名用 ZREVRANK O(log n),分数变化用 Pub/Sub 或 Redis Stream 推送,分维度(实时/小时/日/周榜)用多个 Zset。

生产实践:Top 1000 排行榜,分数拼接毫秒时间戳,ZADD 更新分数,Pub/Sub 推送变化,客户端订阅实时更新。分维度排行榜用多个 Zset,每个 Zset 独立过期时间。

#9 如何实现百万级排行榜功能? high practice

题目

如何实现百万级排行榜功能?

我的回答

百万级数据的排行榜也不会查看全部百万的数据,在缓存中只会加载 topN 前置查看 topN 可以直接在 redis 中通过 zset 去实现,按照对应分数设定排序,超出预期的内容,如果有查看的,可以基于数据库去对分页内容进行查询,这部分业务逻辑实现的话就去基于数据库做就好了,查看频率会比较低,可以在排行榜处增加查看更多,点击后查看,然后跳转到独立页面进行单独实现,这堆查看并发量就没那么高了

点评

优点:- Top N 缓存 + DB 分页思路抓到- 查看更多跳转独立页面方案对
不足:- ❌ 遗漏 Top N 大小如何确定- ❌ 遗漏排行榜实时性(Lua 原子更新/消息队列异步)- ❌ 遗漏分维度排行榜(实时/小时/日/周榜)- ❌ 遗漏排行榜降级方案- ❌ 遗漏排行榜防作弊- ❌ 遗漏并发更新性能优化- ❌ 遗漏 ZREVRANK 获取排名 O(log N)

完整讲解

第一部分:知识重点剖析

**核心考点**:考察大规模排行榜的架构设计能力,理解"缓存 + DB"分层策略。

**知识点拆解**:
1. **核心思路**:缓存只存 Top N,其余走 DB 分页
2. **Top N 大小确定**:
- 业务场景:排行榜展示 Top 10/100/1000
- 性能考虑:ZREVRANGE 0 999 在百万级 Zset 上 O(log n + 1000),毫秒级
- 推荐:Top 1000,覆盖 99% 展示需求
3. **架构分层**:
- **Redis(Top N)**:Zset 存 Top 1000,实时查询
- **DB(全量)**:MySQL 存全量数据,分页查询
- **查看更多**:Top N 外用户点击"查看更多"跳转独立页面,DB 分页
4. **排行榜实时性**:
- **Lua 原子更新**:ZADD + ZREVRANGE 原子执行,避免并发更新导致数据不一致
- **MQ 异步更新**:业务事件发 MQ,消费者更新 Zset,削峰解耦
- **定时刷新**:定时任务从 DB 同步 Top N 到 Redis
5. **分维度排行榜**:
- 实时榜:Zset,实时更新
- 小时榜/日榜/周榜:定时聚合,独立 Zset
- 过期策略:每个维度独立过期时间
6. **排行榜降级**:
- **缓存宕机**:降级到只读 DB(限流)
- **DB 宕机**:只展示 Redis 缓存数据
- **MQ 积压**:降级为定时刷新
7. **防作弊**:
- 异常分数检测(分数突增告警)
- 频率限制(单用户更新频率限制)
- 黑名单机制

**关键性能指标**:
- Top 1000 查询:O(log n + 1000),毫秒级
- 更新分数:O(log n),毫秒级
- 获取排名:O(log n),毫秒级

第二部分:考察方向与延伸递进

1. **Top N 大小如何确定?**
- 解答:根据业务展示需求(Top 10/100/1000)和性能(ZREVRANGE 复杂度)确定。一般 Top 1000 覆盖 99% 展示需求。

2. **排行榜如何保证实时性?**
- 解答:Lua 原子更新(ZADD + ZREVRANGE)、MQ 异步更新(削峰解耦)、定时刷新(兜底)。

3. **排行榜如何防作弊?**
- 解答:异常分数检测(突增告警)、频率限制(单用户更新频率)、黑名单机制。

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,百万级排行榜的核心思路是缓存只存 Top N,其余走 DB 分页。

Top N 大小根据业务需求和性能确定,一般 Top 1000 覆盖 99% 展示需求。ZREVRANGE 0 999 在百万级 Zset 上 O(log n + 1000),毫秒级响应。

架构分层:Redis Zset 存 Top 1000,实时查询;MySQL 存全量数据,分页查询。Top N 外用户点击"查看更多"跳转独立页面,DB 分页查询。

排行榜实时性:Lua 原子更新(ZADD + ZREVRANGE 原子执行,避免并发不一致)、MQ 异步更新(业务事件发 MQ,消费者更新 Zset,削峰解耦)、定时刷新(兜底)。

分维度排行榜:实时榜、小时榜、日榜、周榜用多个独立 Zset,每个维度独立过期时间。

降级方案:缓存宕机降级到只读 DB(限流),DB 宕机只展示 Redis 缓存数据,MQ 积压降级为定时刷新。

防作弊:异常分数检测(突增告警)、频率限制(单用户更新频率)、黑名单机制。

性能指标:Top 1000 查询 O(log n + 1000) 毫秒级,更新分数 O(log n) 毫秒级,获取排名 O(log n) 毫秒级。

#10 秒杀10万QPS下CPU95与Redis连接耗尽 high practice

题目

秒杀10万QPS下CPU95与Redis连接耗尽

我的回答

首先解决问题,将压力降下来,在前端做一定拦截,降低用户循环请求,然后再去网关层做行为识别将黑灰用户识别进行拦截,具体黑灰识别需要对应策略引擎,或者行为分析,简单都可以依据当前秒杀或者短期的连续访问的用户进行拦截,只放入部分;然后到达业务层开始访问 redis 的流量如果都能达到 10wqps,那么就需要使用本地缓存承接了,因为是秒杀场景,库存有限,本地缓存扣完了再去扣redis,然后订单再去访问数据库;上面是业务处理流程;然后定位 cpu 问题,redis连接池耗尽,查看下是否有慢查询出现,10wqps 都访问到一个 redis 节点么?如果是热门 key 需要做额外的划分,加分片将一个 key 拆分成多个到多个 redis 节点上去承接流量;cpu95,看线程具体是哪个引起的,用 top查看进程 id,然后 top -h查看对应的线程 id,转 16 进制后去 jstack 中查看具体是什么线程出现了问题,需要先用 jstack 导出线程情况,95%的使用率基本已经无法打开 arthas了否则用 arthas 也能直观都看到线程使用情况,定位到具体线程;然后对线程内容进行分析;redis 连接池耗尽,要么是链接池配置小了,要么是有慢查询,需要查看慢查询日志,以及查看链接分布情况,是否都是访问热key 导致都访问到一个节点,如果是,就进行热 key 拆分,给热 key 进行加分区,分片到多个节点上,分散流量;mysql 锁等待 10 秒,通过慢查询定位具体信息,查看mysql 锁统计表看具体持有者,这可以异步去记性 mysql 操作,前置在 redis 中执行库存等扣除,异步去通过 mq 在进行 mysql 操作

点评

优点:- 思路全面:先降压力→前端拦截→网关风控→业务层本地缓存- 秒杀架构:本地缓存扣库存→异步落库- CPU 排查路径:top→top -Hp→16 进制→jstack- Redis 热 key 拆分提到- MySQL 锁等待分析 + Redis 前置扣库存 + MQ 异步落库
不足:- ⚠️ "95% 使用率基本无法打开 arthas"表述错误(Arthas 是低开销的)- ❌ 遗漏 top 看 iowait/system 区分用户态/系统态- ❌ 遗漏 async-profiler 火焰图(比 jstack 更直观)- ❌ 遗漏 MySQL 8.0+ performance_schema.data_lock_waits- ❌ 遗漏 innodb_lock_wait_timeout 超时控制- ❌ 遗漏乐观锁(version 字段)- ❌ 遗漏网关限流(令牌桶、漏桶)

完整讲解

第一部分:知识重点剖析

第二部分:考察方向与延伸递进

第三部分:面试者参考回答(300-600 字口语化)

📚 查看全部面试记录