Redis 深度指南

从底层原理到生产实践 · 面试高频考点全面覆盖

💼 想要看真实工作场景?推荐搭配实战排查

本页侧重面试考点速答。若需看真实生产问题的排查过程和修复代码,请看以下实战页面:

🔧 数据库与缓存问题排查

1. Redis 为什么快

⚡ Redis 高性能六大支柱

内存操作

数据存储在内存,纳秒级访问,无需磁盘 I/O

单线程模型

无锁竞争、无上下文切换、无死锁风险

🔌

I/O 多路复用

epoll/kqueue 单线程处理海量并发连接

📈

高效数据结构

SDS、跳表、压缩列表等专为性能优化

💻

C 语言实现

接近操作系统层面,极低运行开销

📨

简单通信协议

RESP 协议解析快、易实现、开销小

💡 生活类比

MySQL = 去档案馆查资料:资料很全(持久化、事务、复杂查询),但每次都要走流程(磁盘 I/O、解析 SQL、锁竞争),慢但可靠。

Redis = 桌上贴的便签纸:随手就能看到(内存访问),内容精简(简单数据结构),但容量有限、可能丢失(内存限制、持久化风险)。

📊 性能对比:QPS 基准测试
MySQL
1%
~1,000
Redis
100x
~100,000

Redis 比 MySQL 快约 100 倍(简单 KV 操作场景)

单线程为什么还能这么快?

Redis 的瓶颈不在 CPU,而在内存和网络 I/O。单线程避免了锁竞争、上下文切换的开销,配合 I/O 多路复用(epoll),一个线程就能高效处理大量并发连接。Redis 6.0+ 引入多线程仅用于网络 I/O 读写,命令执行仍然是单线程。

📑 2. 底层数据结构 面试高频!

SDS (Simple Dynamic String) vs C String

C String

'R' 'e' 'd' 'i' 's' '\0'
  • ❌ 获取长度 O(N) — 需要遍历
  • ❌ 二进制不安全 — 遇到 '\0' 截断
  • ❌ 缓冲区溢出风险
  • ❌ 修改字符串需频繁 realloc

SDS

len: 5
free: 3
buf[]
  • ✅ 获取长度 O(1) — 直接读 len
  • ✅ 二进制安全 — 不依赖 '\0'
  • ✅ 空间预分配 — 减少 realloc
  • ✅ 惰性释放 — free 记录可用空间
ZipList (压缩列表) → ListPack
ZipList 内存布局
zlbytes
zltail
zllen
entry1
entry2
...
zlend

连续内存、紧凑存储,适合小数据量。元素过多时连锁更新风险。

Redis 7.0+ListPack 替代 ZipList。ListPack 去掉了 __prev_len 字段,彻底解决了连锁更新问题(Cascade Update),内存更紧凑、性能更稳定。

SkipList (跳表) — 核心数据结构
📈 跳表多级索引结构与查找路径
L3
HEAD
37
71
NIL
L2
HEAD
12
37
55
71
NIL
L1
HEAD
12
23
37
55
71
NIL
🔍 查找 55 的路径:HEAD → (L2) 跳到 12 → (L2) 跳到 37 → (L2) 跳到 55 ✅ — O(logN)

跳表 = 多层有序链表。通过随机层数实现概率平衡,查找/插入/删除 O(logN),范围查询极其高效。相比红黑树,实现简单且范围查询更友好。ZSet 的底层首选

HashTable 与 Rehash 过程
🔄 渐进式 Rehash 过程

ht[0] (旧表 size=4)

🟢 bucket 0: k1 → v1
⚪ bucket 1: (空)
🟢 bucket 2: k2 → v2 → k3 → v3
🟢 bucket 3: k4 → v4

ht[1] (新表 size=8)

🟢 bucket 0: k1 → v1
🟢 bucket 1: k3 → v3
⚪ bucket 2: (空)
🟢 bucket 3: k4 → v4
🟢 bucket 4: k2 → v2
🟠 bucket 5-7: rehash 进行中...

rehashidx 从 0 逐步推进,每次 CRUD 操作迁移一个 bucket,避免一次性迁移造成阻塞

used / size > load_factor(默认 1,BGSAVE 时为 5)时触发扩容。渐进式 rehash:不是一次性搬迁,而是分多次、在每次操作时顺带迁移一个 bucket,确保服务不中断。在此期间,查找会同时查 ht[0] 和 ht[1],新增只写 ht[1]。

IntSet 与 QuickList

