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

🗺️ 知识图谱

mindmap
  root((Redis))
    数据结构
      String SDS
      List quicklist
      Hash listpack 与 hashtable
      Set intset
      ZSet skiplist
      Bitmap HyperLogLog GEO
    持久化
      RDB 快照
      AOF 与重写
      混合持久化
    过期与淘汰
      惰性删除
      定期删除
      八种淘汰策略
    高可用
      主从复制
      哨兵 Sentinel
      Cluster 与槽位
    缓存问题
      穿透 布隆过滤器
      击穿 互斥锁
      雪崩 过期打散
      一致性 延迟双删
    单线程模型
      IO 多路复用
      Redis 6 多线程 IO

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 位的时钟(访问时更新)
淘汰时,随机采样 N 个 key(默认 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
假设有 10000 个 key,设置的过期时间完全相同。
→ 到期时,10000 个 key 同时失效!
→ 所有请求全部打到数据库!
→ 数据库瞬间被打垮(雪崩)

解决方案:

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 获取到了同一把锁
→ T1 和 T2 同时执行业务 → 并发问题!

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

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 分布式锁不是最佳选择,应该用 ZooKeeper 或 etcd 的分布式锁(基于临时顺序节点,可靠性更高)。」


第 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 在节点 A、B、C 上获取了锁,有效时间 10 秒
节点 C 的系统时钟发生了跳跃(如 NTP 同步导致时间突然前进)
→ 节点 C 上的锁「提前」过期了
客户端 C2 可以在节点 C、D、E 上获取同一把锁
→ 两个客户端同时持有锁!

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

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

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)。生产环境中,如果消息量很大或对可靠性要求很高,建议直接用 Kafka 或 RocketMQ,不要用 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 秒向 Master、Slave、其他 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 yes、lazyfree-lazy-expire yes、lazyfree-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(访问控制),可以禁止某些用户执行危险命令(如 KEYS、FLUSHALL),这是生产环境安全加固的重要手段。」


第 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 场景:
→ 读取 10MB 的 String 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 万),或者对延迟极其敏感,建议直接用 Kafka 或 RocketMQ,不要用 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
场景 1:Key 过期了,但内存没满
→ 过期键删除策略生效(惰性/定期删除)
→ 内存淘汰策略不触发

场景 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 之前)
→ 网络传输慢(GET 大 Key 占用带宽)
→ 迁移困难(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
架构:
→ 1 个 Master + N 个 Slave(主从复制)
→ 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 的槽迁移是一个复杂的话题。在迁移槽的过程中,会有 ASK 和 MOVED 重定向,客户端需要正确处理这两种重定向。另外,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
✅ 避免大 Value(String < 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 Through、Write 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 从『缓存』变成了『多功能数据库』。在生产环境中,RediSearch 和 RedisJSON 最常用: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(应用程序写用户)
→ 可以执行 GET、SET、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 的问题:
→ 修改中间元素时,需要重新分配整个 Ziplist(O(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 Cluster 有 16384 个 Slot:
→ 每个 Slot 必须分配给一个主节点
→ 每个主节点可以有多个从节点(高可用)
→ Slot 分配要均匀(每个主节点约 16384 / N 个 Slot)

查看 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
① 不支持多 Key 跨 Slot 操作
→ MSET key1 key2(如果 key1 和 key2 在不同 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 Through、Write 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:0、user: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=1001,10000 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日
许可协议