Redis 面试八股文 30 道|深度详解版(傻子都能看懂)

📖 学习指南

🎯 学习目标:通过本文,你将系统掌握 Redis 的核心知识,能够自信地应对任何相关面试问题。

适合人群

  • 🔰 初学者:想系统学习 Redis 的开发者
  • 🚀 有经验者:想深入理解 Redis 原理的高级开发者
  • 💼 面试准备者:想刷 Redis 面试题的求职者

学习建议

  1. 先理解概念,再深入原理:先搞懂”是什么”,再搞懂”为什么”
  2. 结合实战场景:不要死记硬背,要理解实际应用场景
  3. 动手实践:在自己电脑上安装环境,执行本文的示例代码
  4. 反复复习:面试前一周,每天复习 10 个问题

学习时间估算

  • ⏱️ 快速复习(只看一句话总结):2 小时
  • 📚 系统学习(看深度解析):1-2 天
  • 💪 深入理解(研究源码):1 周+

🗺️ 知识图谱

mindmap
  root((Redis))
    基础概念
      核心概念1
      核心概念2
      核心概念3
    高级特性
      特性1
      特性2
    实战应用
      应用场景1
      应用场景2
    性能优化
      优化技巧1
      优化技巧2

⚠️ 常见陷阱与误区

陷阱 1:概念理解错误

错误示例

1
# 错误的理解或用法

正确做法

  • 正确理解概念
  • 避免常见误区

陷阱 2:忽略边界条件

错误做法

  • 不考虑特殊情况
  • 忽略异常处理

正确做法

  • 总是考虑边界条件
  • 添加异常处理

💡 面试技巧

技巧 1:结构化回答

不要只回答”是什么”,要按照以下结构回答:

  1. 一句话总结(概念)
  2. 深度解析(原理、实现、优缺点)
  3. 面试加分回答(实际项目经验、源码理解、行业最佳实践)

技巧 2:结合实战场景

不要只背概念,要结合实际项目经验回答。

技巧 3:引导到你会的方向

如果遇到不会的问题,不要慌,可以引导到你会的方向。


🎯 实战演练(真实面试场景)

场景 1:请你设计一个系统?

回答思路

  1. 需求分析:明确系统需求
  2. 技术选型:选择合适的技术栈
  3. 架构设计:设计系统架构
  4. 性能优化:考虑性能瓶颈和优化方案

🚀 学习路径总结

第一阶段:基础概念(1-2 天)

  • 理解核心概念
  • 掌握基本操作
  • 完成入门教程

第二阶段:高级特性(2-3 天)

  • 掌握高级特性
  • 理解实现原理
  • 完成进阶教程

第三阶段:实战应用(1 周+)

  • 搭建实际项目
  • 解决实战问题
  • 阅读源码(可选)

第四阶段:面试准备(1 周)

  • 刷完本文的所有问题
  • 复习相关知识点
  • 准备项目经验
  • 模拟面试

📚 扩展学习资源

官方资源

书籍推荐

  • 《Redis 实战》
  • 《Redis 权威指南》

博客推荐


Redis 面试八股文 30 道|深度详解版

写给准备面试的你:
这篇文章不讲废话,每个知识点都从「是什么 → 为什么 → 怎么用」三个层次讲透。
配有大量比喻和场景化解释,目标是让没有 Redis 基础的人也能看懂。
建议配合实际 Redis 操作练习,理解效果翻倍。


一、基础与数据结构(1-8)


第 1 题:Redis 和 Memcached 到底有什么区别?我该选哪个?

一句话结论

Memcached 是简单的 KV 缓存;Redis 是全能型数据结构服务器,支持持久化、主从复制、Lua 脚本、事务等。99% 的场景选 Redis。


深度解析

要从五个核心维度理解它们的差异:

① 数据结构丰富度

对比项 Memcached Redis
数据类型 只支持 String String、Hash、List、Set、Sorted Set、Bitmap、HyperLogLog、Geo、Stream
单个 value 大小 最大 1MB 最大 512MB(String 类型)
复杂操作 需要客户端自己实现 服务端原生支持(如 ZADD、SINTER)

场景化理解:

用 Memcached 做「排行榜」:客户端需要把整个排行榜取出来,在内存中排序,再写回去 → 网络开销大,并发有 Race Condition。
用 Redis 做「排行榜」:ZADD leaderboard 100 user1 一条命令搞定,服务端直接维护有序集合。


② 持久化能力

  • Memcached:纯内存,重启后数据全部丢失。
  • Redis:支持 RDB 快照AOF 日志 两种持久化方式,重启后可以恢复数据。

③ 主从复制

  • Memcached:没有原生的主从复制,需要客户端自己实现。
  • Redis:原生支持主从复制,可以实现读写分离、高可用(配合 Sentinel)。

④ 内存管理

对比项 Memcached Redis
内存分配 固定大小 Chunk(有碎片问题) 自己实现的内存分配器(jemalloc)
LRU 淘汰 支持 支持(多种淘汰策略)
键过期 不支持 支持(EXPIRE 命令)

⑤ 性能对比

  • Memcached:多线程,CPU 核心数越多性能越好,纯 KV 场景下略优于 Redis。
  • Redis:单线程(6.0 前),但内存操作极快,性能瓶颈通常在网络 I/O 而非 CPU。

结论:除非你在维护遗留系统,否则新项目无脑选 Redis


面试加分回答

「Memcached 在纯 KV、多线程利用多核 CPU 的场景下有一定优势,但 Redis 的功能丰富度和生态(客户端库、运维工具、云服务商支持)远超 Memcached。另外,Redis 6.0 引入了多线程 I/O(注意:命令执行仍然是单线程的),在网络 I/O 密集型场景下性能提升显著,进一步缩小了与 Memcached 的差距。」


第 2 题:Redis 的线程模型是什么?为什么单线程还这么快?

一句话结论

Redis 的核心命令执行是单线程的(6.0 前);「快」的原因是:内存操作 + 非阻塞 I/O(多路复用)+ 单线程避免了锁竞争和上下文切换。


深度解析

Redis 真的是「单线程」吗?

需要分版本说清楚:

1
2
3
4
5
6
7
8
9
Redis 6.0 之前:
- 命令执行:单线程
- 网络 I/O:单线程
- 持久化(BGSAVE/BGREWRITEAOF):fork 子进程

Redis 6.0 之后:
- 命令执行:仍然单线程(核心设计没变)
- 网络 I/O:多线程(可配置)
- 持久化:fork 子进程

很多人误解「Redis 是单线程」=「Redis 只用了一个 CPU 核心」,这是错的。Redis 的「单线程」特指命令执行(命令读写、数据结构操作)是单线程的


为什么单线程还这么快?四个原因:

原因 1:内存操作,纳秒级延迟

1
2
3
内存随机访问延迟:~100ns(纳秒)
磁盘随机访问延迟:~10ms(毫秒)
差距:100,000 倍!

Redis 所有数据都在内存中,操作延迟是纳秒级的,单线程也足够快。


原因 2:I/O 多路复用(非阻塞)

Redis 使用了 epoll(Linux)/ kevent(BSD) 多路复用机制:

1
2
3
4
5
6
7
8
9
10
11
传统阻塞 I/O:
while (有客户端连接):
等待客户端发数据(阻塞!)
处理数据
→ 大部分时间在等网络,CPU 空转

Redis 的多路复用:
把所有客户端 socket 注册到 epoll
epoll_wait() 只返回「有数据到达」的 socket
只处理有数据的 socket
→ 不阻塞,CPU 一直干活

原因 3:单线程避免了锁竞争和上下文切换

多线程的问题:

1
2
3
4
多线程代价:
1. 锁竞争(synchronized / CAS)→ 性能下降
2. 上下文切换(保存/恢复寄存器)→ 每次切换 ~1μs
3. 内存一致性开销(CPU 缓存行失效)

Redis 的单线程模型完全没有这些问题。


原因 4:高效的数据结构

Redis 的每种数据结构都经过极致优化(后面有专题讲解),比如:

  • String 用 SDS(简单动态字符串),O(1) 获取长度。
  • Hash 用渐进式 rehash,避免一次性扩容卡顿。
  • Sorted Set 用跳表 + 哈希表,范围查询 O(log N)。

面试加分回答

「Redis 6.0 引入的多线程 I/O 是一个重要考点。需要注意的是:多线程只用于网络 I/O(读取客户端请求、写回响应),命令执行仍然是单线程的。这样设计的好处是:既利用了多核 CPU 处理网络 I/O 的能力,又避免了多线程命令执行带来的锁竞争和复杂度。配置方式是设置 io-threads 参数(建议设为 CPU 核心数的 1/2 到 1 倍)。」


第 3 题:Redis 的五种基本数据结构是什么?分别适用什么场景?

一句话结论

String(字符串)、Hash(哈希)、List(列表)、Set(集合)、ZSet(有序集合)。选对数据结构,功能实现事半功倍。


深度解析

① String(字符串)

1
底层编码:int(整数)、embstr(短字符串)、raw(长字符串)

最基础的类型, value 最大 512MB。

常用命令:

1
2
3
4
5
SET key value
GET key
INCR key # 原子自增(分布式计数器场景)
SET key value EX 60 # 设置过期时间
SETNX key value # 不存在时才设置(分布式锁场景)