IntSet 整数集合

  • 有序、紧凑的整数数组
  • 支持 int16 / int32 / int64 升级
  • 元素少且都是整数时 Set 的底层
  • 查找 O(logN)(二分),插入 O(N)

QuickList 快速列表

  • List 的底层实现
  • ZipList(或 ListPack)的双端链表
  • 兼顾内存紧凑与操作效率
  • 中间节点可被 LZF 压缩
📋 Redis 数据类型 → 底层结构映射表
数据类型 底层结构 适用条件
String SDS 所有字符串场景
Hash ListPack / HashTable field 数量少且值小 → ListPack;否则 → HashTable
List QuickList (ListPack + LinkedList) 所有 List 场景(早期用 ZipList + LinkedList)
Set IntSet / HashTable 全是整数且数量 ≤ 512 → IntSet;否则 → HashTable
ZSet ListPack / SkipList + HashTable 元素 ≤ 128 且每个 ≤ 64 字节 → ListPack;否则 → SkipList
🛠 生产实践

为什么理解底层结构对内存优化很重要?
知道 Hash 在小数据量下用 ListPack 可以指导你:将多个 key 合并为一个 Hash field,从 O(N) 个 SDS 头变为 1 个 ListPack,内存可节省 50%+。这也是 Redis Object encoding 自动转换的依据——理解阈值才能正确配置 hash-max-listpack-entrieszset-max-listpack-entries 等参数。

💾 3. 持久化机制

RDB 快照持久化
⏰ RDB 快照时间线

触发 BGSAVE

主进程 fork() 子进程,利用操作系统写时复制 (COW)

COW 机制工作

子进程访问 fork 时的内存快照;主进程修改时才复制对应页

生成 dump.rdb

子进程将内存数据以二进制压缩格式写入临时文件,完成后替换旧 RDB

加载恢复

Redis 重启时加载 dump.rdb,速度快(直接映射到内存)

AOF 追加日志持久化
📝 AOF 写入流程
1

Client 发送写命令

SET key value / HSET / LPUSH ...

2

追加到 AOF 缓冲区 (aof_buf)

命令以 RESP 协议格式写入内存缓冲区

3

fsync 写入磁盘

根据 appendfsync 策略决定何时刷盘

4

AOF 文件持久化

appendonly.aof — 记录所有写操作历史

appendfsync 策略安全性性能说明
always 最高 最慢 每条命令都 fsync,最多丢一条
everysec 较高 推荐 每秒 fsync 一次,最多丢 1 秒数据
no 最低 最快 交给 OS 调度刷盘,可能丢较多数据
AOF 重写 (Rewrite)
原始 AOF
10,000 条命令
BGREWRITEAOF
fork 子进程
精简 AOF
~200 条命令

AOF Rewrite 不是读取旧文件,而是直接遍历当前内存数据库状态,用最少的命令记录。例如对同一个 key 做 1000 次 SET,只需记录最终那一条。重写期间新命令写入 AOF 重写缓冲区,最后追加到新文件。

混合持久化 (RDB + AOF)
🎨 混合持久化文件结构 (aof-use-rdb-preamble yes)
RDB 格式头部
快速加载全量基础数据
AOF 格式尾部
记录增量写命令

兼顾 RDB 的加载速度和 AOF 的数据安全性

⚖ RDB vs AOF 全面对比
维度RDBAOF
持久化方式定时快照(二进制)追加写命令日志(文本)
数据安全性可能丢失数分钟最多丢 1 秒
文件大小紧凑(二进制压缩)较大(命令日志)
恢复速度极快较慢(回放命令)
对性能影响fork 时短暂影响持续写盘开销
适用场景冷备份、灾难恢复实时持久化、数据安全优先
📊 Excel 类比

RDB = 另存为 (Save As):每隔一段时间把当前文件状态完整存一份。恢复快,但可能丢两次存档之间的修改。

AOF = 修订记录 (Change History):每次修改都记一条操作日志。完整但文件大,需要定期"接受所有修订"(rewrite)来精简。

🛠 生产案例

"Redis 重启后数据丢了":某项目只配了 RDB,save 间隔 900 秒,结果宕机丢了 14 分钟数据。
解决:开启 AOF(everysec)+ 混合持久化。同时注意:AOF 重写和 RDB bgsave 不要同时进行(内存翻倍风险)。

🔗 4. 高可用:哨兵与集群

主从复制
Master
读写
➔ 同步
Slave-1
只读
➔ 同步
Slave-2
只读

全量同步(首次):Master 生成 RDB → 发送给 Slave → Slave 加载 → 同步增量缓冲区。
增量同步(断线后):基于 repl_backlog 环形缓冲区,offset 差异部分补发。

Sentinel 哨兵自动故障转移
🔁 Sentinel 故障转移全流程
Sentinel 集群
3~5 个实例
1. 持续监控:每秒 PING Master/Slave
❌ Master 宕机
主观下线 SDOWN
2. 多数 Sentinel 确认 → 客观下线 ODOWN
🎯 Leader 选举
Raft 协议
3. 选新 Master:优先级 > 复制偏移量 > RunID
✅ SLAVEOF NO ONE
提升新 Master
4. 其他 Slave 复制新 Master,通知客户端
Redis Cluster — 16384 哈希槽
📊 哈希槽分布示意 (3 主节点)
...
Node A: 0 ~ 5460 (5461 槽)
...
Node B: 5461 ~ 10922 (5462 槽)
...
Node C: 10923 ~ 16383 (5461 槽)

key → CRC16(key) % 16384 → 对应节点,支持在线迁移槽位

公式slot = CRC16(key) & 16383。每个 Master 负责一部分槽,客户端缓存槽→节点的映射,直接定位目标节点,减少跳转。

⚖ Sentinel vs Cluster 对比
维度SentinelCluster
数据量单机数据量支持数据分片,可存更多数据
扩展方式只能读扩展(加 Slave)读写均可水平扩展
故障转移Sentinel 监控 + 自动切换节点间 Gossip 协议自动转移
复杂度部署简单至少 6 节点(3 主 3 从)
适用场景数据量不大,需高可用大数据量、高并发读写
🛠 生产案例

"Master 宕机后 Sentinel 自动切换需要多久?"
取决于配置:down-after-milliseconds(默认 30s)检测下线 + 投票(~2s)+ 故障转移(~5-10s)≈ 30~40 秒。可以通过调小 down-after-millisecondsfailover-timeout 来加速,但太灵敏容易误判(网络抖动触发不必要的切换)。

🔒 5. 分布式锁

为什么需要分布式锁?

多个服务实例同时访问共享资源(库存扣减、订单创建、定时任务去重),需要互斥保证只有一个实例执行。单机 synchronized 只在 JVM 内有效,分布式环境下必须借助外部协调。

基础实现:SET NX EX
# 加锁(原子操作)
SET lock:order:123 "unique_value" NX EX 30

# 释放锁(Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

NX:key 不存在才设置(互斥);EX:设置过期时间(防止死锁);释放时验证 value(防止误解锁)。

Redisson 实现 & 看门狗机制

Redisson 是 Java 客户端,封装了分布式锁的高级实现:

  • 看门狗 (Watchdog):默认锁 30 秒,后台线程每 10 秒(1/3 过期时间)自动续期
  • 业务没执行完 → 看门狗持续续期;执行完释放 → 看门狗停止
  • 解决了"锁过期但任务还没完成"的核心问题
  • 支持可重入锁、公平锁、读写锁、联锁 (MultiLock)
Redlock 算法
🔒 Redlock 多实例加锁流程
Redis-1
加锁
✅ 成功
Redis-2
加锁
✅ 成功
Redis-3
加锁
✅ 成功
Redis-4
失败
❌ 超时
Redis-5
加锁
✅ 成功
结果:4/5 成功(≥ N/2+1 = 3)→ 且耗时 < 锁有效期 → ✅ 加锁成功

单实例锁的问题:Master 加锁后还没同步到 Slave 就宕机,Slave 上没有锁 → 两个客户端同时持有锁。Redlock 向 N 个独立实例加锁,多数成功才算成功。

🛠 生产案例

"锁过期但任务还没完成":电商扣库存耗时 45 秒,锁设了 30 秒过期。第二个请求拿到了锁,两个请求同时扣库存 → 超卖。
解决:使用 Redisson 的看门狗自动续期,业务没执行完锁不会过期。关键代码:lock.lock(30, TimeUnit.SECONDS) 或不设过期时间让看门狗管理。

🔀 6. 缓存一致性问题

Cache Aside (旁路缓存) — 最常用模式
📚 Cache Aside 读流程
1
客户端请求数据 → 先查 Redis 缓存
2
缓存命中 ✅ → 直接返回(快速路径)
3
缓存未命中 ❌ → 查 MySQL 数据库
4
写入 Redis 缓存 → 返回数据
✍ Cache Aside 写流程(先更新 DB,再删除缓存)
更新 MySQL
删除 Redis 缓存
下次读时重建
延迟双删策略
1. 删除缓存
2. 更新数据库
3. 延迟 N 毫秒
4. 再次删除缓存