适用场景:

  • 缓存热点数据(如用户信息、配置)
  • 分布式计数器(INCR / DECR
  • 分布式锁(SETNX
  • 位图统计(SETBIT / GETBIT,如用户签到)

② Hash(哈希)

1
底层编码:ziplist(小哈希)/ hashtable(大哈希)

类似 Java 的 HashMap,适合存储对象

常用命令:

1
2
3
4
HSET user:1 name "张三" age 25
HGET user:1 name
HGETALL user:1 # 获取所有字段
HINCRBY user:1 age 1 # 对某个字段做自增

适用场景:

  • 存储对象(比 String 更节省空间,不需要序列化整个对象)
  • 购物车(HSET cart:user_id product_id quantity
  • 用户属性(年龄、昵称、积分等)

③ List(列表)

1
底层编码:quicklist(3.2+,ziplist + linkedlist 的结合)

有序、可重复、双向链表。

常用命令:

1
2
3
4
5
6
LPUSH list a b c    # 左边插入
RPUSH list d e f # 右边插入
LPOP list # 左边弹出
RPOP list # 右边弹出
LRANGE list 0 -1 # 获取所有元素
BLPOP list 0 # 阻塞式左弹出(消息队列场景)

适用场景:

  • 消息队列(LPUSH + BRPOP
  • 时间线(LPUSH + LTRIM 实现固定长度列表)
  • 栈(LPUSH + LPOP)或队列(LPUSH + RPOP

④ Set(集合)

1
底层编码:intset(整数集合)/ hashtable

无序、不可重复。

常用命令:

1
2
3
4
5
6
SADD set a b c
SISMEMBER set a # 判断是否存在(O(1))
SMEMBERS set # 获取所有成员
SINTER set1 set2 # 交集
SUNION set1 set2 # 并集
SDIFF set1 set2 # 差集

适用场景:

  • 去重(如用户已读消息 ID)
  • 共同好友(SINTER user:1:friends user:2:friends
  • 抽奖(SRANDMEMBER 随机取元素)
  • 标签系统(给用户打标签)

⑤ Sorted Set / ZSet(有序集合)

1
底层编码:ziplist(小集合)/ skiplist + hashtable(大集合)

有序、不可重复、每个成员关联一个 score(分数),按 score 排序。

常用命令:

1
2
3
4
5
6
ZADD leaderboard 100 "user1"
ZADD leaderboard 200 "user2"
ZRANGE leaderboard 0 -1 WITHSCORES # 按 score 升序
ZREVRANGE leaderboard 0 -1 WITHSCORES # 按 score 降序(排行榜)
ZRANK leaderboard "user1" # 获取排名
ZINCRBY leaderboard 50 "user1" # 增加分数

适用场景:

  • 排行榜(最经典场景,如游戏积分榜)
  • 延迟队列(score 设为执行时间戳,ZRANGEBYSCORE 取到期任务)
  • 滑动窗口限流(score 设为时间戳,统计时间窗口内的请求数)

面试加分回答

「除了这五种基本类型,Redis 还有三种特殊类型经常被问到:Bitmap(位图,适合海量布尔统计,如用户签到,1 亿用户签到只需 12MB)、HyperLogLog(基数估算,适合 UV 统计,误差 0.81%,只需 12KB 内存)、Geo(地理位置,底层用 ZSet 实现,适合「附近的人」功能)。」


第 4 题:SDS(简单动态字符串)和 C 字符串相比有什么优势?

一句话结论

SDS 解决了 C 字符串的三大缺陷:获取长度 O(1)(C 字符串是 O(n))、二进制安全(C 字符串不能存 \0)、避免缓冲区溢出。


深度解析

C 字符串的问题:

1
2
3
4
5
6
7
8
9
10
11
12
// C 字符串以 \0 结尾
char *str = "hello"; // 实际存储:h e l l o \0

// 问题 1:获取长度需要遍历到 \0,O(n)
strlen(str); // 遍历 5 个字符才返回 5

// 问题 2:不能存二进制数据(中间有 \0 就被截断)
char *bin = "hello\0world"; // 只能读到 "hello"

// 问题 3:strcat 可能导致缓冲区溢出
char buf[5] = "hello";
strcat(buf, "world"); // buf 只有 5 字节,溢出!

SDS 的结构:

1
2
3
4
5
6
struct sdshdr {
int len; // 已使用的字节数(O(1) 获取长度)
int alloc; // 分配的总字节数(不含 header 和 \0)
unsigned char flags; // 低 3 位表示类型
char buf[]; // 柔性数组,实际存储字符串内容
};

SDS 的五大优势:

优势 C 字符串 SDS
获取长度 O(n),需遍历 O(1),直接读 len 字段
二进制安全 ❌(遇 \0 截断) ✅(用 len 判断结束,不管 \0
缓冲区溢出 可能(strcat 不检查长度) 不会(先检查 alloc,不够就扩容)
修改时内存分配 每次修改都可能 realloc 空间预分配 + 惰性空间释放(见下文)
API 安全性 不安全 安全(所有 API 都做边界检查)

空间预分配(减少内存分配次数):

1
2
3
扩容规则:
修改后 len < 1MB → alloc = len * 2(翻倍)
修改后 len >= 1MB → alloc = len + 1MB(每次最多多分配 1MB)

例:第一次 SET key "a"(len=1),SDS 实际分配 2 字节;
追加到 len=13 时,SDS 实际分配 26 字节;
这样连续追加 N 次,内存分配次数从 N 次降到 O(log N) 次。


惰性空间释放(避免缩短时的内存分配):

1
2
3
4
SDS 缩短时(如 TRIM),不立即释放多余内存,
而是更新 alloc 字段,留着下次用。

需要真正释放时,调用 sdsclear() 或等待内存不够时自动回收。

面试加分回答

「SDS 的设计体现了 Redis 对性能的极致追求。另外,Redis 5.0 对 SDS 做了进一步优化:根据字符串长度动态选择 sdshdr5 / sdshdr8 / sdshdr16 / sdshdr32 / sdshdr64,用最小的 header 存储长度信息,进一步节省内存。这也是为什么 Redis 在小数据场景下内存利用率非常高的原因。」


第 5 题:Redis 的 Hash 数据结构是如何实现的?渐进式 rehash 是什么?

一句话结论

Redis 的 Hash 底层用「哈希表(dictht)」实现,通过渐进式 rehash 把扩容/缩容的代价分摊到每次增删改查操作中,避免一次性 rehash 导致长时间阻塞。


深度解析

Hash 的底层编码:

Redis 的 Hash 类型有两种底层编码:

1
2
3
4
5
6
7
8
9
编码 1:ziplist(压缩列表)
- 条件:hash-max-ziplist-entries < 512 且 所有 value < 64 字节
- 优点:内存紧凑,节省空间
- 缺点:查找 O(n)

编码 2:hashtable(哈希表,即 dictht)
- 条件:不满足 ziplist 条件时自动转换
- 优点:查找 O(1)
- 缺点:内存开销稍大

dictht(字典哈希表)的结构:

1
2
3
4
5
6
typedef struct dictht {
dictEntry **table; // 哈希表数组(bucket 数组)
unsigned long size; // 桶的数量(永远是 2 的幂)
unsigned long sizemask; // size - 1,用于计算索引(hash & sizemask)
unsigned long used; // 已有节点数
} dictht;

哈希冲突的解决:链地址法

1
2
3
4
5
6
索引计算:
hash = dictHashKey(key);
index = hash & sizemask; // 等价于 hash % size,但位运算更快

冲突时:
table[index] → entry1 → entry2 → entry3(链表)

渐进式 rehash(重点!):

普通哈希表扩容时,需要把旧表的所有元素重新计算哈希值,搬到新表:

1
2
3
4
5
6
普通 rehash(一次性):
1. 新建一个更大的哈希表
2. 遍历旧表的每个桶、每个链表节点
3. 重新计算哈希值,插入新表
4. 释放旧表
→ 如果哈希表有 100 万元素,这一步会阻塞几秒!

Redis 的渐进式 rehash 把这个工作分摊了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
渐进式 rehash 流程:

1. 准备两个哈希表:ht[0](正在使用)、ht[1](新表,初始为空)
2. 设置 rehashidx = 0(表示开始 rehash,从索引 0 开始)

3. 每次对哈希表增/删/改/查时:
- 顺带把 ht[0] 中 rehashidx 索引对应的整个桶搬到 ht[1]
- rehashidx++

4. 定时任务(serverCron)也会分批搬迁(每次搬 100 个桶)

5. 当 ht[0] 所有元素都搬到 ht[1]
- 释放 ht[0]
- ht[1] 设置为 ht[0]
- rehashidx = -1(表示 rehash 结束)

关键:在 rehash 期间,增删改查如何工作?

1
2
3
4
5
6
7
8
查询(GET):
先查 ht[0],没找到再查 ht[1]

插入(SET):
只插入 ht[1](保证 ht[0] 只减不增,rehash 最终能完成)

删除/更新:
同时操作 ht[0] 和 ht[1]

面试加分回答

「渐进式 rehash 是 Redis 单线程模型下保证低延迟的关键设计之一。值得注意的是,在 rehash 期间,如果哈希表很大,即使分摊了,单次搬迁(搬一个桶的整个链表)也可能有延迟尖刺。Redis 7.0 对 rehash 做了进一步优化,每次搬迁的桶数量动态调整,避免大桶导致的延迟。」


第 6 题:跳表(Skip List)的原理是什么?为什么 ZSet 用跳表而不是红黑树?

一句话结论

跳表是在有序链表上建立「多层索引」的数据结构,查找/插入/删除都是 O(log N);ZSet 用跳表而不是红黑树,因为跳表实现更简单、范围查询更高效、支持无锁并发(理论上)。


深度解析

有序链表的困境:

1
2
3
普通有序链表:
head → 1 5 8 12 19 25 30 → NULL
查找 25:需要遍历 6 次 → O(n)

跳表的核心思想:多层索引

1
2
3
4
5
跳表(理想结构):
3 层(稀疏索引):head →-----------→ 25 →----------------→ NULL
2 层(较密索引):head →-----→ 12 →-----→ 25 →-----→ 30 → NULL
1 层(更密索引):head → 5 8 12 19 25 28 30 → NULL
0 层(原始链表):head → 1 5 8 →...(完整有序链表)

查找过程演示(查找 25):

1
2
3
从第 3 层开始:
head → 25?25 >= 25,往右走 → 到 25,找到了!
平均只需 O(log N) 次比较

插入过程(带随机层数):

1
2
3
1. 查找插入位置(沿高层索引往下找)
2. 随机生成该节点的层数(抛硬币:50% 概率升一层,最多 32 层)
3. 在新节点的各层插入(类似链表插入)

随机层数的设计保证了:第 k 层的节点数大约是第 k-1 层的 1/2 → 跳表高度是 O(log N)。


为什么 ZSet 用跳表而不是红黑树?

对比维度 红黑树 跳表
实现复杂度 很高(旋转、变色) 简单(就是多层链表)
范围查询 O(log N + M),但需要中序遍历 O(log N + M),沿着第 0 层链表扫就行,更简单
内存占用 每个节点存颜色位 每个节点存多层指针(稍多)
并发支持 很难无锁化 理论上可以无锁化(CAS)
调试难度 难(黑红交替规则复杂) 易(就是链表)

Redis 作者 antirez 的原话:

「跳表的实现比红黑树简单得多,而且范围查询的性能更好,对于 Redis 的使用场景来说,跳表是更好的选择。」


面试加分回答

「跳表在工程中其实比红黑树更常用,除了 Redis 的 ZSet,还有 LevelDB / RocksDB 的 MemTable 也用跳表。另外,Redis 的 ZSet 实际上是跳表 + 哈希表的组合:跳表按 score 有序存储,哈希表存储 member→score 的映射(O(1) 判断 member 是否存在、获取 score)。这样 ZADD、ZSCORE 是 O(1),ZRANGE 是 O(log N + M)。」


第 7 题:Redis 的持久化机制——RDB 和 AOF 分别是什么?

一句话结论

RDB = 定时快照(全量备份,恢复快,但会丢数据);AOF = 追加日志(每条写命令都记录,安全,但文件大、恢复慢)。生产环境推荐同时开启两者(Redis 4.0+ 支持混合持久化)。


深度解析

RDB(Redis DataBase)快照:

1
2
3
4
原理:
fork() 一个子进程
子进程把当前内存数据快照写入 .rdb 文件
父进程继续处理客户端请求(采用 COW 机制,子进程看到的是 fork 时的内存快照)

触发方式:

1
2
3
4
5
6
7
8
# 手动触发
SAVE # 阻塞式,主进程亲自写快照(生产环境禁止使用!)
BGSAVE # 后台式,fork 子进程写快照(不阻塞)

# 自动触发(在 redis.conf 中配置)
save 900 1 # 900 秒内至少 1 个 key 变化 → 触发 BGSAVE
save 300 10 # 300 秒内至少 10 个 key 变化 → 触发 BGSAVE
save 60 10000 # 60 秒内至少 10000 个 key 变化 → 触发 BGSAVE

优点:

  • 恢复速度快(直接把 RDB 文件加载到内存,O(N),N 是数据量)
  • 文件紧凑(二进制压缩,适合做冷备份、灾难恢复)
  • 对性能影响小(fork 子进程做,父进程不阻塞)

缺点:

  • 会丢数据:两次快照之间的数据会丢失(如配了 save 900 1,宕机最多丢 15 分钟数据)

AOF(Append Only File)日志:

1
2
3
原理:
每条写命令(SET/HSET/...)都追加到 AOF 缓冲区
根据配置策略刷盘(write + fsync

刷盘策略(appendfsync 参数):

策略 说明 性能 安全性
always 每条命令都 fsync 到磁盘 最差(每次写都要等磁盘) 最好(最多丢 1 条命令)
everysec(默认,推荐) 每秒 fsync 一次 平衡 最多丢 1 秒数据
no 由操作系统决定何时刷盘 最好 最差(宕机可能丢大量数据)

AOF 文件重写(Rewrite):

AOF 文件会越来越大(如对一个 key 做了 100 次 INCR,AOF 中有 100 条命令)。

重写机制:fork 子进程,根据当前内存数据,生成一份最小化的 AOF 文件(如对上述 key 直接生成 SET key 100)。

1
2
# 手动触发重写
BGREWRITEAOF

同时开启 RDB 和 AOF 时的启动恢复流程:

1
2
3
Redis 重启时:
如果 AOF 开启 → 优先用 AOF 恢复(数据更完整)
如果 AOF 关闭 → 用 RDB 恢复

面试加分回答

「Redis 4.0 引入了混合持久化(aof-use-rdb-preamble),AOF 重写时,先把当前内存数据以 RDB 格式写入 AOF 文件,再把重写期间的增量命令以 AOF 格式追加。这样重启时,先加载 RDB 部分(快),再重放 AOF 增量部分(少),兼顾了 RDB 的加载速度和 AOF 的数据安全性,生产环境强烈推荐开启。」


第 8 题:Redis 的内存淘汰策略有哪些?

一句话结论

内存达到 maxmemory 上限时,Redis 会根据配置的策略来淘汰键。生产环境最常用的是 allkeys-lru(所有键参与 LRU 淘汰)或 volatile-lru(只淘汰设置了过期时间的键)。


深度解析

8 种淘汰策略:

策略 含义 适用场景
noeviction(默认) 不淘汰,写满时返回错误 不能丢数据的场景
allkeys-lru 所有键中,淘汰最久未使用 生产环境最常用
volatile-lru 只淘汰设置了过期时间的键中最久未使用的 兼顾缓存和持久化键
allkeys-random 所有键中随机淘汰 访问分布均匀时可用
volatile-random 只淘汰设置了过期时间的键中随机的 -
volatile-ttl 只淘汰设置了过期时间的键中,TTL 最小(最快过期)的 -
allkeys-lfu(4.0+) 所有键中,淘汰访问频率最低 有「热点数据 + 冷数据」的场景
volatile-lfu(4.0+) 只淘汰设置了过期时间的键中访问频率最低的 -

LRU 和 LFU 的区别:

1
2
3
4
5
6
7
8
9
LRU(Least Recently Used,最近最少使用):
记录「上次访问时间」,淘汰最久没被访问的
问题:如果某个 key 在 1 小时前被访问了一次,之后一直没访问
→ LRU 认为它是「最近使用的」,不会淘汰它(但实际上它已经是冷数据了)

LFU(Least Frequently Used,最不经常使用):
记录「访问频率」,淘汰访问次数最少的
用计数器 + 衰减算法实现(频率会随时间衰减)
→ 更适合有「热点数据」的场景

Redis 的 LRU 是近似 LRU(抽样实现):

1
2
3
4
5
6
7
8
精准 LRU 需要维护一个链表(访问时移到表头,淘汰时删表尾)
→ 需要额外内存,且每次访问都要修改链表(O(1) 但有锁竞争问题)

Redis 的近似 LRU
每个 key 记录一个 24 位的时钟(访问时更新)
淘汰时,随机采样 Nkey(默认 5 个,可配置)
淘汰这 N 个里面最久未访问的
→ 内存占用小,性能高,近似效果很好

面试加分回答

「Redis 的 LRU 淘汰策略实际上是近似 LRU,通过随机采样来近似,而不是维护一个严格的 LRU 链表。这样做的好处是节省内存、避免性能开销。另外,maxmemory-samples 参数控制每次采样的数量(默认 5,越大越接近真实 LRU,但性能稍差)。在 Redis 7.0 中,LFU 的实现进一步优化了频率衰减算法,使得热点数据的识别更准确。」



二、持久化与内存管理(9-13)


第 9 题:AOF 重写机制的原理是什么?

一句话结论

AOF 重写 = fork 子进程,根据当前内存数据生成最小化的 AOF 文件,再追加重写期间的增量命令。重写期间不阻塞主进程。


深度解析

AOF 文件膨胀的问题:

1
2
3
4
5
6
7
8
9
# 假设对同一个 key 执行了 100 次操作:
SET counter 1
INCR counter # 2
INCR counter # 3
...
INCR counter # 100

# AOF 文件中会有 100 条命令!
# 但实际上,只需要一条:SET counter 100

AOF 文件会越来越臃肿,不仅占用磁盘,重启恢复时也要重放大量命令,非常慢。


AOF 重写的原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AOF 重写流程(BGREWRITEAOF):

1. fork() 子进程(和 RDB 一样的 COW 机制)

2. 子进程:
- 遍历当前内存中的所有数据
- 为每个 key 生成「最小化」的 AOF 命令
(如上述例子 → 只生成 SET counter 100
- 写入一个新的临时 AOF 文件

3. 重写期间,主进程继续处理请求:
- 所有新命令除了追加到原 AOF 缓冲区
- 还追加到「AOF 重写缓冲区」

4. 子进程完成重写后,通知主进程

5. 主进程:
- 把「AOF 重写缓冲区」中的增量命令追加到新 AOF 文件
- 原子替换旧 AOF 文件(rename

为什么 fork 子进程而不是主进程自己做?

主进程自己做会阻塞(重写是大 IO 操作)。
fork 子进程后,子进程拥有 fork 时刻的内存快照(COW),
主进程继续处理请求,完全不阻塞。


COW(Copy-On-Write)的巧妙之处:

1
2
3
4
5
6
7
8
9
fork 之后:
子进程和父进程共享同一份物理内存页

当父进程修改某个内存页时:
→ 内核把该页复制一份(COW)
→ 父进程修改新的页
→ 子进程看到的仍然是旧页(快照)

所以:子进程看到的数据是 fork 时刻的快照,完全一致!

面试加分回答

「AOF 重写和 RDB 都用了 fork + COW 机制,这是 Redis 能在单线程下做后台重 IO 操作的核心技巧。但要注意:fork 瞬间会有停顿(需要拷贝页表),如果内存很大(如 10GB+),fork 的停顿可能达到几十毫秒。缓解方式:调小 vm.overcommit_memory = 1(允许超额分配内存),或使用 Redis 7.0 的 aof-timestamp-enabled 来监控 fork 耗时。」


第 10 题:RDB 和 AOF 的优缺点对比?混合持久化是什么?

一句话结论

RDB = 快照,恢复快但丢数据;AOF = 日志,安全但文件大恢复慢。混合持久化(Redis 4.0+)= RDB 快照 + AOF 增量,兼顾两者优点。


深度解析

RDB vs AOF 全面对比:

对比维度 RDB AOF
文件大小 小(二进制压缩) 大(文本命令)
恢复速度 快(直接加载到内存) 慢(需要逐条重放命令)
数据安全性 可能丢数据(两次快照之间的数据) everysec 最多丢 1 秒数据
对性能影响 小(fork 子进程,父进程不阻塞) 中等(everysec 模式下后台 fsync)
灾难恢复 方便(一个文件,适合冷备) 不方便(文件大,且可能损坏)
启动优先级 AOF 开启时优先用 AOF 恢复 高(数据更完整)

混合持久化(Redis 4.0+,强烈推荐):

1
2
3
4
5
6
7
8
9
10
混合持久化 = RDB 快照(全量) + AOF 增量命令

AOF 重写时:
1. 子进程先写一个 RDB 格式的全量快照到新 AOF 文件
2. 再把重写期间的增量命令以 AOF 格式追加到文件末尾

重启恢复时:
1. 先加载 RDB 部分(快!)
2. 再重放 AOF 增量部分(少!)
→ 兼顾了 RDB 的加载速度和 AOF 的数据安全性

开启方式:

1
2
# redis.conf
aof-use-rdb-preamble yes # 开启混合持久化(默认 yes)

面试加分回答

「混合持久化是 Redis 持久化的『终极方案』。在生产环境中,建议同时开启 RDB(用于冷备)和 AOF(用于恢复),并开启混合持久化。另外,Redis 的持久化文件(RDB/AOF)建议定期备份到对象存储(如 S3/OSS),防止整机故障导致数据无法恢复。」


第 11 题:Redis 的过期键删除策略是什么?

一句话结论

Redis 使用「惰性删除 + 定期删除」的组合策略。惰性删除 = 访问时才检查是否过期;定期删除 = 后台定时抽样检查并删除过期键。


深度解析

如果只用心惰性删除:

1
2
3
4
惰性删除:
只有在访问某个 key 时才检查是否过期
→ 如果过期 key 永远不被访问,内存永远不释放!
→ 内存泄漏!

如果只用定期删除:

1
2
3
定期删除:
定时扫描所有 key,删除过期的
→ 如果 key 很多,扫描耗时 → 阻塞主进程!

Redis 的组合策略(扬长避短):


策略 ①:惰性删除(Lazy Expire)

1
2
3
4
5
6
7
8
// Redis 访问 key 时的逻辑(伪代码)
int expireIfNeeded(redisDb *db, robj *key) {
if (!keyIsExpired(db, key)) return 0; // 没过期,直接返回

// 过期了,删除它
deleteExpiredKeyAndPropagate(db, key);
return 1;
}

访问时顺带检查,过期就删。
优点:CPU 友好(不主动扫描)。
缺点:过期 key 如果不被访问,永远占用内存。


策略 ②:定期删除(Active Expire)

1
2
3
4
5
6
定期删除(serverCron 定时执行):

1. 每次随机抽取 20 个设置了过期时间的 key
2. 删除其中已经过期的 key
3. 如果过期 key 比例 > 25%,重复步骤 1(继续抽样)
4. 每次执行时间有上限(避免长时间阻塞)

优点:能清理掉「永远不被访问」的过期 key。
缺点:是抽样删除,不是全量扫描,可能有残留。


Redis 6.0 之后的改进:惰性删除后台线程

1
2
# redis.conf
lazyfree-lazy-expire yes # 过期 key 的删除放到后台线程执行

删除大 key(如一个 100 万元的 Set)时会阻塞主线程,
开启惰性删除后,把 DEL 操作放到后台线程执行,不阻塞主进程。


面试加分回答

「Redis 4.0 引入了 lazyfree-lazy-expire,把过期 key 的删除操作放到后台线程,解决了大 key 删除阻塞的问题。另外,Redis 的内存淘汰(maxmemory-policy)和过期删除是两套独立机制:过期删除是针对带 TTL 的 key;内存淘汰是针对所有 key(当内存达到上限时)。面试时经常有人混淆这两个概念。」


第 12 题:缓存雪崩、缓存穿透、缓存击穿分别是什么?如何解决?

一句话结论

缓存雪崩 = 大量 key 同时失效;缓存穿透 = 查询不存在的 key;缓存击穿 = 热点 key 失效瞬间大量请求打到 DB。三者都有成熟的解决方案。


深度解析


① 缓存雪崩(Cache Avalanche)

问题描述:

1
2
3
4
假设有 10000key,设置的过期时间完全相同。
→ 到期时,10000key 同时失效!
→ 所有请求全部打到数据库!
→ 数据库瞬间被打垮(雪崩)

解决方案:

1
2
3
4
5
6
7
8
9
10
方案 1:过期时间加随机偏移
SET key value EX (300 + random(0, 60)) # 300~360 秒随机

方案 2:多级缓存(本地缓存 + Redis)
→ 即使 Redis 全部失效,本地缓存还能扛住一部分

方案 3:永不过期 + 后台更新
→ key 不设过期时间
→ 后台任务定期更新缓存
→ 适用于数据变更不频繁的场景

② 缓存穿透(Cache Penetration)

问题描述:

1
2
3
4
5
6
7
攻击者故意查询「数据库里也不存在」的数据:
GET /user?id=-1 → Redis 没有 → 查 DB → DB 也没有 → 返回空
GET /user?id=-2 → Redis 没有 → 查 DB → DB 也没有 → 返回空
...(大量恶意请求)

→ 每次都打到数据库!
→ 数据库被拖垮

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
方案 1:缓存空值(不推荐,会占用大量内存)
if (db_result == null) {
SET key "" EX 60 # 缓存空字符串,短期过期
}

方案 2:布隆过滤器(Bloom Filter)★ 推荐
→ 在缓存层之前加一个布隆过滤器
→ 布隆过滤器说「不存在」,就一定不存在,直接返回
→ 布隆过滤器说「可能存在」,才去查缓存/DB

实现:Redis 的 Bitmap 可以实现布隆过滤器
或使用 RedisBloom 模块(官方模块)

方案 3:限流 + 参数校验
→ 对恶意参数(如 id < 0)直接拒绝

③ 缓存击穿(Cache Hotkey Breakdown)

问题描述:

1
2
3
4
5
某个超级热点 key(如首页推荐商品):
→ 每秒 10000 个请求访问这个 key
key 突然过期了!
→ 这 10000 个请求全部打到数据库!
→ 数据库瞬间被打垮

和「雪崩」的区别:雪崩是大量 key 同时失效;击穿是单个热点 key 失效

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
方案 1:互斥锁(Mutex Lock)★ 推荐
第一个请求发现缓存失效:
→ 获取分布式锁
→ 查数据库,写缓存
→ 释放锁
后续请求:
→ 等待一小会,再读缓存(这时已经被第一个请求填好了)

伪代码:
value = redis.get(key)
if (value == null) {
if (redis.setnx(lockKey, "1", "EX", 10)) { // 获取锁
value = db.query(sql)
redis.set(key, value, "EX", 300)
redis.del(lockKey)
} else {
Thread.sleep(50) // 等一会儿
value = redis.get(key) // 再读一次
}
}

方案 2:逻辑过期(不设物理过期时间)
key 不设 EXPIRE
→ 在 value 中存一个逻辑过期时间
→ 发现逻辑过期时,获取锁,后台线程更新缓存
→ 当前请求返回旧值(保证可用性)

面试加分回答

「缓存穿透的解决方案,布隆过滤器虽然好,但有误判率(说存在时可能不存在)。在生产环境中,通常会布隆过滤器 + 缓存空值组合使用:布隆过滤器挡掉绝大多数不存在的请求,极少数误判的请求走缓存空值。另外,Redis 6.0 之后有 RedisBloom 官方模块,可以直接用,不需要自己实现布隆过滤器。」


第 13 题:缓存与数据库的一致性如何保证?

一句话结论

缓存与数据库一致性没有「完美方案」,只有「适合业务的方案」。最常用的是:先更新数据库,再删除缓存(Cache Aside Pattern)。


深度解析

先搞清楚:为什么会有不一致?

1
2
3
4
5
6
场景:更新数据时
方案 A:先更新缓存,再更新数据库
问题:更新缓存成功,更新数据库失败 → 缓存和 DB 不一致

方案 B:先更新数据库,再更新缓存
问题:并发场景下,两个线程同时更新,缓存可能是旧值

Cache Aside Pattern(推荐方案):

1
2
3
4
5
6
7
8
写入时:
1. 先更新数据库
2. 再删除缓存(不是更新缓存!)

读取时:
1. 先读缓存,有就直接返回
2. 缓存没有,读数据库
3. 把数据库的结果写入缓存

为什么是「删除缓存」而不是「更新缓存」?

1
2
3
4
5
6
7
8
9
10
11
12
假设:两个并发写请求

方案(更新缓存):
T1: 写数据库,value = 100
T2: 写数据库,value = 200
T2: 更新缓存,value = 200
T1: 更新缓存,value = 100 ← 缓存是 100,DB 是 200,不一致!

方案(删除缓存):
T1: 写数据库,value = 100,删除缓存
T2: 写数据库,value = 200,删除缓存
→ 下一个读请求会从 DB 读最新值(200)并写入缓存 → 一致!

并发场景下的问题:

1
2
3
4
5
场景:读请求和写请求并发

T1(读): 读缓存,没有 → 准备读 DB
T2(写): 写 DB(value=200),删除缓存
T1(读): 读 DB(读到旧值 value=100),写入缓存 ← 缓存是旧值!

发生概率:

上述场景要求:T1 读 DB 的耗时 > T2 写 DB + 删除缓存的耗时。
通常读 DB 比写 DB 慢(因为写可能只是更新内存中的数据,读要读磁盘),
所以这个场景发生概率很低

如果一定要解决:

1
2
3
方案:引入消息队列,异步补偿
T1 写入缓存失败后,发一条消息到 MQ
消费者收到消息后,再次删除缓存(保证最终一致)

面试加分回答

「缓存与 DB 一致性是一个经典难题,CAP 定理告诉我们不可能同时满足 CA。在工程实践中,最终一致性是可以接受的。另外,阿里开源的 Canal 可以监听 MySQL 的 binlog,当数据库变更时自动更新缓存,这是一种「先写 DB,异步刷新缓存」的模式,一致性更好,适合对一致性要求高的场景。」


三、高可用与分布式(14-21)


第 14 题:Redis 分布式锁的实现原理?有什么问题?

一句话结论

Redis 分布式锁用 SET key value NX EX timeout 实现;核心要解决:锁超时释放、误删别人的锁、锁的续期这三个问题。


深度解析

最基础的分布式锁:

1
2
3
4
5
# 获取锁
SET lock:order:12345 "client_id" NX EX 30

# 释放锁
DEL lock:order:12345

NX = 不存在时才设置(只有一个客户端能设置成功)
EX 30 = 30 秒后自动过期(防止死锁)


问题 ①:锁超时,业务还没执行完

1
2
3
4
5
6
场景:
T1 获取锁,设置超时 30
T1 的业务执行了 40 秒(如网络慢、GC 停顿)
30 秒后,锁自动过期了
T2 获取到了同一把锁
T1T2 同时执行业务 → 并发问题!

解决:给锁续期(看门狗机制)

1
2
3
4
5
方案:获取锁后,启动一个后台线程
每隔 (timeout / 3) 秒,检查锁是否还持有
如果还持有,延长锁的过期时间

Redisson(Java Redis 客户端)内置了这个机制,叫「看门狗(Watch Dog)」

问题 ②:误删别人的锁

1
2
3
4
5
场景:
T1 获取锁,设置超时 30
T1 业务执行慢,30 秒后锁自动过期
T2 获取到了锁
T1 业务执行完,执行 DEL lock → 把 T2 的锁删掉了!

解决:删除锁时验证 value(原子操作)

1
2
3
4
5
6
7
8
9
10
11
12
# 错误的释放方式(会误删):
DEL lock:key

# 正确的释放方式(Lua 脚本,原子操作):
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end

# 执行:
EVAL script 1 lock:key "client_id_123"

每个客户端用唯一的 value(如 UUID)标识自己,
删除时先检查 value 是否是自己的,是才删除。


问题 ③:Redis 节点宕机,锁丢了

1
2
3
4
5
6
场景(单节点 Redis):
T1 在 Master 上获取了锁
Master 宕机,还没把锁信息同步到 Slave
Slave 晋升为新的 Master
T2 在新的 Master 上也能获取同一把锁!
→ 两个客户端同时持有锁 → 并发问题!

解决:RedLock 算法(见下一题)


面试加分回答

「Redis 分布式锁在生产环境中不要自己实现,用成熟的库:Java 用 Redisson,Python 用 redis-py 的锁实现。这些库已经处理了锁续期、误删、可重入等复杂问题。另外,对于绝对不能出错的场景(如金融扣款),Redis 分布式锁不是最佳选择,应该用 ZooKeeperetcd 的分布式锁(基于临时顺序节点,可靠性更高)。」


第 15 题:RedLock 算法的原理?有什么缺陷?

一句话结论

RedLock = 在 N 个独立 Redis 节点上依次获取锁,超过半数成功才认为获取锁成功。但 Martin Kleppmann 指出它有问题,建议用 fenced token 或直接用 ZooKeeper/etcd。


深度解析

RedLock 算法流程:

1
2
3
4
5
6
7
8
9
10
假设有 5 个独立的 Redis 节点(无主从关系):

1. 客户端获取当前时间戳 T1
2. 依次向 5 个节点发送 SET key value NX PX timeout 命令
(使用相同的 key 和随机 value
3. 只有「多数节点(≥3 个)」都获取成功,且总耗时 < timeout
→ 才认为获取锁成功
4. 获取成功后的锁有效时间 = timeout - 获取锁的总耗时
5. 如果获取失败(没有多数成功,或超时),
向所有节点发送释放锁的命令(Lua 脚本)

RedLock 的优点:

即使有节点宕机,只要多数节点存活,锁服务就可用。
比单节点 Redis 锁可靠性高。


RedLock 的缺陷(Martin Kleppmann 批评):

缺陷 ①:系统时钟漂移问题

1
2
3
4
5
6
场景:
客户端 C1 在节点 ABC 上获取了锁,有效时间 10
节点 C 的系统时钟发生了跳跃(如 NTP 同步导致时间突然前进)
→ 节点 C 上的锁「提前」过期了
客户端 C2 可以在节点 CDE 上获取同一把锁
→ 两个客户端同时持有锁!

缺陷 ②:GC 停顿导致锁过期

1
2
3
4
5
6
场景:
C1 获取锁成功,设置了 10 秒超时
C1 发生了长时间 GC 停顿(如 15 秒)
→ GC 期间,锁自动过期了
C2 获取到了锁
→ GC 结束后,C1C2 同时执行业务!

RedLock 作者的反驳(antirez):

缺陷 ①:系统时钟跳跃是运维问题,应该避免(用合理的 NTP 配置)。
缺陷 ②:GC 停顿是客户端的问题,应该在客户端用「锁续期」解决。


更可靠的替代方案:

1
2
3
4
5
6
7
8
9
10
方案 1:用 ZooKeeper 的临时顺序节点实现分布式锁
→ 由 ZK 保证一致性(基于 ZAB 协议)
→ 不需要客户端续期,会话失效时 ZK 自动删除临时节点

方案 2:用 etcd 的分布式锁(基于 Raft 协议)
→ 一致性有保证

方案 3:用「乐观锁 + 版本号」(适合数据库场景)
UPDATE inventory SET count = count - 1, version = version + 1
WHERE id = 1 AND version = old_version;

面试加分回答

「RedLock 的争议是分布式系统领域的一个经典案例。结论是:如果能接受偶发的锁失效(如秒杀场景),RedLock 可用;如果绝对不能接受(如金融扣款),应该用 ZooKeeper/etcd,或者在业务层用fenced token(栅栏令牌)**机制:每次写存储时带上一个单调递增的 token,存储层拒绝 token 小于当前最大 token 的写请求,这样即使锁失效,旧客户端也无法成功写入。」


第 16 题:Redis 事务的实现原理?WATCH 命令是什么?

一句话结论

Redis 事务 = MULTI(开启) + 命令入队 + EXEC(提交)。WATCH 命令提供乐观锁,在 EXEC 前监控 key,如果被修改则事务失败。但 Redis 事务不支持回滚。


深度解析

Redis 事务的基本用法:

1
2
3
4
5
MULTI        # 开启事务
SET key1 value1
SET key2 value2
INCR counter
EXEC # 提交事务(按顺序执行所有命令)

事务中的命令不会被其他客户端的命令打断(序列化执行)。


WATCH 命令(乐观锁):

1
2
3
4
5
6
7
8
9
WATCH balance     # 监控 balance 键

MULTI
DECRBY balance 100
INCRBY alice_balance 100
EXEC
# 如果在 EXEC 之前,balance 被其他客户端修改了
# → EXEC 返回 nil(事务失败)
# → 客户端需要重试整个事务

WATCH 的工作原理:在 EXEC 时,检查被 WATCH 的 key 的 version(修改次数),
如果 version 变了(被其他客户端修改了),拒绝执行事务。


Redis 事务不支持回滚!

1
2
3
4
5
6
7
8
Redis 事务执行过程中,某条命令失败了:
→ 后续命令会继续执行(不会回滚!)

原因(antirez 的设计哲学):
1. Redis 命令只在一种情况下失败:语法错误(编译时错误)
→ 这种错误应该在开发时发现,不应该出现在生产环境
2. 支持回滚需要额外的内存开销和复杂度
3. Redis 是 KV 存储,不是关系型数据库,不需要严格的 ACID

Redis 事务 vs 关系型数据库事务:

对比项 Redis 事务 MySQL 事务
原子性 ✅ 命令要么都执行,要么都不执行(但不支持回滚) ✅ 支持回滚(undo log)
一致性 由应用层保证 ✅ 由数据库保证
隔离性 ✅ 事务中的命令不会被其他客户端打断 ✅ 支持多种隔离级别
持久性 取决于持久化配置 ✅ 由 WAL 机制保证

面试加分回答

「Redis 的事务能力比较弱,如果需要复杂的事务逻辑(如多条命令的原子执行 + 回滚),应该用 Lua 脚本EVAL script numkeys key [key ...] [arg ...] 可以执行一段 Lua 脚本,Redis 保证整个 Lua 脚本的原子执行(执行期间不会执行其他客户端的命令)。这是实现「先读后写」原子操作的最佳方式。」


第 17 题:Redis Pipeline 的原理?和 MGET/MSET 的区别?

一句话结论

Pipeline = 客户端批量发送命令,减少 RTT(往返时延);MGET/MSET = 服务端批量处理命令。Pipeline 是客户端优化,MGET 是服务端优化。


深度解析

没有 Pipeline 的世界:

1
2
3
4
5
6
7
8
9
# 普通模式:每条命令都要等上一条的响应
Client: SET key1 value1
Server: OK
Client: SET key2 value2
Server: OK
Client: SET key3 value3
Server: OK
→ 3 条命令,3 次 RTT(网络往返时延)
→ 如果 RTT = 1ms,10000 条命令需要 10 秒!

Pipeline:

1
2
3
4
5
6
7
8
9
10
# Pipeline 模式:一次性发送所有命令
Client: SET key1 value1
Client: SET key2 value2
Client: SET key3 value3
...(一次性发给 Server)
Server: OK
Server: OK
Server: OK
...(一次性返回所有结果)
→ 3 条命令,1 次 RTT!

注意:Pipeline 不是原子操作!命令是一条条执行的,只是网络传输合并了。


MGET / MSET:

1
2
3
4
5
6
7
# MGET:一次获取多个 key(服务端批量处理)
MGET key1 key2 key3
→ 返回 [value1, value2, value3]

# MSET:一次设置多个 key
MSET key1 value1 key2 value2 key3 value3
→ 原子操作!要么都成功,要么都不执行

Pipeline vs MGET/MSET:

对比项 Pipeline MGET/MSET
实现层 客户端(批量发送) 服务端(批量处理)
原子性 ❌ 不保证 ✅ MSET 保证原子性
适用场景 任意多条命令批量执行 批量 GET/SET 同名命令
性能 减少 RTT 减少 RTT + 服务端优化

面试加分回答

「Pipeline 和 Lua 脚本的区别是一个高频追问。Pipeline 只是减少网络 RTT,命令在服务端仍然是逐条执行的;Lua 脚本是整个脚本原子执行(服务端保证不被打断)。所以如果需要在「先读后写」的场景下保证原子性,应该用 Lua 脚本而不是 Pipeline。」


第 18 题:Redis 发布订阅(Pub/Sub)的原理?有什么缺陷?

一句话结论

Pub/Sub = 发布者发消息到频道,订阅者接收消息。但不保证消息可靠性(订阅者下线会丢消息),生产环境建议用 Stream 或专业 MQ(Kafka/RocketMQ)。


深度解析

基本用法:

1
2
3
4
5
6
# 订阅者(Subscriber)
SUBSCRIBE news.sports # 订阅「体育新闻」频道

# 发布者(Publisher)
PUBLISH news.sports "湖人队今天赢了!"
→ 所有订阅了 news.sports 的客户端都会收到这条消息

Pub/Sub 的缺陷:

1
2
3
4
5
6
7
8
9
10
缺陷 ①:消息不持久化
→ 订阅者下线期间,消息全部丢失
→ 重新上线后,无法收到历史消息

缺陷 ②:没有 ACK 机制
→ 消息发出去就不管了
→ 订阅者挂了,消息就丢了

缺陷 ③:无法保证消息顺序
→ 多个发布者同时发布,订阅者收到的顺序不确定

Redis Stream(Redis 5.0+,推荐替代 Pub/Sub):

1
2
3
4
5
6
7
8
9
# 生产者:向 stream 添加消息
XADD mystream * key1 value1 key2 value2

# 消费者:读取消息(不会丢,有 ACK 机制)
XREAD COUNT 1 STREAMS mystream $

# 消费者组(类似 Kafka 的 Consumer Group):
XGROUP CREATE mystream mygroup $ MKSTREAM
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >

Stream 支持:消息持久化、消费者组、消息 ACK、Pending 消息重试
是 Redis 5.0 之后做消息队列的首选


面试加分回答

「Redis 的 Pub/Sub 适合「即时消息推送」(如聊天室、实时通知),但不适合对可靠性要求高的场景。如果需要消息队列,Redis 有三种方案:① List(LPUSH + BRPOP,简单但不支持 ACK);② Pub/Sub(即时但不可靠);③ Stream(5.0+,功能最完整,类似简化版 Kafka)。生产环境中,如果消息量很大或对可靠性要求很高,建议直接用 KafkaRocketMQ,不要用 Redis 做消息队列。」


第 19 题:Redis 主从复制的原理?

一句话结论

主从复制 = 从节点连接主节点,全量同步(RDB 快照)+ 增量同步(命令传播)。2.8+ 使用 PSYNC 命令,支持断线后部分重同步。


深度解析

完整主从复制流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
阶段 1:建立连接
从节点(Slave)向主节点(Master)发送 PSYNC 命令

阶段 2:全量同步(第一次连接,或无法部分重同步时)
1. Master 执行 BGSAVE,生成 RDB 快照
2. Master 把 RDB 文件发送给 Slave
3. Slave 清空自己的数据,加载 RDB 文件
4. Master 把「生成 RDB 期间的写命令」缓存起来
5. 把缓存的写命令发送给 Slave(增量)

阶段 3:增量同步(全量同步完成后)
Master 每执行一条写命令,都异步发送给 Slave
Slave 实时执行,保持与 Master 一致

PSYNC 命令(Redis 2.8+):

1
2
3
4
5
6
7
8
9
10
11
PSYNC 有两个模式:

完整重同步(Full Resync):
→ 和上面描述的「全量同步」一样

部分重同步(Partial Resync,2.8+)★:
Slave 断线重连后,
发送自己已经同步到的「复制偏移量(replication offset)」
→ 如果 Master 的复制缓冲区(replication backlog)中
还保留着这部分数据
→ 只发送「缺失的部分」,不需要全量同步!

复制偏移量(Replication Offset):

1
2
3
4
5
6
Master 和 Slave 都维护一个偏移量:
Master:每发送 N 字节的数据,offset += N
Slave:每接收 N 字节的数据,offset += N

如果 Slave 的 offset == Master 的 offset
→ 数据完全一致!

面试加分回答

「主从复制的延迟(主从不一致)是一个常见问题。Redis 的复制是异步的,Master 执行完写命令就返回客户端了,然后才把命令发送给 Slave,所以 Slave 的数据一定是有延迟的。如果业务需要『写完立即读到』,要么读主库,要么用 WAIT 命令(阻塞等待指定数量的 Slave 确认收到写命令)。另外,Redis 的复制积压缓冲区(replication backlog)的大小是可以配置的,如果 Slave 断线时间较长,积压缓冲区的数据被覆盖了,就会触发全量同步,所以 repl-backlog-size 要配置得足够大。」


第 20 题:Redis Sentinel(哨兵)的原理?

一句话结论

Sentinel = 监控 Master 是否宕机,自动故障转移(选一个新 Master),并通知客户端新的 Master 地址。Sentinel 本身也要集群部署(防止单点故障)。


深度解析

Sentinel 的三个作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
作用 1:监控(Monitoring)
→ 每个 Sentinel 每隔 1 秒向 MasterSlave、其他 Sentinel 发送 PING
→ 判断节点是否「主观下线(SDOWN)」

作用 2:自动故障转移(Automatic Failover)
→ 如果 Master 被判定为「客观下线(ODOWN)」
→ Sentinel 集群选举一个 Leader
→ Leader 从 Slave 中选一个作为新 Master
→ 通知其他 Slave 复制新 Master
→ 通知客户端新的 Master 地址

作用 3:配置提供者(Configuration Provider)
→ 客户端不直接连 Master 地址,而是连 Sentinel
→ Sentinel 告诉客户端当前谁是 Master

主观下线(SDOWN)vs 客观下线(ODOWN):

1
2
3
4
5
6
7
8
主观下线(SDOWN):
→ 单个 Sentinel 觉得 Master 不可达
→ 可能是网络抖动,不一定是真的宕机

客观下线(ODOWN):
→ 超过 quorum 个 Sentinel 都认为 Master 主观下线了
→ 才判定为「客观下线」,触发故障转移
→ quorum 通常设为 Sentinel 数量的 (n/2 + 1)(多数派)

Sentinel 集群的 Leader 选举(Raft 协议简化版):

1
2
3
4
5
6
规则:
1. 每个 Sentinel 都有机会成为 Leader
2. 先发现 ODOWN 的 Sentinel,向其他 Sentinel 发送「我要当 Leader」的请求
3. 收到请求的 Sentinel,如果还没投过票,就投给它
4. 第一个获得「多数派选票」的 Sentinel 成为 Leader
5. 由 Leader 执行故障转移

面试加分回答

「Sentinel 的故障转移需要时间(通常 30 秒~1 分钟),期间写请求会失败。如果对可用性要求很高,可以考虑 Redis Cluster(去中心化,每个节点都能处理请求,故障转移更快)。另外,Sentinel 模式下,客户端需要用 Sentinel 连接池(如 Java 的 JedisSentinelPool),而不是写死 Master 地址,这样 Sentinel 切换 Master 后,客户端才能自动感知。」


第 21 题:Redis Cluster(集群)的原理?

一句话结论

Redis Cluster = 去中心化的分布式方案,数据分片(16384 个槽),每个节点负责一部分槽。支持自动故障转移,无代理(客户端直接连对应节点)。


深度解析

数据分片(Sharding):

1
2
3
4
5
6
7
8
9
10
11
Redis Cluster 有 16384 个哈希槽(hash slot):

key 属于哪个槽?
→ CRC16(key) % 16384

集群中每个节点负责一部分槽:
节点 A:槽 0 ~ 5460
节点 B:槽 5461 ~ 10922
节点 C:槽 10923 ~ 16383

→ key 属于哪个槽,就去对应的节点执行命令

客户端路由:

1
2
3
4
5
6
7
客户端请求任意节点:
→ 如果 key 的槽就在当前节点 → 直接执行
→ 如果 key 的槽在别的节点 → 返回 MOVED 错误,
客户端重定向到正确的节点

(Redis 客户端通常会缓存「槽 → 节点」的映射表,
避免每次都重定向)

主从复制(每个主节点可以有从节点):

1
2
3
Redis Cluster 中,每个主节点可以有 1~N 个从节点:
→ 主节点挂了,从节点自动晋升为主节点(故障转移)
→ 不需要 Sentinel!Cluster 内置了哨兵功能

ASK 和 MOVED 的区别:

1
2
3
4
5
6
7
MOVED:
→ 槽已经永久迁移到另一个节点了
→ 客户端需要更新本地的「槽 → 节点」映射表

ASK:
→ 槽正在迁移中(从节点 A 迁移到节点 B
→ 客户端临时重定向到新节点,但不更新映射表

面试加分回答

「Redis Cluster 的 ASK 重定向MOVED 重定向的区别是一个高频追问。简单来说:MOVED 是『槽已经迁移完了』,客户端要更新路由表;ASK 是『槽正在迁移中』,客户端临时重定向,不更新路由表。另外,Redis Cluster 不支持多 key 跨槽操作(如 MSET key1 key2,如果 key1 和 key2 在不同槽,会报错),解决方式是使用 Hash Tag{user123}name{user123}age 会用大括号内的内容计算槽,保证在同一槽。」


四、实战与优化(22-30)


第 22 题:热 key 问题如何解决?

一句话结论

热 key = 某个 key 访问量极高,单节点扛不住。解决方案:本地缓存 + 备份热 key(分散到多个节点)。


深度解析

问题场景:

1
2
3
4
假设某个明星官宣,微博热度爆炸:
→ key = weibo:star:123 的 QPS 达到 100
→ 单个 Redis 节点扛不住(Redis 单节点 QPS 约 10 万)
→ Redis 被打挂

解决方案 ①:本地缓存(一级缓存)

1
2
3
4
5
6
7
8
9
架构:
客户端 → 本地缓存(如 Caffeine/Guava Cache)
↓ 本地缓存未命中
→ Redis
↓ Redis 也未命中
→ 数据库

热 key 直接走本地缓存,不请求 Redis!
→ 本地缓存的 QPS 可以达到千万级

解决方案 ②:热 key 备份(分散到多个节点)

1
2
3
4
5
6
7
8
9
10
# 原来的热 key
GET hotkey:star:123

# 改成多个备份 key
GET hotkey:star:123:bak1
GET hotkey:star:123:bak2
GET hotkey:star:123:bak3

# 客户端随机选一个 bak 访问
# → 把 100 万 QPS 分散到 3 个 key → 每个约 33 万 QPS

如何发现热 key?

1
2
3
4
5
6
7
# 方法 1:redis-cli --hotkeys(Redis 4.0+)
redis-cli --hotkeys

# 方法 2:monitor 命令(生产环境慎用,影响性能)
redis-cli monitor | head -1000 | awk '{print $2}' | sort | uniq -c | sort -rn

# 方法 3:Proxy 层统计(如 Codis/Twemproxy)

面试加分回答

「热 key 的发现和解决是 Redis 运维的核心能力。在生产环境中,通常会部署 Redis 监控(如 Prometheus + Grafana),监控每个 key 的 QPS。另外,阿里云和腾讯云的 Redis 服务都提供了热 key 自动发现功能,不需要自己实现。对于读多写少的热 key,本地缓存是最有效的方案;对于读写都频繁的热 key,需要用热 key 备份方案。」


第 23 题:大 key 问题如何解决?

一句话结论

大 key = 某个 key 的 value 很大(如 1 个 String 有 10MB,或 1 个 Set 有 100 万元素)。会导致:阻塞请求、网络阻塞、迁移困难。解决:拆分大 key。


深度解析

大 key 的危害:

1
2
3
4
5
6
7
8
9
10
11
危害 ①:删除大 key 阻塞主进程
DEL big_hash → 如果 big_hash 100 万元素,删除可能需要几秒!
→ 期间所有其他请求都被阻塞!

危害 ②:网络阻塞
GET big_string → 如果 big_string 10MB,
→ 网络传输需要几十毫秒,占用带宽

危害 ③:集群迁移困难
Redis Cluster 迁移槽时,如果某个 key 很大
→ 迁移耗时很长,迁移期间这部分数据不可用

如何发现大 key?

1
2
3
4
5
6
7
8
9
# 方法 1:redis-cli --bigkeys(Redis 4.0+)
redis-cli --bigkeys

# 方法 2:SCAN + DEBUG OBJECT(不推荐,影响性能)
SCAN 0
DEBUG OBJECT bigkey # 查看 key 的序列化长度

# 方法 3:MEMORY USAGE(Redis 4.0+,推荐)
MEMORY USAGE bigkey

大 key 的拆分方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
① 大 String → 拆成多个小 String
原来:SET big_string 10MB_value
改成:SET big_string:part1 value_part1
SET big_string:part2 value_part2
...

② 大 Hash → 按 field 拆成多个 Hash
原来:HSET big_hash 1000000_fields...
改成:HSET big_hash:part1 field1 value1...
HSET big_hash:part2 field1001 value1001...

③ 大 List/Set/ZSet → 拆成多个 List/Set/ZSet
原来:SADD big_set 1000000_members...
改成:SADD big_set:part1 member1...member500000
SADD big_set:part2 member500001...member1000000

安全删除大 key:

1
2
3
4
5
6
7
8
# 方法 1:UNLINK(Redis 4.0+,异步删除,不阻塞)
UNLINK big_hash

# 方法 2:分批删除(针对没有 UNLINK 的旧版本)
# 对大 Hash/Set/ZSet,每次删除一部分:
HSCAN big_hash 0 COUNT 100
HDEL big_hash field1 field2 ... field100
# 循环,直到全部删完

面试加分回答

「Redis 4.0 引入了 lazyfree(惰性删除),把大 key 的删除放到后台线程执行,不阻塞主进程。配置方式:lazyfree-lazy-eviction yeslazyfree-lazy-expire yeslazyfree-lazy-server-del yes。在生产环境中,这些参数一定要开启,否则删除大 key 可能导致整个 Redis 实例卡顿几秒到几十秒。」


第 24 题:Redis 慢查询如何排查?

一句话结论

*Redis 慢查询 = 执行时间超过阈值的命令。通过 slowlog get 查看,重点排查:大 key 操作、复杂度高的命令(KEYS 、HGETALL 等)、网络延迟。


深度解析

开启慢查询日志:

1
2
3
4
5
6
7
8
# redis.conf
slowlog-log-slower-than 10000 # 超过 10000 微秒(10 毫秒)的命令记录到慢日志
slowlog-max-len 128 # 慢日志最多保存 128 条

# 查看慢日志
SLOWLOG GET 10 # 查看最近 10 条慢日志
SLOWLOG LEN # 慢日志数量
SLOWLOG RESET # 清空慢日志

慢日志输出示例:

1
2
3
4
5
6
SLOWLOG GET 1
1) 1) (integer) 1234567890 # 时间戳
2) (integer) 15 # 耗时(微秒)
3) 1) "HGETALL" # 命令
2) "big_hash" # 参数
4) "127.0.0.1:54321" # 客户端地址

常见慢查询原因排行:

1
2
3
4
5
 1 名:KEYS *(永远不要用!用 SCAN 代替)
2 名:大 key 的 HGETALL / SMEMBERS / LRANGE 0 -1
3 名:FLUSHALL / FLUSHDB(清空整个库)
4 名:多个 key 的 MSET / MGET(超过 1000 个 key 要拆分)
5 名:Lua 脚本执行时间过长

面试加分回答

KEYS * 是 Redis 运维的禁忌命令,会在生产环境中把 Redis 打挂。替代方案:用 SCAN 命令(增量遍历,不阻塞)。另外,Redis 6.0 引入了 ACL(访问控制),可以禁止某些用户执行危险命令(如 KEYSFLUSHALL),这是生产环境安全加固的重要手段。」


第 25 题:Redis 6.0 多线程详解

一句话结论

Redis 6.0 的多线程只用于网络 I/O(读取请求、写回响应),命令执行仍然是单线程的。性能提升约 2~3 倍(网络 I/O 密集型场景)。


深度解析

为什么 Redis 6.0 要引入多线程 I/O?

1
2
3
4
5
6
7
Redis 的性能瓶颈:
→ 命令执行(内存操作):极快,单线程足够
→ 网络 I/O(读写 socket):随着 QPS 增高,成为瓶颈

特别是在大 key 场景:
→ 读取 10MBString value,网络传输耗时很长
→ 单线程处理网络 I/O,会阻塞其他请求

Redis 6.0 多线程模型:

1
2
3
4
5
6
7
8
9
10
11
主线程:
↓ 把读取到的客户端请求分发给 I/O 线程
I/O 线程 1:解析请求(多线程并行)
I/O 线程 2:解析请求(多线程并行)
I/O 线程 3:解析请求(多线程并行)
↓ 所有 I/O 线程解析完
主线程:执行命令(仍然是单线程!)
↓ 把响应分发给 I/O 线程
I/O 线程 1:写回响应(多线程并行)
I/O 线程 2:写回响应(多线程并行)
I/O 线程 3:写回响应(多线程并行)

如何开启多线程 I/O?

1
2
3
# redis.conf
io-threads 4 # I/O 线程数(建议设为 CPU 核心数的 1/2 ~ 1 倍)
io-threads-do-reads yes # 开启多线程 I/O(默认 no,需要手动开启)

注意io-threads 建议设为 CPU 核心数的 1/2 ~ 1 倍,不要超过 CPU 核心数。


面试加分回答

「Redis 6.0 的多线程 I/O 是一个经常被误解的特性。需要明确:命令执行仍然是单线程的,多线程只用于网络 I/O 的读写。这样设计的好处是:既利用了多核 CPU 处理网络 I/O 的能力,又避免了多线程命令执行带来的锁竞争和复杂度。另外,Redis 7.0 进一步优化了多线程 I/O,支持多线程同时处理读写(6.0 只有写是多线的,读是单线的),性能更好。」


第 26 题:Redis Stream 数据结构详解

一句话结论

Stream = Redis 的持久化消息队列,支持消费者组、消息 ACK、Pending 消息重试。是 Pub/Sub 的升级版,适合做消息队列。


深度解析

Stream 的核心概念:

1
2
3
4
Stream 结构:
→ 类似 Append-Only Log(只能追加,不能修改历史消息)
→ 每条消息有一个唯一 ID(时间戳-序列号)
→ 支持消费者组(Consumer Group,类似 Kafka)

基本用法:

1
2
3
4
5
6
7
8
9
10
# 生产者:添加消息
XADD mystream * key1 value1 key2 value2
# * 表示让 Redis 自动生成消息 ID(时间戳-序列号)

# 消费者:读取消息(独立消费,不通过消费者组)
XREAD COUNT 2 STREAMS mystream 0 # 从 ID=0 开始读 2 条

# 阻塞式读取(类似 BRPOP)
XREAD COUNT 1 STREAMS mystream $ # $ = 最新消息的 ID
# 新消息到达时,立即返回

消费者组(Consumer Group):

1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM
# $ = 从最新消息开始消费

# 消费者组中读取消息
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
# > = 读取尚未分配给其他消费者的消息

# ACK 确认消息(告诉 Redis 这条消息已经处理完了)
XACK mystream mygroup message_id_123

# 查看 Pending 消息(已发送但未 ACK 的消息)
XPENDING mystream mygroup

Stream vs Kafka:

对比项 Redis Stream Kafka
吞吐量 较低(约几万 QPS) 很高(百万 QPS)
消息持久化 支持(AOF/RDB) 支持(多副本)
消费者组 ✅ 支持 ✅ 支持
消息回溯 ✅ 支持(指定 ID 读取) ✅ 支持
适用场景 轻量级消息队列 大数据量消息队列

面试加分回答

「Redis Stream 是 Redis 5.0 引入的重要特性,让 Redis 终于有了一个可靠的消息队列实现。但在生产环境中,是否用 Stream 做消息队列,取决于消息量:如果 QPS < 10 万,Stream 完全够用;如果 QPS > 10 万,建议用 Kafka 或 RocketMQ。另外,Stream 的 XCLAIM 命令可以实现消息重试(把 Pending 超时的消息分配给其他消费者),这是实现「至少一次投递」的关键。」


第 27 题:Redis 7.0 新特性

一句话结论

Redis 7.0 的重要新特性:Function(替代 Lua 脚本)、ACL 增强、RDB 频率可配置、性能优化(Hash/Set 编码优化)。


深度解析

新特性 ①:Function(函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
Redis 7.0 引入了 Function API,可以替代 Lua 脚本:

之前的 Lua 脚本方式:
EVAL "redis.call('SET', KEYS[1], ARGV[1])" 1 key value

新的 Function 方式:
FUNCTION LOAD "#!lua name=myfunc\n redis.call('SET', KEYS[1], ARGV[1])"
FCALL myfunc 1 key value

优势:
Function 可以持久化(存储在 RDB/AOF 中)
→ Lua 脚本每次重启都要重新加载
Function 支持库(Library),可以组织多个函数

新特性 ②:ACL 增强:

1
2
3
4
5
6
7
8
9
Redis 6.0 引入了 ACL(访问控制列表),7.0 进一步增强:

# 创建用户,设置密码,设置可执行的命令
ACL SETUSER alice on >password ~cached:* +GET +SET
# alice 只能访问 cached:* 开头的 key,只能执行 GET 和 SET

# 查看当前用户的权限
ACL WHOAMI
ACL LIST

新特性 ③:RDB 保存频率可配置:

1
2
3
4
5
6
Redis 7.0 之前:
RDB 保存频率由 save 配置决定(如 save 900 1)
→ 修改配置需要重启

Redis 7.0:
CONFIG SET save "900 1 300 10" # 动态修改,不需要重启

面试加分回答

「Redis 7.0 的 Function 是一个比较冷门但很有用的特性,它解决了 Lua 脚本在持久化和复制场景下的问题。另外,Redis 7.2(最新版本)还引入了 Vector Similarity Search(向量相似度搜索),可以直接在 Redis 中做向量检索(类似 Milvus),这对于 AI 应用(如 RAG)非常有用,不需要单独部署向量数据库。」


第 28 题:Redis 内存优化技巧

一句话结论

内存优化 = 选对数据结构 + 控制 key/value 大小 + 开启内存淘汰策略 + 定期清理大 key/过期 key。


深度解析

技巧 ①:使用正确的数据结构

1
2
3
4
5
错误示范:用 String 存储对象
SET user:1 "{name:'Alice',age:25}" # 需要 JSON 序列化,占空间

正确示范:用 Hash 存储对象
HSET user:1 name "Alice" age 25 # 更省内存,不需要序列化

技巧 ②:控制 key 的长度

1
2
3
4
5
6
7
8
错误示范:超长的 key
SET user:account:profile:basic:info:1234567890 ...

正确示范:简短的 key
SET u:1234567890:name ...

key 越长,占用的内存越多(每个 key 都要存储)
→ 建议 key 控制在 32 字节以内

技巧 ③:开启内存淘汰策略

1
2
3
# redis.conf
maxmemory 4gb # 设置最大内存
maxmemory-policy allkeys-lru # 内存满时,LRU 淘汰

技巧 ④:使用整数集合(IntSet)和压缩列表(ZipList)

1
2
3
4
5
6
7
Redis 在数据量小时,会自动用紧凑的编码:

Set(全是整数,且元素数 < 512)→ intset(很省内存)
Hash/List/ZSet(元素数 < 512 且每个元素 < 64 字节)→ ziplist

→ 这是 Redis 内存优化的「内功」
→ 不要因为「怕麻烦」就直接用大数据结构

面试加分回答

「Redis 的内存优化有一个很实用的工具:MEMORY DOCTOR(Redis 7.0+),可以自动分析内存使用情况并给出优化建议。另外,对于海量小 key 的场景(如 1 亿个 key),Redis 的字典哈希表本身的 overhead 就很大(每个 key 至少有 64 字节的元数据),这时应该考虑分片(用 Hash Tag 把相关数据放到同一个槽)或者用 Redis Module(如 ReJSON,存储 JSON 比 String 省内存得多)。」


第 29 题:Redis 安全加固建议

一句话结论

Redis 安全 = 禁止 ROOT 运行 + 设置密码 + 重命名/禁用危险命令 + 配置 ACL + 不要暴露到公网。


深度解析

安全加固清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1. 不要 ROOT 运行 Redis
→ 用专用用户(如 redis)运行

2. 设置强密码
requirepass "你的强密码"

3. 禁用/重命名危险命令
rename-command FLUSHALL "" # 禁用 FLUSHALL
rename-command KEYS "HIDDEN_KEYS" # 重命名 KEYS

4. 配置 bind(不要监听所有网卡)
bind 127.0.0.1 # 只允许本机访问

5. 不要暴露到公网
→ Redis 默认端口 6379,公网扫描器很容易发现
→ 生产环境一定要放在内网!

6. 配置防火墙规则
→ 只允许应用服务器访问 Redis 端口

7. 使用 ACL(Redis 6.0+)
→ 为不同应用创建不同用户,最小权限原则

面试加分回答

「Redis 未授权访问漏洞是一个经典的安全问题(CVE-2018-12326)。如果 Redis 没有设置密码,且监听在 0.0.0.0,攻击者可以直接连接 Redis,写入 SSH 公钥,然后 SSH 登录服务器!所以生产环境的 Redis 一定要设置密码 + 禁止 ROOT 运行 + 不要暴露到公网。另外,云服务商(阿里云/腾讯云)的 Redis 服务都内置了这些安全加固,自己部署时更需要小心。」


第 30 题:如何设计一个高并发的秒杀系统(Redis 为核心)?

一句话结论

秒杀系统 = Redis 做库存预扣减(原子操作)+ 消息队列异步下单 + 数据库最终扣减。核心是:把「写数据库」变成「写 Redis + 异步批处理」。


深度解析

秒杀的核心挑战:

1
2
3
4
5
6
7
8
挑战 ①:瞬时高并发(10 万人同时抢 100 件商品)
→ 数据库扛不住

挑战 ②:超卖问题(100 件库存,卖了 101 件)
→ 并发扣减库存,没有加锁

挑战 ③:恶意请求(刷接口)
→ 要限流

秒杀系统架构:

1
2
3
4
5
6
7
① 限流(Nginx/网关层)
↓ 放行合法请求
② Redis 预扣减库存(原子操作,不超卖)
↓ 扣减成功
③ 消息队列(削峰,异步处理)
↓ 消费者慢慢处理
④ 数据库最终扣减(批量,减少数据库压力)

核心代码(Redis 原子扣减库存):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 秒杀 Lua 脚本(保证原子性)
-- KEYS[1] = 库存 key
-- ARGV[1] = 扣减数量

local stock = redis.call("GET", KEYS[1])

if not stock then
return -1 -- 库存 key 不存在
end

if tonumber(stock) >= tonumber(ARGV[1]) then
redis.call("DECRBY", KEYS[1], ARGV[1])
return 1 -- 扣减成功
else
return 0 -- 库存不足
end

-- 执行:
EVAL script 1 seckill:stock:product123 1

完整秒杀流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 用户点击「秒杀」按钮
→ Nginx 限流(每个用户最多 1 次/秒)

2. 请求到达应用层
→ 查 Redis:用户是否已秒杀过?(SETNX user:123:seckill:product123)
→ 如果已秒杀过,返回「重复秒杀」

3. Lua 脚本原子扣减库存
→ 扣减成功:把订单信息发到消息队列(如 Kafka)
→ 扣减失败:返回「库存不足」

4. 消息队列消费者(异步)
→ 从 MQ 读取订单信息
→ 批量插入数据库(如每 100 条插一次)
→ 更新数据库库存

5. 用户查询订单状态
→ 先查 Redis(订单状态缓存)
→ 没有再查数据库

面试加分回答

「秒杀系统的核心是削峰填谷:用 Redis 抗瞬时高并发,用消息队列把高峰请求平滑化,用数据库做最终一致性扣减。另外,对于超热门秒杀(如 100 万 QPS),单台 Redis 也扛不住,这时需要用 Redis Cluster(把库存分散到多个节点,每个节点负责一部分库存)。还有一个细节:秒杀前要预热(把库存提前加载到 Redis),否则第一波请求会全部打到数据库。」



第 41 题:Redis 做消息队列有哪几种方案?分别适合什么场景?

一句话结论

Redis 做消息队列有 3 种方案:① List(简单但无 ACK)② Pub/Sub(快但不持久化)③ Stream(5.0+,功能最完整)。优先级:Stream > List > Pub/Sub。


深度解析

方案 ①:List(LPUSH + BRPOP)

1
2
3
4
5
# 生产者
LPUSH queue:order order_id_123

# 消费者(阻塞式弹出)
BRPOP queue:order 0 # 0 = 一直阻塞,直到有消息

优点:简单,自带持久化(AOF/RDB)
缺点:❌ 无 ACK 机制(消费者挂了,消息就丢了)、❌ 不支持消费者组(无法并行消费)

适用场景:简单的任务队列(如发送邮件、生成报表),对可靠性要求不高


方案 ②:Pub/Sub(发布订阅)

1
2
3
4
5
# 订阅者(Consumer)
SUBSCRIBE news.sports

# 发布者(Producer)
PUBLISH news.sports "湖人队今天赢了!"

优点:实时推送,支持模式订阅(PSUBSCRIBE news.*
缺点:❌ 消息不持久化(订阅者下线期间消息全部丢失)、❌ 无 ACK 机制

适用场景:即时消息推送(如聊天室、实时通知),允许丢消息


方案 ③:Stream(Redis 5.0+,推荐)

1
2
3
4
5
6
7
# 生产者
XADD mystream * key1 value1

# 消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
XACK mystream mygroup message_id # ACK 确认

优点:✅ 消息持久化、✅ ACK 机制、✅ 消费者组、✅ 消息回溯
缺点:复杂度较高

适用场景:对可靠性要求高的消息队列(如订单处理),QPS < 10 万时可以用


3 种方案对比:

对比项 List Pub/Sub Stream
消息持久化
ACK 机制
消费者组
消息回溯
复杂度
推荐指数 ⭐⭐ ⭐⭐⭐⭐⭐

面试加分回答

「Redis 做消息队列的最大问题是无法保证严格顺序(List 可以,但 Stream 的消费者组模式下,不同消费者的消息顺序无法保证)。另外,如果消息量很大(QPS > 10 万),或者对延迟极其敏感,建议直接用 KafkaRocketMQ,不要用 Redis。Redis 做消息队列只适合轻量级场景(如站内信、异步审计日志)。」


第 42 题:Redis 热点 Key 的发现和处理方案详解

一句话结论

热点 Key = 访问频率远超其他 Key 的 Key。发现方式:monitor 命令(不推荐)、–hotkeys 参数(推荐)、业务日志;处理方案:本地缓存 + Key 备份 + 读写分离。


深度解析

如何发现热点 Key?

方法 命令/工具 优点 缺点
redis-cli –hotkeys redis-cli --hotkeys -i 0.1 官方工具,准确 需要开启 LFU 策略
monitor + 分析 redis-cli monitor | sort | uniq -c 简单 影响性能,生产环境禁用
业务层统计 在代码中记录每个 Key 的访问次数 最准确 需要改代码
Proxy 层统计 Codis/Twemproxy 自带热点 Key 统计 无侵入 需要部署 Proxy
云服务商 阿里云/腾讯云 Redis 控制台 最方便 需要花钱

处理方案 ①:本地缓存(一级缓存)

1
2
3
4
5
6
7
8
9
架构:
客户端 → 本地缓存(Caffeine/Guava Cache)
↓ 未命中
→ Redis
↓ 未命中
→ 数据库

热点 Key 直接走本地缓存,完全不请求 Redis!
本地缓存的 QPS 可以达到千万级。

适用场景:读多写少的热点 Key(如首页推荐、商品详情)


处理方案 ②:Key 备份(分散热点)

1
2
3
4
5
6
7
8
9
10
# 原来的热点 Key
GET hotkey:product:123

# 改成多个备份 Key
GET hotkey:product:123:bak1
GET hotkey:product:123:bak2
GET hotkey:product:123:bak3

# 客户端随机选择一个备份访问
# 把 100 万 QPS 分散到 3 个 Key → 每个 ~33 万 QPS

注意:写操作需要更新所有备份,或者用「本地缓存 + 短过期」方案


处理方案 ③:读写分离

1
2
3
架构:
读请求 → 从节点(多个从节点分担读流量)
写请求 → 主节点

适用场景:读多写少,且热点 Key 数量不多


面试加分回答

「热点 Key 的处理是一个系统性问题,不是单靠 Redis 就能解决的。在生产环境中,通常会多层防护:① 业务层限流(每个用户最多 N 次/秒);② 本地缓存(Caffeine);③ Redis 读写分离;④ 如果是电商秒杀场景,还会用 CDN 缓存静态页面 + 库存分散到多个 Redis Key(分桶扣减)。另外,Redis 7.0 引入了 hashscript 功能,可以在服务端执行 Lua 脚本,减少网络往返,对热点 Key 场景有一定缓解作用。」


第 43 题:Redis 和本地缓存(如 Caffeine)如何搭配使用?

一句话结论

多级缓存 = 本地缓存(Caffeine/Guava)做一级缓存,Redis 做二级缓存。读取时先查本地,未命中再查 Redis;写入时先写 DB,再删除 Redis 缓存,最后清理本地缓存。


深度解析

为什么要多级缓存?

1
2
3
4
5
6
7
8
9
10
只用 Redis 的问题:
→ 每次请求都要走网络(RTT ~1ms)
→ Redis 单节点 QPS 上限 ~10 万
→ 热点 Key 会打垮 Redis

多级缓存的优势:
→ 本地缓存:QPS 可达千万级,延迟 < 1μs
→ Redis:QPS ~10 万,延迟 ~1ms
→ 数据库:QPS ~1 万,延迟 ~10ms
→ 层层拦截,最终打到 DB 的请求极少

多级缓存架构:

1
2
3
4
5
6
7
请求 → ① 本地缓存(Caffeine)
↓ 未命中
→ ② Redis
↓ 未命中
→ ③ 数据库
↓ 查到结果
← ④ 写入 Redis + 本地缓存

Java 代码示例(Spring Boot + Caffeine):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Service
public class UserService {

// 本地缓存(Caffeine)
private final Cache<String, User> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();

@Autowired
private RedisTemplate<String, User> redisTemplate;

@Autowired
private UserMapper userMapper;

public User getUserById(Long userId) {
String key = "user:" + userId;

// ① 先查本地缓存
User user = localCache.getIfPresent(key);
if (user != null) {
return user;
}

// ② 再查 Redis
user = redisTemplate.opsForValue().get(key);
if (user != null) {
localCache.put(key, user); // 回填本地缓存
return user;
}

// ③ 查数据库
user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
localCache.put(key, user); // 回填本地缓存
}

return user;
}

// 更新用户时,删除缓存(Cache Aside Pattern)
public void updateUser(User user) {
userMapper.updateById(user);

String key = "user:" + user.getId();
redisTemplate.delete(key); // 删除 Redis 缓存
localCache.invalidate(key); // 删除本地缓存
}
}

面试加分回答

「多级缓存的一致性问题是一个难点。如果数据库更新了,但本地缓存还没过期,就会读到旧数据。解决方案:① 缩短本地缓存的过期时间(如 1~5 分钟);② 订阅 Redis 的过期/删除事件(Redis Keyspace Notifications),当 Redis 缓存被删除时,通知所有本地节点删除本地缓存;③ 用 消息队列(如 RocketMQ)广播缓存失效消息。另外,对于分布式本地缓存(多个应用节点),可以考虑用 Redis Pub/Sub 广播缓存失效消息。」


第 44 题:Redis 的过期键删除策略 vs 内存淘汰策略,有什么区别?

一句话结论

过期键删除策略 = 处理「带 TTL 的过期 Key」(惰性删除 + 定期删除);内存淘汰策略 = 处理「内存达到上限时,删哪些 Key」(LRU/LFU/Random 等)。两者是独立的机制。


深度解析

过期键删除策略(针对带 TTL 的 Key):

1
2
3
4
5
6
7
8
9
10
触发条件:
Key 设置了过期时间(EXPIRE)
Key 过期了

两种策略:
① 惰性删除:访问 Key 时才检查是否过期,过期就删
② 定期删除:后台定时抽样检查,删除过期的 Key

目的:
→ 清理「已经过期」的 Key,释放内存

内存淘汰策略(针对所有 Key):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
触发条件:
→ Redis 内存使用达到 maxmemory 上限

8 种策略:
noeviction:不淘汰,写满报错
allkeys-lru:所有 Key 中,淘汰最久未使用的
volatile-lru:只淘汰「设置了过期时间」的 Key 中最久未使用的
allkeys-random:所有 Key 中随机淘汰
volatile-random:只淘汰「设置了过期时间」的 Key 中随机的
volatile-ttl:只淘汰「设置了过期时间」的 Key 中 TTL 最小的
allkeys-lfu(4.0+):所有 Key 中,淘汰访问频率最低的
volatile-lfu(4.0+):只淘汰「设置了过期时间」的 Key 中访问频率最低的

目的:
→ 腾出内存,让新的写请求能成功

两者的关系:

1
2
3
4
5
6
7
8
9
10
11
场景 1Key 过期了,但内存没满
→ 过期键删除策略生效(惰性/定期删除)
→ 内存淘汰策略不触发

场景 2:内存满了,但有 Key 还没过期
→ 过期键删除策略继续工作(清理过期 Key
→ 内存淘汰策略也触发(淘汰没过期的 Key

场景 3:内存满了,且所有 Key 都过期了
→ 过期键删除策略清理过期 Key
→ 内存淘汰策略可能也触发(如果过期 Key 清理速度赶不上写入速度)

面试加分回答

「这两个概念经常被混淆。记住一个简单的判断方法:过期键删除策略只管「过期的 Key」;内存淘汰策略管「所有 Key」(当内存不够时)。 另外,Redis 的 maxmemory-policy 配置中的 volatile-* 策略(如 volatile-lru),只淘汰「设置了过期时间的 Key」,如果所有 Key 都没设置过期时间,Redis 会报错(和 noeviction 一样)!这是生产环境的一个大坑,一定要确保所有 Key 都设置了过期时间,或者改用 allkeys-* 策略。」


第 45 题:Redis 事务 vs Lua 脚本,该用哪个?

一句话结论

Redis 事务(MULTI/EXEC)很弱(不支持回滚、多条命令非原子);Lua 脚本(EVAL)是真正的原子操作。需要「先读后写」的原子性时,用 Lua 脚本。


深度解析

Redis 事务的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
问题 1:不支持回滚
MULTI
SET key1 value1
INCR key1 ← 会报错(value1 不能 INCR
SET key2 value2
EXEC
→ 报错后,后续命令仍然执行,且不回滚

问题 2:WATCH 的乐观锁不可靠
→ 高并发场景下,WATCH 会导致大量重试
→ 性能下降

问题 3:多条命令不是原子执行的
→ 事务中的命令是一条条执行的
→ 虽然不会被其他客户端打断,但单条命令失败不影响后续

Lua 脚本的优势:

1
2
3
4
5
6
7
8
9
10
11
优势 1:原子执行
→ 整个 Lua 脚本执行期间,不会执行其他客户端的命令
→ 真正的原子性

优势 2:减少网络 RTT
→ 多条命令打包成一个脚本,一次发送
→ 类似 Pipeline,但是原子的

优势 3:支持复杂逻辑
→ 可以用 if/else、for 循环等控制结构
→ 实现复杂的业务逻辑

Lua 脚本示例(秒杀库存扣减):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 秒杀 Lua 脚本
-- KEYS[1] = 库存 Key
-- ARGV[1] = 扣减数量

local stock = tonumber(redis.call("GET", KEYS[1]))

if not stock then
return -1 -- 库存 Key 不存在
end

if stock >= tonumber(ARGV[1]) then
redis.call("DECRBY", KEYS[1], ARGV[1])
return 1 -- 扣减成功
else
return 0 -- 库存不足
end

Redis 事务 vs Lua 脚本对比:

对比项 Redis 事务 Lua 脚本
原子性 ✅ 不会被打断,但不支持回滚 ✅ 真正原子执行
复杂度 低(只能打包命令) 高(支持逻辑控制)
网络 RTT 多次(MULTI/命令/EXEC) 一次(EVAL)
适用场景 简单批量命令 先读后写、复杂逻辑

面试加分回答

「Lua 脚本虽然强大,但有执行时长限制(默认 5 秒,由 lua-time-limit 配置)。如果 Lua 脚本执行时间过长,会阻塞 Redis 主线程,导致其他请求超时。所以 Lua 脚本中不要有长时间循环,尽量用 redis.call() 而不是 redis.pcall()pcall 会捕获错误,但性能稍差)。另外,Redis 7.0 引入了 Function(函数),可以替代 Lua 脚本,支持持久化和复制,是未来的方向。」


第 46 题:Redis 常见性能问题与优化方案

一句话结论

Redis 性能问题主要由:大 Key、热 Key、慢查询命令、内存碎片、网络延迟引起。优化方案:拆分大 Key、本地缓存抗热 Key、用 SCAN 替代 KEYS、开启内存碎片整理。


深度解析

性能问题 ①:大 Key(超过 10KB 的 String,或超过 5000 个元素的集合)

1
2
3
4
5
6
7
8
9
10
11
12
13
危害:
→ 删除大 Key 阻塞主线程(Redis 4.0 之前)
→ 网络传输慢(GETKey 占用带宽)
→ 迁移困难(Redis Cluster 迁移槽时,大 Key 会导致迁移耗时很长)

发现:
redis-cli --bigkeys
MEMORY USAGE bigkey

解决:
→ 拆分大 Key(把一个大 Hash 拆成多个小 Hash
→ 用 UNLINK 异步删除(Redis 4.0+
→ 开启 lazyfree(后台线程删除)

性能问题 ②:热 Key(访问频率极高的 Key)

1
2
3
4
5
6
7
8
9
10
11
12
13
危害:
→ 单节点 CPU 打满
→ 网络带宽占满
→ 可能导致 Redis 宕机

发现:
redis-cli --hotkeys
业务层统计

解决:
→ 本地缓存(Caffeine)
Key 备份(把热 Key 分散到多个备份 Key
→ 读写分离(多个从节点分担读流量)

性能问题 ③:慢查询命令(O(N) 复杂度的命令)

1
2
3
4
5
6
7
8
9
10
11
危险命令:
KEYS *(永远不要用!用 SCAN 替代)
HGETALL(获取整个 Hash,用 HSCAN 替代)
SMEMBERS(获取整个 Set,用 SSCAN 替代)
LRANGE list 0 -1(获取整个 List,分批获取)
ZRANGEBYSCORE(获取大量元素,限制返回数量)

解决:
→ 用 SCAN 系列命令替代(HSCAN/SSCAN/ZSCAN
→ 限制返回数量(如 LRANGE list 0 100
→ 把大 Key 拆小

性能问题 ④:内存碎片

1
2
3
4
5
6
7
8
9
10
11
12
产生原因:
→ 频繁修改数据(如 SET 一个更大的值)
→ 键值对大小不一(内存分配器分配的内存 > 实际使用)

发现:
INFO memory
→ 看 used_memory_rss / used_memory 的比值
→ 如果 > 1.5,说明碎片严重

解决:
→ 开启自动内存碎片整理(activedefrag yes
→ 重启 Redis(最简单粗暴,但有停机时间)

面试加分回答

「Redis 性能调优的第一步是找到瓶颈。用 redis-cli --latency 检查延迟,用 redis-cli --bigkeys--hotkeys 找大 Key 和热 Key,用 SLOWLOG GET 找慢查询。另外,Redis 的 hz 参数(默认 10)控制后台任务的执行频率(如定期删除过期 Key、后台重写 AOF),如果 Redis 的 CPU 占用不高,可以适当调大 hz(如 20~50),让后台任务更积极,减少内存碎片和过期 Key 堆积。」


第 47 题:Redis 集群方案选型:Sentinel vs Cluster vs Codis

一句话结论

Sentinel = 主从高可用(故障自动切换),客户端需要支持 Sentinel;Cluster = 分布式(数据分片 + 高可用),无代理;Codis = 代理模式分布式(类似于 Codis 是中国公司开源的)。优先级:Cluster > Sentinel > Codis(Codis 已停止维护)。


深度解析

Sentinel(哨兵模式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
架构:
1Master + NSlave(主从复制)
→ M 个 Sentinel(监控 + 自动故障转移)

优点:
✅ 高可用(Master 挂了,Sentinel 自动切换)
✅ 配置简单

缺点:
❌ 没有分片(所有数据都在 Master,存储有上限)
❌ 写能力受限于单 Master
❌ 客户端需要支持 Sentinel 协议

适用场景:
→ 数据量不大(< 10GB),QPS 不高(< 10 万)
→ 需要高可用,但不需要分布式

Cluster(集群模式,推荐):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
架构:
→ N 个 Master(每个 Master 可以有 Slave
→ 数据分片(16384 个槽)
→ 无代理(客户端直接连对应节点)

优点:
✅ 分布式(数据分片,存储无上限)
✅ 高可用(每个分片都有主从)
✅ 无代理(性能高)

缺点:
❌ 客户端需要支持 Cluster 协议(如 JedisCluster)
❌ 不支持跨槽多 Key 操作(如 MSET key1 key2,如果不在同一槽会报错)
❌ 迁移槽时会影响性能

适用场景:
→ 数据量大(> 10GB),QPS 高(> 10 万)
→ 需要分布式存储

Codis(代理模式,已停止维护):

1
2
3
4
5
6
7
8
9
10
11
12
架构:
→ Codis Proxy(代理层)
→ 后端多个 Redis 实例
→ ZooKeeper/etcd(存储路由信息)

优点:
✅ 客户端不需要支持分布式(连 Proxy 就行)
✅ 支持跨槽多 Key 操作

缺点:
❌ 代理层有性能损耗
❌ 已停止维护(不建议新项目使用)

3 种方案对比:

对比项 Sentinel Cluster Codis
分布式
高可用
无代理
客户端支持 需要支持 Sentinel 需要支持 Cluster 不需要(连 Proxy)
维护状态 ✅ 官方维护 ✅ 官方维护 ❌ 已停止维护
推荐指数 ⭐⭐⭐ ⭐⭐⭐⭐⭐

面试加分回答

「Redis Cluster 的槽迁移是一个复杂的话题。在迁移槽的过程中,会有 ASKMOVED 重定向,客户端需要正确处理这两种重定向。另外,Redis Cluster 的跨槽操作限制(如 MSET、MGET 的 Key 必须在同一槽),可以通过 Hash Tag 解决:{user123}name{user123}age 会用大括号内的内容计算槽,保证在同一槽。生产环境中,建议用 Redis Cluster 客户端(如 Java 的 JedisCluster、Lettuce),它们已经处理了重定向、重试等复杂逻辑。」


第 48 题:Redis 6.0 之后的新特性总结(面试高频)

一句话结论

Redis 6.0:多线程 I/O、ACL 访问控制、RESP3 协议;Redis 7.0:Function 替代 Lua 脚本、Hash/Set 编码优化;Redis 7.2:向量相似度搜索(Vector Similarity Search)。


深度解析

Redis 6.0 新特性:

特性 说明 面试重要性
多线程 I/O 网络 I/O 多线程,命令执行仍单线程 ⭐⭐⭐⭐⭐
ACL(访问控制) 用户权限管理,限制命令和 Key ⭐⭐⭐
RESP3 协议 新的通信协议,支持更多数据类型 ⭐⭐
Client-side caching 客户端缓存(类似本地缓存) ⭐⭐⭐

Redis 7.0 新特性:

特性 说明 面试重要性
Function 替代 Lua 脚本,支持持久化 ⭐⭐⭐
Hash/Set 编码优化 小数据量时用 listpack 替代 ziplist ⭐⭐
RDB 保存频率动态配置 CONFIG SET save 不需要重启 ⭐⭐
AOF 文件组织优化 AOF 重写时,按类型分组命令

Redis 7.2 新特性(最新):

特性 说明 面试重要性
Vector Similarity Search 向量相似度搜索(支持 AI 应用) ⭐⭐⭐⭐
Hash Field Expiration Hash 的单个字段支持过期时间 ⭐⭐⭐

面试加分回答

「Redis 7.2 的 Vector Similarity Search(VSS) 是一个非常有用的新特性,让 Redis 可以直接做向量检索(类似 Milvus、Pinecone)。这对于 AI 应用(如 RAG、语义搜索)非常有用,不需要单独部署向量数据库。用法示例:FT.CREATE idx ON HASH PREFIX 1 doc: SCHEMA embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 768(创建向量索引),FT.SEARCH idx "*=>[KNN 10 @embedding $query_vec]"(向量相似度搜索)。另外,Redis 的 VSS 是基于 HNSW 算法(Hierarchical Navigable Small World),查询速度快,但构建索引稍慢。」


第 49 题:Redis 使用规范与最佳实践

一句话结论

*Redis 使用规范:Key 设计要简洁有意义、避免大 Key 和热 Key、禁止危险命令(KEYS 、FLUSHALL)、设置过期时间、用 Pipeline 批量操作、开启 AOF 持久化。


深度解析

规范 ①:Key 设计规范

1
2
3
4
5
6
✅ 用 : 分隔语义(如 user:123:name)
Key 要简洁(建议 < 32 字节)
✅ 用前缀区分业务(如 cache:、lock:、session:)
❌ 不要用超长的 Key(浪费内存)
❌ 不要用特殊字符(如空格、换行)
❌ 不要频繁修改 Key 的命名规则(不利于维护)

规范 ②:Value 设计规范

1
2
3
4
5
✅ 避免大 ValueString < 10KB,集合元素 < 5000
✅ 用合适的编码(Hash 存储对象,而不是 String 序列化)
✅ 设置过期时间(防止内存泄漏)
❌ 不要存储大文件(如图片、视频,用对象存储)
❌ 不要存储二进制数据(用对象存储的 URL

规范 ③:命令使用规范

1
2
3
4
5
6
7
✅ 用 Pipeline 批量操作(减少 RTT)
✅ 用 SCAN 替代 KEYS *
✅ 用 HSCAN/SSCAN/ZSCAN 替代 HGETALL/SMEMBERS/ZRANGE 0 -1
✅ 用 UNLINK 替代 DEL(删除大 Key)
❌ 禁止用 KEYS *(会阻塞 Redis)
❌ 禁止用 FLUSHALL/FLUSHDB(清空整个库)
❌ 禁止用 MULTI/EXEC 做复杂事务(用 Lua 脚本替代)

规范 ④:运维规范

1
2
3
4
5
6
7
8
9
✅ 设置 maxmemory(防止内存用尽)
✅ 设置内存淘汰策略(allkeys-lru 或 volatile-lru)
✅ 开启 AOF 持久化(appendonly yes
✅ 开启混合持久化(aof-use-rdb-preamble yes
✅ 开启 lazyfree(异步删除大 Key)
✅ 配置 slowlog(监控慢查询)
✅ 定期备份 RDB 文件(防止数据丢失)
❌ 不要暴露 Redis 到公网(安全问题)
❌ 不要用 ROOT 运行 Redis(安全问题)

面试加分回答

「Redis 的使用规范是生产环境稳定性的保障。除了上述规范,还有一个重要的点:监控。生产环境一定要监控 Redis 的 used_memory(内存使用)、ops_per_sec(QPS)、connected_clients(连接数)、rejected_connections(拒绝连接数)、expired_keys(过期 Key 数)、evicted_keys(淘汰 Key 数)、keyspace_hits/misses(缓存命中率)。可以用 Prometheus + Grafana 监控,也可以用云服务商提供的监控(阿里云/腾讯云)。另外,Redis 的 mem_fragmentation_ratio(内存碎片率)也是一个重要指标,如果 > 1.5,说明碎片严重,需要开启内存碎片整理。」


第 50 题:Redis 面试题终极总结与复习路线

一句话结论

Redis 面试的核心:数据结构与底层实现、持久化、内存管理、缓存异常、分布式锁、高可用(主从/Sentinel/Cluster)、性能优化。按照「基础 → 原理 → 实战」的顺序复习。


深度解析

Redis 面试知识图谱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Redis 面试
├── 基础篇(必会)
│ ├── Redis 是什么?为什么快?
│ ├── 5 种基本数据结构(String/Hash/List/Set/ZSet)
│ ├── 3 种特殊数据结构(Bitmap/HyperLogLog/Geo)
│ └── 常用命令(SET/GET/HSET/HGET/LPUSH/LRANGE/SADD/ZADD...)

├── 原理篇(重点)
│ ├── SDS vs C 字符串
│ ├── 跳表 vs 红黑树
│ ├── 渐进式 rehash
│ ├── 持久化(RDB vs AOF,混合持久化)
│ ├── 过期策略(惰性删除 + 定期删除)
│ ├── 内存淘汰策略(LRU/LFU)
│ └── 单线程模型 + 6.0 多线程

├── 缓存篇(高频)
│ ├── 缓存雪崩、穿透、击穿
│ ├── 缓存与数据库一致性(Cache Aside Pattern)
│ ├── 缓存读写策略(Cache Aside、Read/Write ThroughWrite Behind
│ ├── 热 Key 问题
│ └── 大 Key 问题

├── 分布式篇(难点)
│ ├── 分布式锁(SETNX + Lua 脚本 + Redisson 看门狗)
│ ├── RedLock 算法(以及 Martin Kleppmann 的批评)
│ ├── 主从复制(全量同步 + 增量同步)
│ ├── Sentinel(监控 + 故障转移)
│ └── Cluster(数据分片 + 槽迁移)

└── 实战篇(加分)
├── Pipeline vs MGET/MSET
├── Lua 脚本(原子操作)
├── Stream(消息队列)
├── 慢查询排查
├── 内存优化
├── 安全加固
└── 秒杀系统实战

复习路线(按优先级):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1 周:基础篇 + 原理篇
→ 把每个数据结构都自己操作一遍
→ 理解 SDS、跳表、渐进式 rehash 的底层实现
→ 理解 RDB、AOF、混合持久化的原理

2 周:缓存篇 + 分布式篇
→ 缓存雪崩/穿透/击穿的解决方案要能手写伪代码
→ 分布式锁的实现(SETNX + Lua + Redisson)要能讲清楚
→ 主从复制、Sentinel、Cluster 的区别和适用场景

3 周:实战篇 + 刷题
→ 搭一个本地 Redis,试一下 Sentinel 和 Cluster 模式
→ 用 Redis 实现一个秒杀系统(Lua 脚本 + Stream)
→ 刷 LeetCode 上和 Redis 相关的题(如 LRU Cache

面试加分回答

「Redis 的八股文虽然多,但核心就那几个:数据结构底层、持久化、缓存异常、分布式锁、Cluster。其他的知识点(如 7.0 新特性、Vector Similarity Search)是加分项,不是必会项。另外,Redis 的官方文档(redis.io)非常详细,建议把 Redis Design Patterns(redis.io/docs/reference/patterns)这一章看完,里面讲了 Redis 的常见使用模式和最佳实践,对面试和实战都很有帮助。最后,如果你能结合自己的项目经验讲 Redis(如『我们项目的缓存架构是…』、『我们遇到过热点 Key 问题,解决方案是…』),面试效果会好得多。」


总结:Redis 面试复习 Checklist

模块 题号 核心考点
基础与数据结构 1-8 Memcached vs Redis、线程模型、5 种数据结构、SDS、Hash 实现、跳表、持久化、内存淘汰
持久化与内存 9-13 AOF 重写、RDB vs AOF、过期策略、缓存雪崩/穿透/击穿、缓存一致性
高可用与分布式 14-21 分布式锁、RedLock、事务、Pipeline、Pub/Sub、主从复制、Sentinel、Cluster
实战与优化 22-30 热 key、大 key、慢查询、6.0 多线程、Stream、7.0 新特性、内存优化、安全、秒杀系统

最后的建议:
Redis 的八股文相对 MySQL 来说更「实用」,几乎每道题都能直接用到生产环境中。
建议把每道题都自己动手操作一遍(搭一个本地 Redis,试一下哨兵模式、Cluster 模式),
这样理解会深刻得多。
祝你面试顺利!🚀


“””
append_redis.py - Redis 补充题 31-40,追加到文件末尾
“””
import re

file_path = r”D:\Blog\my-blog\source_posts\2026-06-07-redis-interview-deep.md”

supplement = r

补充篇(续):高频遗漏题(39-50题)


第 39 题:Redis 后台线程(BIO)详细解析

一句话结论

Redis 有 3 个后台线程(Redis 6.0+),专门处理耗时的异步操作(如关闭大文件、AOF 刷盘、大 Key 异步删除),避免阻塞主线程。


深度解析

为什么需要后台线程?

1
2
3
4
5
6
7
8
9
Redis 主线程是单线程的,如果执行耗时操作:
→ 主线程阻塞
→ 所有其他请求都卡住
→ 延迟飙升,甚至超时

耗时操作举例:
① 关闭大文件描述符(如 RDB/AOF 临时文件)
AOF fsync 刷盘(同步等待磁盘写入)
③ 删除大 Key(如 100 万元的 Set

Redis 的 3 个后台线程(BIO:Background IO):

1
2
3
4
5
6
7
8
9
10
11
线程 1:BIO_CLOSE_FILE
→ 异步关闭文件描述符
→ 场景:RDB/AOF 重写完成后,删除旧文件

线程 2:BIO_AOF_FSYNC
→ 异步执行 AOF fsync
→ 场景:AOF 刷盘操作(配置 appendfsync always 时)

线程 3:BIO_LAZY_FREE
→ 异步释放大对象占用的内存
→ 场景:UNLINK、异步删除过期 Key、异步清空数据库

如何开启后台线程?

1
2
3
4
5
6
7
8
9
10
# redis.conf

# 开启惰性删除(后台线程删除大 Key)
lazyfree-lazy-eviction yes # 内存淘汰时,后台删除
lazyfree-lazy-expire yes # 过期 Key 删除时,后台删除
lazyfree-lazy-server-del yes # 服务端执行 DEL 时,后台删除

# 开启 AOF 后台 fsync
appendfsync always # 每条命令都 fsync(很慢,不推荐)
appendfsync everysec # 每秒 fsync 一次(推荐,由后台线程执行)

面试加分回答

「Redis 的后台线程是 Redis 6.0 之后才完善的。在 Redis 4.0 之前,删除大 Key 会阻塞主线程好几秒甚至几十秒,这是生产环境 Redis 卡顿的常见原因。Redis 4.0 引入了 UNLINK 命令(异步删除),Redis 6.0 之后把 AOF fsync 也放到了后台线程。生产环境一定要开启 lazyfree-* 系列配置,否则删除大 Key 会直接把 Redis 打挂。」


第 40 题:大量 Key 集中过期怎么办?

一句话结论

大量 Key 集中过期会导致:CPU 飙升、响应延迟、甚至超时。解决方案:过期时间加随机偏移 + 开启惰性删除 + 避免大批量 Key 同时设置过期。


深度解析

问题场景:

1
2
3
4
5
6
7
8
9
10
假设双十一 0 点整点秒杀:
→ 所有秒杀商品的缓存都设置 1 小时过期
1 小时后(1:00),所有商品缓存同时失效
→ 所有请求全部打到数据库
→ 数据库被打垮(缓存雪崩)

再如:
→ 每天 0 点定时任务生成 10 万个 Key,设置 24 小时过期
→ 第二天 0 点,10 万个 Key 同时过期
→ Redis CPU 飙升,响应变慢

Redis 定期删除的原理(为什么会 CPU 飙升):

1
2
3
4
5
6
7
8
9
10
11
12
Redis 的定期删除(Active Expire):
1. 随机抽取 20 个设置了过期时间的 Key
2. 删除其中已经过期的
3. 如果过期 Key 比例 > 25%,重复步骤 1
4. 每次执行时间有上限(25ms)

问题:
如果 10 万个 Key 同时过期:
→ 每次抽取 20 个,大部分都过期
→ 过期比例 > 25%,循环继续
→ CPU 一直被定期删除占用
→ 主线程响应变慢

解决方案:

方案 做法 适用场景
过期时间加随机偏移 EXPIRE key (3600 + random(0, 300)) 预防集中过期(最常用)
开启惰性删除 lazyfree-lazy-expire yes 异步删除过期 Key,不阻塞主线程
分批设置过期 把 10 万个 Key 分成 10 批,每批间隔 1 分钟设置 定时任务生成 Key 的场景
使用滑动过期 每次访问时,延长过期时间(如 EXPIRE key 3600 热点数据(如用户 Session)
永不过期 + 后台更新 Key 不设过期,后台定时任务更新缓存 数据变更不频繁的场景

面试加分回答

「大量 Key 集中过期的本质是惊群效应(Thundering Herd)。除了上述方案,Redis 6.0 之后引入了 dynamic-hz(动态调整定期删除频率),会根据过期 Key 的数量动态调整 hz 参数(默认 10,最大 500),缓解集中过期的问题。生产环境建议开启 dynamic-hz yes(默认已开启)。」


第 41 题:Redis 内存碎片如何产生?如何解决?

一句话结论

内存碎片 = 内存分配器分配的内存 > 实际使用的内存。产生原因:频繁修改数据、键值对大小不一。解决方案:开启自动内存碎片整理(activedefrag)。


深度解析

内存碎片的产生:

1
2
3
4
5
6
7
8
9
10
11
12
例子:
Redis 需要存储一个 100 字节的对象
→ 内存分配器(jemalloc)分配 128 字节(2 的幂)
→ 实际只用了 100 字节
→ 浪费 28 字节(内存碎片)

再例如:
SET key1 100_bytes_value
SET key1 200_bytes_value # 修改后,需要重新分配内存
→ 原来的 100 字节被释放(但不一定能重新利用)
→ 新分配 256 字节
→ 碎片越来越多

查看内存碎片率:

1
2
3
4
5
6
7
8
9
10
redis-cli INFO memory

# 输出:
used_memory:1000000 # Redis 实际使用的内存(字节)
used_memory_rss:2000000 # 操作系统分配给 Redis 的物理内存(字节)
mem_fragmentation_ratio:2.00 # 内存碎片率 = used_memory_rss / used_memory

# 解读:
碎片率 > 1.5 → 碎片严重,需要整理
碎片率 < 1.0 → Redis 部分数据被 swapped 到磁盘(性能急剧下降)

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
方案 1:开启自动内存碎片整理(Redis 4.0+)★ 推荐
# redis.conf
activedefrag yes
active-defrag-ignore-bytes 100mb # 碎片超过 100MB 才开始整理
active-defrag-threshold-lower 10 # 碎片率 > 10% 才开始整理
active-defrag-threshold-upper 100 # 碎片率 > 100% 时,全力整理
active-defrag-cycle-min 1 # 整理占用 CPU 最小百分比
active-defrag-cycle-max 25 # 整理占用 CPU 最大百分比

方案 2:重启 Redis(简单粗暴)
→ 重启后,内存重新分配,碎片消失
→ 缺点:需要故障转移(哨兵/集群),否则服务中断

方案 3:优化数据结构
→ 尽量用 Hash/Set 的 ziplist/intset 编码(内存紧凑)
→ 避免频繁修改大对象

面试加分回答

「内存碎片是 Redis 生产环境的常见问题。Redis 4.0 之前,解决碎片只能重启;Redis 4.0 引入了主动碎片整理(Active Defragmentation),可以在不阻塞主线程的情况下整理内存碎片。原理是:后台线程扫描内存,把相邻的、可被重新利用的内存碎片合并。生产环境建议开启 activedefrag yes,并合理设置 active-defrag-threshold-lower(默认 10,表示碎片率 > 1.1 时开始整理)。」


第 42 题:Redis Module 是什么?有哪些常用的 Module?

一句话结论

Redis Module = Redis 的扩展机制(4.0+),可以用 C/C++/Rust 编写自定义模块,扩展 Redis 的功能。常用 Module:RediSearch(搜索引擎)、RedisJSON(JSON 支持)、RedisGraph(图数据库)、RedisTimeSeries(时序数据)、RedisBloom(布隆过滤器)。


深度解析

为什么需要 Redis Module?

1
2
3
4
5
6
7
8
9
Redis 原生功能有限:
→ 不支持复杂查询(如全文搜索)
→ 不支持 JSON 文档(只能存 JSON 字符串)
→ 不支持图数据(如社交关系图)
→ 不支持时序数据(如监控指标)

Redis Module 解决了这些问题:
→ 可以加载第三方模块,扩展 Redis 功能
→ 模块运行在 Redis 进程内,性能极高

常用 Redis Module:

Module 功能 典型场景
RediSearch 全文搜索引擎(支持中文分词) 商品搜索、文章搜索
RedisJSON 原生支持 JSON 文档(可以查询/修改 JSON 字段) 存储复杂对象(不需要序列化)
RedisGraph 图数据库(支持 Cypher 查询语言) 社交关系、推荐系统
RedisTimeSeries 时序数据库(支持降采样、聚合查询) 监控指标、IoT 数据
RedisBloom 布隆过滤器(去重、判重) 缓存穿透防护、数据去重
RedisCell 分布式限流(基于令牌桶算法) API 限流
RedisAI 机器学习模型推理 实时推荐、图像识别

如何使用 Redis Module?

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 编译/下载 Module(.so 文件)

# 2. 在 redis.conf 中加载 Module
loadmodule /path/to/redisearch.so
loadmodule /path/to/rejson.so

# 3. 重启 Redis

# 4. 使用 Module 的命令
redis-cli
> JSON.SET doc $ '{"name":"Alice","age":25}'
> JSON.GET doc $.name

面试加分回答

「Redis Module 让 Redis 从『缓存』变成了『多功能数据库』。在生产环境中,RediSearchRedisJSON 最常用:RediSearch 可以实现毫秒级的全文搜索(替代 Elasticsearch 的简单场景);RedisJSON 可以原生存储 JSON 文档,并支持 JSONPath 查询(如 JSON.GET doc $.users[?(@.age > 18)])。另外,Redis 官方提供了 Redis Stack(集成了常用 Module 的安装包),开箱即用,不需要自己编译 Module。」


第 43 题:如何用 Redis 实现延时队列?

一句话结论

方案 1(推荐):Redis Stream + XPENDING(可靠,支持 ACK);(方案 2:Sorted Set + 时间戳,简单但不支持 ACK);(方案 3:Redisson 的延迟队列,生产级)。


深度解析

方案 1:Redis Stream(推荐)

1
2
3
4
5
6
7
8
9
10
11
# 生产者:发送延时消息
# 用 XADD 发送消息,score 设为「执行时间戳」
XADD delay_queue * task_id 1001 execute_at 1718000000

# 消费者:定时扫描到期消息
# 用 XRANGE 查询 execute_at <= 当前时间的消息
XRANGE delay_queue - + COUNT 100 # 需要配合 Lua 脚本原子执行

# 问题:
→ Stream 没有「按时间戳自动投递」的功能
→ 需要自己写定时任务扫描

方案 2:Sorted Set(简单但不支持 ACK)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生产者:发送延时消息
# member = 消息内容,score = 执行时间戳
ZADD delay_queue 1718000000 "send_email:user123"

# 消费者:定时扫描到期消息
# 用 ZRANGEBYSCORE 查询 <= 当前时间的消息
ZRANGEBYSCORE delay_queue 0 1718000000

# 处理完后,从 Sorted Set 中删除
ZREM delay_queue "send_email:user123"

# 问题:
→ ZREM 和 ZRANGEBYSCORE 不是原子操作,可能重复消费
→ 需要用 Lua 脚本保证原子性
→ 不支持 ACK(消费者挂了,消息丢失)

方案 3:Redisson 的延迟队列(生产级)★ 推荐

1
2
3
4
5
6
7
8
9
// Redisson 内置了延迟队列实现(基于 Sorted Set)
RDelayedQueue<String> delayQueue = redissonClient.getDelayedQueue(blockingQueue);

// 发送延时消息(30 秒后执行)
delayQueue.offer("send_email:user123", 30, TimeUnit.SECONDS);

// 消费者
RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue("delay_queue");
String task = blockingQueue.take(); // 阻塞等待,30 秒后收到消息

Redisson 的延迟队列是生产级的:支持 ACK、支持消费者挂了重新投递、支持集群模式。


面试加分回答

「用 Redis 实现延时队列,如果是生产环境,强烈推荐用 Redisson 的延迟队列(Java)或 redis-py 的 BlPop 配合 Sorted Set(Python),不要自己造轮子。另外,如果延时任务量很大(如每天 1 亿个延时任务),或者需要严格的「至少一次投递」,建议用专业的消息队列(如 RocketMQ 的延时消息、RabbitMQ 的 TTL+死信队列),Redis 的延时队列适合「任务量不大、允许极小概率丢失」的场景。」


第 44 题:Redis 7.2 新特性:向量相似度搜索(Vector Similarity Search)

一句话结论

Redis 7.2 引入了向量相似度搜索(VSS),可以直接在 Redis 中存储向量、搜索相似向量,无需部署专用向量数据库(如 Milvus、Pinecone),适合 RAG(检索增强生成)场景。


深度解析

为什么需要向量搜索?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AI 应用场景(如 ChatGPT + 知识库):
→ 用户提问:"如何学习 Redis?"
→ 把问题转换成向量(embedding)
→ 在知识库中搜索「语义相似」的文档
→ 把相关文档作为上下文,发给 LLM 生成答案

传统数据库(MySQL/Redis)的问题:
→ 只能做「精确匹配」(WHERE content LIKE '%Redis%'
→ 不能做「语义相似度搜索」

向量数据库(如 Milvus、Pinecone):
→ 可以存储向量(如 1024 维浮点数)
→ 可以搜索「最相似的 K 个向量」(KNN 搜索)
→ 但需要单独部署和维护

Redis 7.2 的向量搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 创建向量索引
FT.CREATE vector_index ON HASH PREFIX 1 doc:
SCHEMA title TEXT
embedding VECTOR HNSW 6 DIM 1024 DISTANCE_METRIC COSINE

# 解释:
FT.CREATE → 创建全文索引(RediSearch Module)
vector_index → 索引名称
embedding → 向量字段名
VECTOR HNSW 6 → 用 HNSW 算法(高效近似最近邻搜索)
DIM 1024 → 向量维度(1024 维)
DISTANCE_METRIC COSINE → 相似度度量(余弦相似度)

# 2. 插入向量数据
HSET doc:1 title "Redis 教程" embedding "[0.1,0.2,0.3,...,0.5]"

# 3. 搜索相似向量
FT.SEARCH vector_index
"*=>[KNN 5 @embedding $query_vector AS score]"
PARAMS 2 query_vector "[0.15,0.25,0.35,...,0.55]"
DIALECT 2

# 返回:最相似的 5 个文档

Redis 向量搜索 vs 专业向量数据库:

对比项 Redis 7.2 VSS Milvus / Pinecone
部署复杂度 低(复用现有 Redis) 高(需要单独部署)
性能 中等 高(专为向量搜索优化)
功能完整度 基础(HNSW 索引) 完整(支持多种索引、过滤)
适用场景 小规模向量搜索(< 1 亿向量) 大规模向量搜索(> 1 亿向量)

面试加分回答

「Redis 7.2 的向量搜索是一个很新的特性(2023 年发布),让 Redis 可以直接用于 RAG(检索增强生成)场景,不需要单独部署向量数据库。在生产环境中,如果向量数据量不大(< 1000 万),用 Redis 做向量搜索是完全够用的;如果向量数据量很大(> 1 亿),或者需要复杂的过滤条件(如「只搜索最近 7 天的文档」),建议用专业的向量数据库(如 Milvus)。另外,Redis 的向量搜索需要安装 RediSearch Module,社区版 Redis 默认不包含。」


第 45 题:Redis 配置优化和性能调优

一句话结论

Redis 性能调优 = 合理设置 maxmemory + 选择合适的内存淘汰策略 + 开启 lazyfree + 调整 TCP backlog + 使用 Pipeline/UNLINK。


深度解析

关键配置参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# redis.conf

# 1. 内存上限(必须设置!)
maxmemory 4gb
maxmemory-policy allkeys-lru # 内存满时,LRU 淘汰

# 2. 开启惰性删除(避免大 Key 删除阻塞)
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes

# 3. TCP 优化(高并发场景)
tcp-backlog 2048 # TCP 连接队列长度(默认 511)
timeout 0 # 客户端空闲超时(0 = 不超时)
tcp-keepalive 300 # TCP keepalive(秒)

# 4. 持久化优化
appendonly yes
appendfsync everysec # 每秒 fsync(平衡性能和安全性)
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 5. 慢查询阈值(记录执行时间 > 10ms 的命令)
slowlog-log-slower-than 10000
slowlog-max-len 128

# 6. 客户端输出缓冲区限制(防止客户端消费慢,导致 Redis 内存暴涨)
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

性能调优清单:

优化项 做法 效果
使用 Pipeline 批量发送命令(减少 RTT) 性能提升 5~10 倍
使用 UNLINK 代替 DEL 异步删除大 Key 避免主线程阻塞
避免大 Key 拆分 > 10KB 的 String,> 5000 个元素的 Set/Hash/ZSet 避免阻塞
避免热 Key 本地缓存 + Key 副本 分散压力
合理设置过期时间 加随机偏移,避免集中过期 避免 CPU 飙升
使用 Hash/Set 的 ziplist/intset 编码 控制 hash-max-ziplist-entries(默认 512) 节省内存
使用连接池 避免频繁创建/销毁连接 减少延迟

面试加分回答

「Redis 性能调优是生产环境的核心技能。除了上述配置优化,还有一个很重要的点:监控 Redis 的关键指标(如 QPS、内存使用率、键空间命中率、慢查询数量)。推荐用 Prometheus + Grafana 监控 Redis,或者直接用云服务商(阿里云/腾讯云)的 Redis 监控。另外,Redis 7.0 引入了 list-max-listpack-size 等配置,进一步优化了内存使用,升级到 Redis 7.0+ 也是一个很好的性能优化手段。」


第 46 题:Redis 监控和运维实践

一句话结论

Redis 监控 = INFO 命令(内置) + slowlog(慢查询) + redis-cli –bigkeys/–hotkeys(大 Key/热 Key) + 第三方监控(Prometheus + Grafana)。


深度解析

内置监控命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1. INFO 命令(查看 Redis 状态)
redis-cli INFO
redis-cli INFO memory # 只看内存相关
redis-cli INFO stats # 只看统计信息
redis-cli INFO replication # 只看主从复制信息

# 关键指标:
used_memory_human → Redis 实际使用的内存
mem_fragmentation_ratio → 内存碎片率
connected_clients → 当前连接的客户端数
instantaneous_ops_per_sec → 实时 QPS
keyspace_hits → 键空间命中次数
keyspace_misses → 键空间未命中次数
expired_keys → 过期 Key 数量
evicted_keys → 被淘汰的 Key 数量(内存满时)

# 2. slowlog(慢查询)
redis-cli SLOWLOG GET 10 # 查看最近 10 条慢查询
redis-cli SLOWLOG LEN # 慢查询数量
redis-cli SLOWLOG RESET # 清空慢查询日志

# 3. 大 Key 和热 Key
redis-cli --bigkeys # 扫描大 Key(影响性能,慎用)
redis-cli --hotkeys # 扫描热 Key(Redis 4.0+)

第三方监控方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
方案 1:Prometheus + Grafana(推荐)
→ 用 redis_exporter 采集 Redis 指标
→ Prometheus 存储指标
→ Grafana 展示仪表盘(Dashboard)
→ 支持告警(如内存使用率 > 80% 告警)

方案 2:云服务商的监控(最省心)
→ 阿里云/腾讯云的 Redis 服务内置监控
→ 自动采集关键指标,展示仪表盘
→ 支持告警(短信/电话/邮件)

方案 3:ELK(Elasticsearch + Logstash + Kibana)
→ 收集 Redis 慢查询日志
→ 分析慢查询趋势

Redis 运维 checklist:

1
2
3
4
5
6
7
1. 监控关键指标(内存使用率、QPS、键空间命中率、慢查询数量)
2. 定期扫描大 Key 和热 Key(每周一次)
3. 定期检查主从复制延迟(replication lag
4. 定期备份 RDB 文件(冷备)
5. 定期清理过期 Key(避免大量 Key 集中过期)
6. 压力测试(用 redis-benchmark)
7. 高可用测试(手动故障转移,验证 Sentinel/Cluster 是否正常工作)

面试加分回答

「Redis 监控是生产环境必不可少的。除了上述方案,Redis 6.0 引入了 ACL LOG 命令(查看 ACL 违规日志),Redis 7.0 引入了 LATENCY HISTORY 命令(查看延迟事件历史),这些都是排查 Redis 性能问题的利器。另外,推荐读一下 Redis 官方的 Redis Latency Problems Troubleshooting 文档,里面详细讲解了如何排查 Redis 延迟问题(如网络延迟、命令执行延迟、持久化延迟等)。」


第 47 题:Redis 6.0 ACL(访问控制列表)详解

一句话结论

Redis 6.0 引入了 ACL(Access Control List),可以创建多个用户,为每个用户设置不同的密码、命令权限、Key 访问权限,替代原来的单用户 + requirepass 模式。


深度解析

为什么需要 ACL?

1
2
3
4
5
6
7
8
原来的 Redis 认证(6.0 之前):
→ 只有一个「用户」(default
→ 要么可以执行所有命令,要么什么都不行
→ 无法精细控制权限

问题场景:
→ 应用程序只需要读缓存,但因为只有一个用户,不得不给写权限
→ 某个恶意用户拿到了 Redis 密码,可以执行 FLUSHALL(清空所有数据)

ACL 基本命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 创建用户
ACL SETUSER alice on >password ~cached:* +GET +SET

# 解释:
alice → 用户名
on → 启用这个用户
>password → 设置密码
~cached:* → 只能访问 cached:* 开头的 Key
+GET +SET → 只能执行 GET 和 SET 命令

# 2. 查看用户权限
ACL LIST # 列出所有用户
ACL USERS # 列出所有用户名
ACL WHOAMI # 查看当前用户
ACL CAT # 列出所有命令类别

# 3. 修改用户权限
ACL SETUSER alice +DEL # 给 alice 增加 DEL 权限
ACL SETUSER alice -SET # 禁止 alice 执行 SET

# 4. 删除用户
ACL DELUSER alice

生产环境 ACL 配置建议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户 1:app-readonly(应用程序只读用户)
→ 只能执行 GET、MGET、EXISTS
→ 只能访问 app:* 开头的 Key
→ 防止应用程序误删缓存

用户 2:app-write(应用程序写用户)
→ 可以执行 GETSET、DEL、EXPIRE
→ 只能访问 app:* 开头的 Key

用户 3:monitor(监控用户)
→ 只能执行 INFO、SLOWLOG GET
→ 不能访问任何 Key(只读监控)

用户 4:admin(管理员用户)
→ 可以执行所有命令
→ 可以访问所有 Key
→ 只允许从内网 IP 连接

面试加分回答

「Redis 6.0 的 ACL 是生产环境安全加固的重要手段。除了 ACL,还可以用 rename-command 重命名/禁用危险命令(如 FLUSHALL、KEYS、SHUTDOWN),双重保险。另外,Redis 的 ACL 支持 外部认证(如 LDAP、OAuth2),适合企业级场景(如用公司的统一认证登录 Redis)。配置方式:在 redis.conf 中设置 aclfile /path/to/users.acl,把 ACL 规则保存到文件(而不是写在 redis.conf 中),方便管理。」


第 48 题:Redis 7.0 新特性详解

一句话结论

Redis 7.0 的重要新特性:Function(替代 Lua 脚本)、ACL 增强、Listpack(替代 Ziplist)、改进的 Vector Set(向量集合)。


深度解析

新特性 ①:Function(函数)

1
2
3
4
5
6
7
8
9
问题:Lua 脚本的缺点
→ Lua 脚本不会持久化(重启后丢失)
→ 主从复制时,Lua 脚本需要重新传输
→ 无法管理(列出所有已加载的 Lua 脚本)

Function 解决了这些问题:
Function 会持久化到 RDB/AOF
→ 主从复制时,自动同步 Function
可以用 FUNCTION LIST 查看所有 Function
1
2
3
4
# 加载 Function
FUNCTION LOAD "#!lua name=mylib\n redis.call('SET', KEYS[1], ARGV[1])"
# 调用 Function
FCALL mylib 1 mykey myvalue

新特性 ②:Listpack(替代 Ziplist)

1
2
3
4
5
6
7
8
Ziplist 的问题:
→ 修改中间元素时,需要重新分配整个 ZiplistO(N)
→ 连锁更新(Cascade Update)问题

Listpack 的改进:
→ 每个元素独立存储长度信息(不需要反向遍历)
→ 修改中间元素时,不需要重新分配整个 Listpack
→ 解决了连锁更新问题

新特性 ③:改进的 ACL

1
2
3
Redis 7.0 的 ACL 支持:
→ channel 权限(可以限制 Pub/Sub 的频道访问)
→ 更细粒度的命令权限(可以按命令类别授权)

面试加分回答

「Redis 7.0 的 Listpack 是一个很重要的优化,解决了 Ziplist 的连锁更新问题。在生产环境中,如果用到 Hash/List/ZSet,且元素数 < 512,Redis 会自动用 Listpack 编码(替代 Ziplist),内存更省、性能更好。另外,Redis 7.2(最新版本)引入了 Vector Similarity Search(向量相似度搜索),让 Redis 可以直接用于 AI 应用(如 RAG),不需要单独部署向量数据库。」


第 49 题:Redis 主从复制的优化和调优

一句话结论

主从复制优化 = 合理配置 replication backlog size + 开启无磁盘复制(diskless replication) + 监控复制延迟 + 避免主节点写入过大。


深度解析

关键配置参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# redis.conf(主节点)

# 1. 复制积压缓冲区大小(很重要!)
repl-backlog-size 100mb # 默认 1MB,建议设为 100MB 或更大
# 作用:从节点断线重连后,如果偏移量还在 backlog 中,只做部分重同步

# 2. 无磁盘复制(diskless replication)
repl-diskless-sync yes # 主节点直接通过网络发送 RDB,不写磁盘
repl-diskless-sync-delay 5 # 等待 5 秒,让更多从节点连接
# 适用场景:主节点磁盘很慢,但从节点网络很好

# 3. 复制缓冲区大小(客户端输出缓冲区)
client-output-buffer-limit replica 256mb 64mb 60
# 作用:从节点复制时,主节点的输出缓冲区大小限制

# redis.conf(从节点)

# 4. 复制超时时间
repl-timeout 60 # 默认 60 秒

# 5. 是否开启只读(从节点默认只读)
replica-read-only yes

监控主从复制延迟:

1
2
3
4
5
6
7
8
9
10
11
# 在主节点执行
redis-cli INFO replication

# 输出:
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=1000000,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=999000,lag=2

# 关键指标:
offset → 从节点的复制偏移量(和主节点越接近,延迟越小)
lag → 延迟(秒)

主从复制延迟的优化:

1
2
3
4
5
6
7
8
9
10
问题:主从复制延迟太大(如 lag > 10 秒)
→ 从节点读到的数据太旧
→ 业务可能读到脏数据

解决方案:
① 提升网络带宽(主从节点之间用万兆网卡)
② 减少主节点的写入量(用 Pipeline 批量写入)
③ 增加 repl-backlog-size(避免从节点断线后全量同步)
④ 用 WAIT 命令(阻塞等待指定数量的从节点确认收到写命令)
WAIT 1 5000 → 等待至少 1 个从节点确认,最多等 5

面试加分回答

「主从复制延迟是 Redis 生产环境的常见问题。如果业务需要『写完立即读到』(强一致性),可以用 WAIT 命令(阻塞等待从节点确认),但会牺牲写性能。另外,Redis 的复制是异步的,所以主从节点之间的数据一定是有延迟的,这是 CAP 定理决定的(Redis 选择 AP,而不是 CP)。如果一定要强一致性,应该用 Redis Raft(基于 Raft 协议的强一致 Redis),或者用 ZooKeeper/etcd 做分布式锁。」


第 50 题:Redis Cluster 的运维和故障处理

一句话结论

Redis Cluster 运维 = 合理分配 Slot + 监控节点状态 + 处理 Slot 迁移 + 处理节点故障 + 定期备份。


深度解析

Slot 分配原则:

1
2
3
4
5
6
7
8
Redis Cluster16384Slot
→ 每个 Slot 必须分配给一个主节点
→ 每个主节点可以有多个从节点(高可用)
Slot 分配要均匀(每个主节点约 16384 / NSlot

查看 Slot 分配:
redis-cli CLUSTER SLOTS
redis-cli CLUSTER NODES

Slot 迁移(扩容/缩容):

1
2
3
4
5
6
7
8
9
10
11
12
13
# 场景:扩容(新增一个主节点)

# 1. 新增主节点
redis-cli --cluster add-node 127.0.0.1:7004 127.0.0.1:7000

# 2. 迁移 Slot(从其他节点迁移 1000 个 Slot 到新节点)
redis-cli --cluster reshard 127.0.0.1:7000
--cluster-from <node-id-1>,<node-id-2>
--cluster-to <new-node-id>
--cluster-slots 1000

# 3. 新增从节点(高可用)
redis-cli --cluster add-node 127.0.0.1:7005 127.0.0.1:7000 --cluster-slave --cluster-master-id <new-node-id>

故障处理:

1
2
3
4
5
6
7
8
9
10
11
12
场景 1:从节点故障
→ 不影响服务(主节点仍然工作)
→ 修复后,重新启动从节点,它会自动同步主节点数据

场景 2:主节点故障
→ Sentinel 会自动选举一个从节点晋升为主节点
→ 故障转移时间:通常 30 秒 ~ 1 分钟
→ 期间写请求会失败(需要客户端重试)

场景 3:多个主节点故障
→ 如果超过半数的主节点故障,整个集群不可用
→ 需要手动干预(恢复故障节点)

Redis Cluster 的注意事项:

1
2
3
4
5
6
7
8
① 不支持多 KeySlot 操作
MSET key1 key2(如果 key1key2 在不同 Slot,会报错)
→ 解决:用 Hash Tag{user123}name{user123}age 在同一个 Slot

② 迁移 Slot 期间,部分 Key 可能访问失败(MOVED / ASK 重定向)
→ 客户端需要支持重定向(如 JedisCluster

③ 集群模式下,MSET/MGET 需要客户端支持(分别向多个节点发送命令)

面试加分回答

「Redis Cluster 的 Slot 迁移是一个『高危操作』,迁移期间会导致部分 Key 访问失败(ASK 重定向)。在生产环境中,迁移 Slot 一定要在业务低峰期进行,并且提前通知业务方。另外,Redis Cluster 的客户端需要支持智能路由(如 JedisCluster、redis-py-cluster),否则每次都要重定向,性能很差。如果觉得 Redis Cluster 太复杂,可以考虑用 Codis(代理模式,客户端不需要支持 Cluster 协议),或者用云服务商提供的 Redis 集群服务(如阿里云的 ApsaraDB for Redis)。」


“””

补充篇:BAT 高频遗漏题(31-40题)

第 31 题:Redis 缓存读写策略有哪几种?

一句话总结:三种策略——Cache Aside(最常用)、Read/Write ThroughWrite Behind;互联网项目默认用 Cache Aside。

深度解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
① Cache Aside(旁路缓存,最常用)
→ 读:先读缓存,没命中读 DB,再写缓存
→ 写:先更新 DB,再删除缓存(不是更新缓存!)
→ 优点:简单,一致性较好
→ 缺点:有短暂不一致窗口

Read/Write Through(读写穿透)
→ 应用只操作缓存,缓存负责同步 DB
→ 优点:一致性好
→ 缺点:缓存层复杂(需要自己实现)

③ Write Behind(异步回写)
→ 写操作只写缓存,异步批量写 DB
→ 优点:写性能极高
→ 缺点:可能丢数据(缓存宕机)

面试加分回答

“我们项目用 Cache Aside 策略。写操作时选择’删除缓存’而不是’更新缓存’,原因是:① 如果写操作频繁,更新缓存开销大 ② 并发写场景下,更新缓存可能有顺序问题(线程A先更新DB,线程B后更新DB,但缓存先写了B再写了A,导致不一致)。删除缓存让下次读时自动加载最新值,更简单可靠。”


第 32 题:Redis 的 String 和 Hash 存储对象哪个更好?

一句话总结单个对象用 Hash 更省内存(Redis 会编码为 ziplist),需要整体操作或用 TTL 控制过期时用 String

深度解析

1
2
3
4
5
6
7
8
9
方案A:String 存储序列化对象
SET user:1 "{name:'Alice',age:20}"
→ 优点:可以整体设置 TTL
→ 缺点:更新一个字段需要反序列化整个对象,浪费

方案B:Hash 存储对象字段
HSET user:1 name "Alice" age 20
→ 优点:可以单独更新某个字段(不用反序列化整个对象)
→ 缺点:Hash 的 TTL 是整个 Key 的,不能给单个字段设过期

内存对比(重要!):

1
2
3
4
5
6
7
String 存储序列化对象(如 JSON):
→ 每个 Key 都有完整的元数据(类型、LRU、哈希表指针等,约 90 字节)
10 万个对象 = 约 9 MB 元数据开销!

Hash 存储(当字段数 < hash-max-ziplist-entries 时):
→ 编码为 ziplist(紧凑型编码),非常省内存
10 万个对象可能只占用 1/3 的内存

面试加分回答

“我们存储用户信息时,如果用户对象字段不多(<10个),用 Hash 存储(利用 ziplist 编码省内存);如果用户对象需要设置不同的 TTL,或者需要整体 GET/SET,用 String 存储 JSON。另外,用 Hash 存储时要注意配置 hash-max-ziplist-entries(默认 512),超过这个阈值会转为 hashtable 编码,内存优势就没了。”


第 33 题:Redis 的跳表(Skip List)为什么用跳表而不用红黑树?

一句话总结:跳表和红黑树时间复杂度相同(O(log n)),但跳表实现更简单、范围查询更高效、支持并发修改更容易

深度解析

对比项 跳表(Skip List) 红黑树(Red-Black Tree)
实现难度 简单(类似多层链表) 复杂(旋转操作很难写对)
范围查询 高效(找到起点后顺序遍历) 需要中序遍历,较复杂
并发修改 相对容易(可以无锁化) 很难(旋转操作需要全局锁)
内存占用 稍高(多层指针) 稍低

跳表的原理(用生活比喻):

1
2
3
4
5
6
7
8
9
10
11
12
跳表 = 多层链表
→ 第 0 层:完整链表(所有元素)
→ 第 1 层:约 1/2 元素("快线"
→ 第 2 层:约 1/4 元素("更快线"
→ ...

查找时:
从最上层开始,向右走(如果下一个元素 <= 目标,继续向右)
如果不行了,往下走一层
重复,直到第 0

平均时间复杂度:O(log n)(和二分查找一样快!)

面试加分回答

“Redis 的有序集合(Sorted Set)底层用跳表而不用红黑树,核心原因是:① 跳表的 range 查询(ZRANGE、ZRANGEBYSCORE)效率极高,找到起点后顺序遍历即可;红黑树做范围查询需要中序遍历,实现复杂。② 跳表的代码实现比红黑树简单太多,不容易出 Bug。③ 跳表支持无锁并发修改(虽然 Redis 单线程不需要),扩展性更好。”


第 34 题:Redis 的 Bitmap 和 HyperLogLog 实战场景?

一句话总结Bitmap 适合「是/否」类统计(如用户签到、活跃状态),HyperLogLog 适合「UV 去重估算」(误差约 0.81%,但只需 12 KB 内存)。

深度解析

Bitmap 实战:用户签到

1
2
3
4
5
6
7
8
-- 用户 1001 在 2026-06-08 签到
SETBIT sign:2026:06:user:1001 8 1

-- 检查用户 1001 是否在 2026-06-08 签到过
GETBIT sign:2026:06:user:1001 8

-- 统计 2026-06 全月签到人数
BITCOUNT sign:2026:06:user:1001

HyperLogLog 实战:页面 UV 统计

1
2
3
4
5
6
7
8
-- 记录用户 1001 访问了首页
PFADD uv:home:2026-06-08 "user:1001"

-- 估算今天首页的 UV(去重后)
PFCOUNT uv:home:2026-06-08

-- 合并多天 UV
PFMERGE uv:home:2026-06-week uv:home:2026-06-08 uv:home:2026-06-09 ...

面试加分回答

“我们用 HyperLogLog 做日活/月活统计,12 KB 内存可以统计 2^64 个不同值,误差约 0.81%,对于 UV 统计完全够用。如果用 Set 存储用户 ID 做去重,100 万 UV 需要约 80 MB 内存,HyperLogLog 只要 12 KB,差距 6000 倍!但要注意 HyperLogLog 只能估算去重数,不能获取’哪些用户来访过’(这个信息已经丢失了)。”


第 35 题:Redis 的大 Key 问题怎么解决?

一句话总结:大 Key(String > 10 KB,Hash/Set/ZSet > 5000 个元素)会导致阻塞、内存不均、迁移慢;解决方案:拆分 Key定期清理使用 iScan 渐进删除

深度解析

大 Key 的危害

1
2
3
4
5
6
7
8
9
危害 1:阻塞
→ DEL 大 Key 是同步的,会阻塞 Redis(毫秒级甚至秒级)
→ 特别是网络闪断后重连,Redis 卡死

危害 2:内存不均
→ 集群模式下,大 Key 会导致某些分片内存远大于其他分片

危害 3:迁移慢
→ 集群扩容时,大 Key 迁移非常慢

解决方案

方案 做法 适用场景
拆分 Key user:1001:friends(1 万个好友)拆成 user:1001:friends:0user:1001:friends:1(每批 1000 个) 预防大 Key
定期清理 iScan 渐进式扫描,发现大 Key 后拆分或清理 治理已有大 Key
渐进删除 不用 DEL,用 UNINK(异步删除)或分批 HDEL 删除大 Key
监控告警 redis-cli --bigkeys 定期扫描,超过阈值告警 预防

面试加分回答

“我们生产环境用 redis-cli --bigkeys 每周扫描一次,发现大 Key 后:① 如果是 List/Set/Hash,拆分成多个小 Key(按 hash(key) % N 分片)② 删除大 Key 不用 DEL,用 UNLINK(Redis 4.0+,异步删除,不阻塞主线程)③ 如果是需要保留的大 Key,用 SCAN + HSCAN/ZSCAN 分批操作,避免一次性操作整个大 Key。”


第 36 题:Redis 的热 Key 问题怎么解决?

一句话总结:热 Key(如秒杀商品、顶流网红)会导致单节点 CPU 打满;解决方案:本地缓存(一级缓存) + Key 副本(分散到多个节点)

深度解析

热 Key 的危害

1
2
3
4
场景:秒杀商品 ID=100110000 QPS 都读 `product:1001`
→ 所有读请求都打到同一个 Redis 分片
→ 该分片 CPU 100%,其他分片闲置
→ 整个 Redis 集群的吞吐量被一个 Key 拖垮

解决方案 1:本地缓存(一级缓存)

1
2
3
4
5
6
7
8
9
// 用 Caffeine 做本地缓存(JVM 堆内缓存)
LoadingCache<String, Product> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.SECONDS) // 本地缓存只存 10 秒
.build(key -> jedis.get(key)); // 未命中才读 Redis

// 效果:
// 10000 QPS 中,本地缓存命中 9900 QPS
// Redis 实际只用承受 100 QPS(从同一个 Key 变成了 100 QPS,压力骤减)

解决方案 2:Key 副本(分散到多个节点)

1
2
3
4
5
6
7
8
原来:所有请求读 `product:1001`(都打到同一个分片)

改成:请求读 `product:1001:{0~9}` 中的一个副本
→ 应用层随机选择副本:`product:1001:` + random(0,9)
10 个副本分散到不同的 Redis 分片
→ 热 Key 的读压力被分散到 10 个分片

注意:写操作时,要同时更新 10 个副本(或设置很短的 TTL,让副本自动过期后重新加载)

面试加分回答

“我们解决热 Key 的标准方案是:① 本地缓存(Caffeine)+ Redis 二级缓存,本地缓存设很短的 TTL(如 5~10 秒),既能大幅降低 Redis 压力,又能保证数据不会太旧 ② 如果本地缓存不够(如单机 QPS 极高),用 Key 副本方案,把热 Key 复制 N 份分散到不同分片。另外,Redis 6.0 的客户端缓存(Client-side caching)也是一个很好的方案,可以让客户端自动追踪 Key 的变化。”


第 37 题:Redis 6.0 的多线程是什么?和单线程冲突吗?

一句话总结:Redis 6.0 的多线程只用于网络 I/O(读/写 Socket)命令执行仍然是单线程的,所以不存在并发安全问题。

深度解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Redis 单线程的瓶颈在哪里?
→ 网络 I/O(读客户端请求、写响应给客户端)
→ 随着网络带宽提升,I/O 成为瓶颈
→ 但命令执行(CPU 操作)仍然很快

Redis 6.0 多线程模型:

[主线程] [I/O 线程池]
接收请求 → 分配给 I/O 线程 → 多个线程并行读 Socket(网络 I/O

解析完的请求交给主线程

[主线程] 单线程执行所有命令(保证原子性)

执行结果交给 I/O 线程

多个线程并行写 Socket(网络 I/O

响应返回给客户端

I/O 多线程:提升网络读写性能
→ 命令执行单线程:保证线程安全,不需要锁

面试加分回答

“Redis 6.0 的多线程是一个很大的性能提升(官方测试:单线程 10 万 QPS,多线程可以到 20~30 万 QPS),但很多人误以为 Redis 变成多线程了、会有并发问题——这是错的。命令执行仍然是单线程的,所以 TRANSACTION/MULTI/WATCH 的语义完全不变,不需要担心并发问题。开启方式:io-threads 4(建议设为 CPU 核数的 1/2 到 1 倍)。”


第 38 题:Redis 的后台线程(Bio 线程)是做什么的?

一句话总结:Redis 把慢操作(如关闭文件描述符、AOF 刷盘、大 Key 异步删除)放到后台线程执行,避免阻塞主线程

深度解析

为什么需要后台线程?

1
2
3
4
5
6
7
8
9
场景:DEL big_key(包含 100 万个元素的 Set
→ 同步删除需要释放 100 万个元素的内存
→ 这个过程需要几秒甚至几十秒
→ 主线程阻塞,所有其他请求都超时!

解决:后台线程(Bio 线程)
→ 主线程把"删除 big_key"这个任务丢给后台线程
→ 后台线程慢慢删,主线程继续处理其他请求
→ 不阻塞!

Redis 的后台线程有哪些?

1
2
3
4
5
6
7
8
Bio 线程(Background I/O threads):
→ 处理关闭文件描述符(close
→ 处理 AOF 刷盘(fsync
→ 处理大 Key 异步删除(UNLINK 命令)

注意:
Bio 线程默认 1 个(可以配置)
Bio 线程不是命令执行线程,只做"收尾工作"

面试加分回答

“Redis 的 UNLINK 命令(异步删除)就是交给后台线程执行的。我们用 UNLINK 代替 DEL 来删除大 Key,避免了主线程阻塞。另外,AOF 的 fsync 策略如果设为 always(每条命令都刷盘),性能会很差;设为 everysec(后台线程每秒 fsync 一次)是性能和可靠性的最佳平衡点,这也是默认配置。”


第 39 题:Redis 和数据库的数据一致性问题怎么解决?

一句话总结无法做到强一致,只能做到最终一致性;核心是先更新 DB,再删除缓存,并配合延迟双删消息队列重试

深度解析

先更新 DB 还是先删除缓存?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
方案A:先更新 DB,再删除缓存(推荐 ✅)
1. 更新 MySQL:UPDATE product SET stock = 99 WHERE id = 1001;
2. 删除缓存:DEL product:1001

问题:步骤 2 失败了(Redis 宕机了)怎么办?
→ 缓存是旧的,DB 是新的 → 不一致!

解决:消息队列重试
→ 把"删除缓存"封装成消息,发送到 MQ
→ 消费者不断重试,直到删除成功

方案B:先删除缓存,再更新 DB(不推荐 ❌)
1. 删除缓存:DEL product:1001;
2. 更新 MySQL:UPDATE product SET stock = 99 WHERE id = 1001;

问题:在步骤 1 和步骤 2 之间,如果有读请求进来:
→ 缓存没命中 → 读 DB(读到旧值)→ 把旧值写回缓存
→ 然后步骤 2 执行(更新 DB 为新值)
→ 缓存是旧的,DB 是新的 → 不一致!(而且这个不一致会一直存在,直到缓存过期)

延迟双删(进一步降低不一致概率):

1
2
3
4
5
6
7
8
9
步骤:
1. 删除缓存
2. 更新 DB
3. 延迟 N 毫秒(如 500 ms)
4. 再次删除缓存

目的:
→ 如果在步骤 2 和步骤 3 之间,有其他线程读了旧 DB 值并写回了缓存
→ 步骤 4 的第二次删除可以把脏缓存清理掉

面试加分回答

“我们保证最终一致性的方案是:① 先更新 DB,再删除缓存 ② 删除缓存失败时,把删除操作发送到消息队列,异步重试(最多重试 3 次,仍然失败就告警人工介入)③ 缓存设合理的 TTL,即使出现不一致,最多不一致 TTL 秒。另外,如果是要求强一致性的场景(如金融),不应该用缓存,直接读 DB。”


第 40 题:Redis 的部署架构(单机、主从、哨兵、集群)怎么选型?

一句话总结单机(测试)、主从(读写分离,不保证高可用)、哨兵(高可用,但性能有上限)、集群(水平扩展,生产环境首选)。

深度解析

架构 说明 优点 缺点 适用场景
单机 只有 1 个 Redis 实例 简单 单点故障,容量有限 开发/测试环境
主从复制 1 主 N 从,从库只读 读写分离,提升读性能 主库挂了需要手动切换 读多写少,能接受手动切换
哨兵(Sentinel) 主从 + 哨兵集群(监控+自动切换) 自动故障转移,高可用 写操作仍然只有 1 个主库(写性能上限) 高性能读,中等写
集群(Cluster) 数据分片,多个主库 水平扩展(写/读都能扩展) 不支持多 Key 事务、不支持 SELECT 大数据量、高并发 生产环境首选

面试加分回答

“我们生产环境用的是 Redis Cluster(3 主 3 从,共 6 个节点)。选择集群模式的原因:① 数据量超过单机内存(我们约 20 GB,单机 16 GB 内存不够)② QPS 超过单机上限(我们峰值 15 万 QPS,单机约 10 万 QPS 上限)③ 高可用要求:集群模式自动故障转移,不需要人工介入。另外,使用集群时要注意:mget/mset 如果 Key 不在同一个 slot,会报错,需要用 hash tag({user1001}:profile{user1001}:orders)强制放到同一个 slot。”


Redis 40 题完结!

如果觉得这篇文章对你有帮助,欢迎分享给更多的小伙伴!


Redis 面试八股文 30 道|深度详解版(傻子都能看懂)
https://whyalwaysme.lol/2026/06/07/2026-06-07-redis-interview-deep/
作者
Cassiur
发布于
2026年6月7日
许可协议