防止"读请求在写请求更新 DB 之前读到旧值并回填缓存"的不一致窗口。延迟时间 > 主从同步延迟 + 读业务耗时。

Canal — 基于 Binlog 的缓存更新
MySQL
➔ Binlog
Canal
伪装 Slave
➔ 解析
MQ
Kafka/RocketMQ
➔ 消费
更新/删除缓存

Canal 伪装成 MySQL Slave 订阅 Binlog,数据变更时自动触发缓存更新。解耦、可靠、无侵入业务代码,大型系统的主流方案。

⚖ 缓存一致性策略对比
策略一致性复杂度适用场景
先更新 DB 再删除缓存 较高 大多数业务(推荐)
延迟双删 较高 读多写少、容忍短暂不一致
Canal 订阅 Binlog 最高 大型系统、强一致性要求
设置较短 TTL 最终一致 最低 兜底方案,任何策略都应配
🛠 生产案例

"数据库更新了但缓存还是旧值":某商品改价后,用户看到的价格还是旧的。排查发现用了"先删缓存再更新 DB"的策略——删缓存后、DB 更新前,另一个请求读到旧 DB 值回填了缓存。
解决:改为"先更新 DB 再删缓存" + 延迟双删 + 设置 TTL 兜底。大型系统可引入 Canal。

🔥 7. BigKey 与 HotKey

📦 BigKey 大Key

定义:单个 key 的 value 过大(String > 10KB,集合元素 > 5000)

  • 危害:阻塞 Redis(DEL/序列化耗时长)、网络拥堵、内存不均
  • 检测redis-cli --bigkeysMEMORY USAGE keySTRLEN / LLEN / HLEN / SCARD / ZCARD
  • 处理
    • 拆分大 Hash 为多个小 Hash
    • 压缩 value(gzip/snappy)
    • UNLINK 异步删除替代 DEL
    • 设置合理过期时间

🔥 HotKey 热 Key

定义:被高频访问的 key(QPS 远高于其他 key)

  • 危害:单个节点成为热点、CPU 飙升、甚至被打挂
  • 检测redis-cli --hotkeysMONITOR(慎用)、OBJECT FREQ key(需开启 maxmemory-policy 为 LFU)
  • 处理
    • 本地缓存(Caffeine/Guava Cache)
    • 读写分离(从节点分担读)
    • Key 打散:key_1, key_2 ... key_N 随机读
🛠 生产案例

"一个 Hash 有 1000 万 field 导致阻塞":某系统用一个 Hash 存全量用户配置,每次 HGETALL 返回 1000 万条数据,Redis 阻塞 3 秒以上,导致整个集群超时。
拆分策略:按用户 ID 取模拆为 1024 个小 Hash(如 user:config:001 ~ user:config:1024),每个约 1 万 field。配合 HSCAN 替代 HGETALL,阻塞降到毫秒级。

🏆 8. 面试题精选 30 道

💡 使用提示
点击题目展开答案,建议先独立思考再查看参考答案。
1
Redis 为什么选择单线程模型?Redis 6.0 的多线程是怎么回事?
基础高频

Redis 的性能瓶颈在内存和网络 I/O,不在 CPU。单线程避免了锁竞争、上下文切换和死锁问题,代码也更简洁。

Redis 6.0 引入的多线程仅用于网络 I/O 读写,命令执行仍然是单线程。通过 io-threads 配置,默认关闭。开启后可以显著提升网络 I/O 密集型场景的吞吐量。

➤ 追问:单线程如何处理并发连接?→ I/O 多路复用(epoll)
2
SDS 和 C 字符串有什么区别?为什么 Redis 要自己实现 SDS?
数据结构高频

SDS (Simple Dynamic String) 相比 C 字符串有四大优势:

1. O(1) 获取长度:SDS 自带 len 字段,C 字符串需要 O(N) 遍历

2. 二进制安全:不依赖 '\0' 判断结束,可以存储图片、视频等二进制数据

3. 防止缓冲区溢出:修改前自动检查空间,不够自动扩容

4. 空间预分配 & 惰性释放:减少内存重分配次数,提高性能

➤ 追问:SDS 有几种类型?→ sdshdr5/8/16/32/64,不同长度用不同结构体节省内存
3
跳表 (SkipList) 的原理是什么?为什么 ZSet 用跳表而不用红黑树?
数据结构超高频

跳表是多层有序链表,通过随机层数实现概率平衡。查找时从最高层开始,逐层下降,时间复杂度 O(logN)。

Redis 选择跳表而非红黑树的原因:

1. 范围查询更友好:ZSet 的 ZRANGE/ZRANGEBYSCORE 需要范围遍历,跳表在最低层是有序链表,范围遍历天然高效

2. 实现简单:跳表代码量远小于红黑树,易于理解和维护

3. 内存灵活:可以通过调整概率参数在时间和空间之间权衡

➤ 追问:跳表的最高层数?→ Redis 中最大 32 层
4
什么是渐进式 Rehash?为什么要渐进式?
数据结构高频

当 HashTable 需要扩容/缩容时,不是一次性把所有 key 从 ht[0] 迁移到 ht[1],而是分多次、在每次 CRUD 操作时顺带迁移一个 bucket

原因:如果一次性迁移百万级 key,会长时间阻塞 Redis 主线程,导致服务不可用。

渐进式 Rehash 期间的行为:

- 查找:先查 ht[0],没找到再查 ht[1]

- 新增:只写 ht[1]

- 修改/删除:先在 ht[0] 操作,然后迁移该 bucket 到 ht[1]

➤ 追问:什么条件触发扩容?→ used/size > load_factor(默认 1,BGSAVE 时为 5)
5
RDB 和 AOF 有什么区别?生产环境怎么选?
持久化超高频

详见第 3 节对比表。生产环境推荐:

推荐方案:同时开启 RDB + AOF,使用混合持久化。

- RDB 用于冷备份、灾难恢复(定时生成,文件小,恢复快)

- AOF 用于实时数据保护(everysec 策略,最多丢 1 秒数据)

- 混合持久化让 AOF 重写时先用 RDB 格式记录全量数据,再追加增量 AOF,兼顾恢复速度和数据安全

➤ 追问:AOF 重写原理?→ fork 子进程遍历内存生成最简命令,期间新命令写入重写缓冲区最后追加
6
Redis Cluster 的哈希槽机制是什么?为什么是 16384 个槽?
集群高频

Redis Cluster 将数据分为 16384 个哈希槽,每个 key 通过 CRC16(key) % 16384 计算所属槽位,每个 Master 节点负责一部分槽。

为什么是 16384 而不是 65536?

1. Gossip 协议消息大小:节点间心跳携带槽位信息 bitmap,16384 只需 2KB,65536 需要 8KB

2. 集群规模:16384 个槽建议集群不超过 1000 节点,实际足够

3. 槽位压缩:16384 的 bitmap 更紧凑,传输效率更高

➤ 追问:槽怎么迁移?→ CLUSTER SETSLOT + MIGRATE 命令,在线迁移不影响服务
7
Redis 分布式锁怎么实现?有哪些坑?
分布式锁超高频

基础实现:SET key value NX EX 30(NX 互斥 + EX 防死锁),释放用 Lua 脚本验证 value 后删除。

常见坑

1. 锁过期任务未完成:用 Redisson 看门狗自动续期

2. Master 故障锁丢失:Redlock 多数派加锁

3. 误解锁别人的锁:value 用 UUID 唯一标识

4. 不可重入:用 Redisson 的 Hash 结构计数实现可重入

➤ 追问:Redlock 的争议?→ Martin Kleppmann 认为有时钟同步和 GC 停顿问题,Antirez 有反驳
8
缓存和数据库一致性有哪些方案?各有什么优缺点?
缓存一致性超高频

详见第 6 节。核心结论:

1. 先更新 DB 再删缓存(推荐 Cache Aside):一致性较高,极端情况下短暂不一致

2. 延迟双删:第一次删 + 更新 DB + 延迟后第二次删,覆盖并发读的旧值回填

3. Canal 订阅 Binlog(大型系统):解耦、可靠,但架构复杂

4. 设置 TTL(兜底):任何方案都应配 TTL,保证最终一致性

➤ 追问:为什么是删缓存而不是更新缓存?→ 更新缓存在并发场景下可能出现旧值覆盖新值
9
什么是 BigKey?怎么发现和处理?
BigKey高频

BigKey 是指单个 key 的 value 过大。标准:String > 10KB,集合类型元素 > 5000。

发现redis-cli --bigkeysMEMORY USAGE keySTRLEN/LLEN/HLEN/SCARD/ZCARD

处理:拆分大集合为多个小集合、压缩 value、用 UNLINK 异步删除、设置合理过期时间。

注意:不要对 BigKey 执行 DEL(同步阻塞)、HGETALLSMEMBERS 等全量操作,使用 HSCAN/SSCAN 替代。

➤ 追问:UNLINK 和 DEL 的区别?→ UNLINK 异步删除,不阻塞主线程
10
什么是 HotKey?如何检测和处理?
HotKey高频

HotKey 是指被高频访问的 key,QPS 远高于平均水平,会导致单个节点过载。

检测redis-cli --hotkeys(需 LFU 策略)、OBJECT FREQ keyMONITOR(生产慎用)

处理

- 本地缓存(Caffeine/Guava Cache)拦截部分请求

- 读写分离,从节点分担读压力

- Key 打散:hot_key_1 ~ hot_key_N 随机读

- Redis 4.0+ 的 LFU 淘汰策略自动识别热点

➤ 追问:MONITOR 为什么生产慎用?→ 会将所有命令输出到客户端,高并发下严重影响性能
11
Redis 的过期删除策略是什么?
基础高频

Redis 使用两种策略结合:

1. 惰性删除 (Lazy):不主动删除过期 key,访问时检查是否过期,过期则删除。CPU 友好但内存不友好。

2. 定期删除 (Periodic):每 100ms 随机抽取部分设置了过期时间的 key,检查并删除过期的。如果过期比例超过 25%,继续抽查。

两者结合,在 CPU 和内存之间取得平衡。

➤ 追问:如果大量 key 同时过期会怎样?→ 定期删除循环执行,可能短暂影响性能
12
Redis 的内存淘汰策略有哪些?
基础高频

当 Redis 内存使用达到 maxmemory 限制时,有以下淘汰策略:

不淘汰:noeviction(默认,拒绝写入)

全量 key:allkeys-lru(最近最少使用)、allkeys-lfu(最不常用)、allkeys-random(随机)

仅过期 key:volatile-lru、volatile-lfu、volatile-random、volatile-ttl(优先删 TTL 短的)

推荐:allkeys-lfu(Redis 4.0+),最符合实际访问模式。

➤ 追问:LRU 和 LFU 的区别?→ LRU 基于最近访问时间,LFU 基于访问频率
13
Redis 主从复制的原理是什么?全量同步和增量同步的区别?
集群高频

全量同步(首次或断线过长):Master 执行 BGSAVE 生成 RDB → 发给 Slave → Slave 清空旧数据并加载 RDB → Master 发送缓冲区中的增量命令。

增量同步(短时间断线):基于 repl_backlog 环形缓冲区,Slave 带着 offset 重连,Master 补发 offset 之后的数据。如果 offset 已经被覆盖则退化为全量同步。

关键参数repl-backlog-size(默认 1MB,建议根据写流量调大)。

➤ 追问:Master 关闭持久化,Slave 开启持久化可行吗?→ 可行但 Master 重启后数据丢失,Slave 会同步空数据
14
Sentinel 哨兵的工作原理?如何选举新 Master?
集群高频

Sentinel 是 Redis 高可用的实现,通过监控、通知、自动故障转移三大功能保证服务可用。

工作流程:每秒 PING 所有节点 → 超时标记主观下线 (SDOWN) → 多数 Sentinel 确认标记客观下线 (ODOWN) → Raft 协议选举 Leader Sentinel → Leader 执行故障转移。

选新 Master 规则:优先级最高 > 复制偏移量最大 > RunID 最小。

建议:至少部署 3 个 Sentinel 实现高可用(容忍 1 个故障)。

➤ 追问:Sentinel 之间如何通信?→ 发布/订阅 + 定期 PING
15
Redis Cluster 为什么不支持跨 slot 的批量操作?如何解决?
集群

Cluster 中不同 key 可能分布在不同节点,跨节点的批量操作(MGET、PIPELINE、Lua 脚本)无法直接执行。

解决方案

1. Hash Tag{user}:name{user}:age 使用相同的 hash tag,保证落在同一个 slot

2. 客户端对 key 按 slot 分组,分组后并行发送到各节点,再汇总结果

3. 评估是否真的需要 Cluster,Sentinel + 单机可能更简单

➤ 追问:Hash Tag 有什么风险?→ 数据倾斜,所有带相同 tag 的 key 集中在一个节点
16
Redisson 的看门狗机制原理?
分布式锁高频

Redisson 加锁时如果不指定 leaseTime,会启动一个后台定时任务 (Watchdog)

- 默认锁有效期 30 秒 (lockWatchdogTimeout)

- 每隔 10 秒(1/3 超时时间)检查锁是否还被持有

- 如果是,自动续期到 30 秒

- 业务执行完毕释放锁后,看门狗不再续期,锁自然过期

- 如果持有锁的客户端宕机,看门狗停止,锁 30 秒后自动过期释放

➤ 追问:如果指定了 leaseTime 会怎样?→ 看门狗不会启动,到期自动释放
17
什么是缓存穿透、缓存击穿、缓存雪崩?分别怎么解决?
缓存一致性超高频

缓存穿透:查询一个不存在的数据,缓存和 DB 都没有,每次都穿透到 DB。

→ 解决:布隆过滤器(拦截不存在 key)、缓存空值(设短 TTL)

缓存击穿:一个热点 key 过期瞬间,大量请求同时打到 DB。

→ 解决:互斥锁(只放一个请求去加载)、逻辑过期(不设 TTL,异步更新)

缓存雪崩:大量 key 同时过期,或 Redis 宕机,请求全部涌向 DB。

→ 解决:TTL 加随机偏移、Redis 高可用集群、多级缓存、限流降级

➤ 追问:布隆过滤器的缺点?→ 有误判率(假阳性),不能删除已添加的元素
18
Redis 的 I/O 多路复用模型是怎样的?
基础

Redis 使用 epoll (Linux) / kqueue (macOS) / select (通用) 实现 I/O 多路复用:

1. Redis 启动时创建 epoll 实例,将监听 socket 注册到 epoll

2. 当新连接到来或已有连接有数据可读时,epoll 通知 Redis

3. Redis 的事件循环 (aeMain) 不断从 epoll 获取就绪事件

4. 根据事件类型分发给对应的处理器(连接处理器、命令处理器、回复处理器)

核心:一个线程同时监控 thousands 个连接,只有就绪的连接才会被处理,避免了线程切换开销。

19
ZipList 的连锁更新问题是什么?ListPack 怎么解决的?
数据结构

ZipList 中每个 entry 都有 prev_len 字段记录前一个 entry 的长度。当 entry 长度在 253~254 字节之间变化时,会导致 prev_len 从 1 字节扩展到 5 字节,引发连锁更新——后续所有 entry 的 prev_len 都需要扩展,最坏 O(N²)。

ListPack 的改进:去掉 prev_len,改为记录自身长度。从后向前遍历时,通过当前 entry 的自身长度字段回溯到前一个 entry 的起始位置。彻底消除了连锁更新问题。

➤ 追问:实际中连锁更新常见吗?→ 不常见,需要特殊的 entry 长度分布才会触发
20
Redis 的 RESP 协议是什么?
基础

RESP (REdis Serialization Protocol) 是 Redis 的通信协议,特点:简单、易读、易实现

基于前缀区分类型:

- + 简单字符串:+OK\r\n

- - 错误:-ERR unknown command\r\n

- : 整数::1\r\n

- $ 批量字符串:$5\r\nhello\r\n

- * 数组:*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n

Redis 6.0 引入 RESP3,支持更多数据类型(Map、Set、Boolean 等)。

21
Redis 事务是怎么实现的?支持回滚吗?
基础

Redis 事务通过 MULTI → 命令队列 → EXEC 实现:

1. MULTI:开始事务,后续命令进入队列

2. EXEC:原子性执行队列中的所有命令

3. DISCARD:放弃事务

4. WATCH:乐观锁,监视 key 在 EXEC 前是否被修改

不支持回滚!如果队列中某条命令执行失败(语法错误除外),后续命令继续执行。这是设计决策——Redis 追求简单快速,回滚需要保存状态日志。

➤ 追问:需要严格事务怎么办?→ 用 Lua 脚本代替,保证原子性
22
Redis 为什么用 Lua 脚本?有什么注意事项?
基础高频

Lua 脚本在 Redis 中原子执行(不会被其他命令打断),适合需要多条命令保证原子性的场景(分布式锁释放、限流等)。

注意事项

1. Lua 脚本不能执行太久(默认 5 秒超时 lua-time-limit

2. 脚本过长会占用大量内存,影响复制

3. Redis 7.0 推荐用 Function 替代 Lua 脚本(持久化更好)

4. EVALSHA 优于 EVAL,避免每次传输完整脚本

23
Redis 的 publish/subscribe 模式有什么缺点?
基础

Redis 的 Pub/Sub 模式有两个关键缺点:

1. 不持久化:消息发出后如果订阅者不在线,消息就丢了。没有 ack 机制。

2. 不保证可靠投递:网络异常时消息可能丢失。

如果需要可靠消息队列,应该用 Redis Stream(Redis 5.0+),支持持久化、消费者组、ack 机制,类似 Kafka 的简化版。

➤ 追问:Redis Stream 和 Kafka 的区别?→ Stream 更轻量但不支持分区、没有 Kafka 的生态系统
24
Redis 的 String 类型底层占多少内存?
数据结构

一个简单的 SET key value 实际占用的内存远大于 value 本身:

- RedisObject 头:16 字节(type + encoding + lru + refcount + ptr)

- SDS 头:sdshdr8 至少 3 字节(len + alloc + flags)+ '\0'

- key 本身:也是 SDS

- HashTable 开销:两个 dictEntry 指针(key + value + next)24 字节 + 桶数组

一个简单的 key-value 最少约 64~100 字节,即使 value 只有几个字符。这是优化内存时需要重点关注的。

➤ 追问:如何减少内存?→ 小 Hash 用 ListPack 编码,合并多个 key 为 Hash field
25
Redis 如何做内存优化?
数据结构高频

1. 利用编码优化:小数据量自动用 ListPack/IntSet,调整阈值参数

2. 合并 key 为 Hash field:多个 key 合并为一个 Hash,减少 RedisObject 头开销

3. 使用合适的数据类型:位图 (Bitmap) 存布尔值、HyperLogLog 存 UV、Geo 存经纬度

4. 设置合理过期:避免无用的 key 占用内存

5. 开启 lazyfree:大 key 异步删除,避免阻塞

6. 压缩列表存储:利用 ziplist-max-entriesziplist-max-value 控制

26
Redis Cluster 的 Gossip 协议是什么?
集群

Gossip 是一种去中心化的集群信息传播协议

- 每个节点定期(每秒)随机选择几个节点发送 PING 消息,携带自己的状态信息

- 收到 PING 的节点回复 PONG,并更新自己的集群状态

- 新节点加入时,只需 CLUSTER MEET 一个现有节点,信息会通过 Gossip 逐步传播到所有节点

- 优点:去中心化、容错好、扩展方便

- 缺点:信息传播有延迟,最终一致而非强一致

27
Redlock 算法的流程和争议?
分布式锁

Redlock 流程(详见第 5 节):

1. 向 N 个独立 Redis 实例同时请求加锁

2. 在超时时间内,获得 N/2+1 个锁就算成功

3. 加锁成功后设置锁有效期为 初始有效期 - 加锁耗时

4. 失败则向所有实例释放锁

争议(Martin Kleppmann):分布式系统中时钟不可信(GC 停顿、时钟跳跃),可能导致锁安全性被破坏。
Antirez 反驳:实际场景中时钟误差极小,Redlock 足够安全,比单实例锁更可靠。
建议:大多数场景用 Redisson 单实例 + 看门狗就够了,极端场景用 ZooKeeper/etcd。

28
Redis 和 Memcached 有什么区别?
基础
维度RedisMemcached
数据结构丰富(String/Hash/List/Set/ZSet/Stream...)仅 String
持久化RDB + AOF不支持
线程模型单线程(6.0 网络 I/O 多线程)多线程
集群原生 Cluster客户端分片
内存管理丰富淘汰策略LRU
发布/订阅支持不支持

总结:Redis 功能更丰富,是当前的首选。Memcached 在纯 KV 缓存 + 多核场景下有优势。

29
Redis 的管道 (Pipeline) 和事务有什么区别?
基础

Pipeline:将多条命令打包一次发送,减少网络 RTT。命令之间不保证原子性,每条命令独立执行。

事务 (MULTI/EXEC):命令原子执行(中间不会插入其他客户端的命令),但不保证隔离性(WATCH 可实现乐观锁)。

可以结合:在 Pipeline 中发送 MULTI + 命令 + EXEC,既减少网络开销又保证原子性。

➤ 追问:Pipeline 中某条命令失败会怎样?→ 该命令返回错误,其他命令正常执行
30
生产环境中 Redis 的常见性能瓶颈和优化手段?
实战超高频

常见瓶颈

1. BigKey 操作:HGETALL/DEL 大 key 阻塞主线程 → 拆分或用 UNLINK

2. 集中过期:大量 key 同时到期 → TTL 加随机偏移

3. 内存满:频繁淘汰 → 扩容或优化数据结构

4. AOF fsync 频繁:磁盘 I/O 成为瓶颈 → everysec 策略或 SSD

5. 慢查询:KEYS *、SORT、SINTER 大集合 → 用 SCAN 替代

排查工具SLOWLOG GETredis-cli --latencyINFOMEMORY DOCTOR

➤ 追问:如何监控 Redis 延迟?→ redis-cli --latency、--latency-history、Prometheus + Grafana
Redis 深度指南 · 面试备战库 · 持续更新