Redis 面试全攻略

数据结构 · 缓存三大问题 · 持久化 · 集群 · 分布式锁

8大数据结构 缓存穿透/击穿/雪崩 RDB vs AOF 分布式锁

Redis 数据结构全览

5 大基本类型 + 3 大扩展类型

类型底层编码典型场景停车场类比
String int / embstr / raw 缓存、计数器、分布式锁、Session 停车场的临时寄存柜
List quicklist (ziplist + linkedlist) 消息队列、最新列表、栈/队列 排队取车的顺序列表
Hash ziplist / hashtable 对象存储(用户信息、商品详情) 停车场不同类型的车位(VIP/普通/临时)
Set intset / hashtable 去重、标签、共同好友、抽奖 今日会员ID集合(自动去重)
ZSet ziplist / skiplist + hashtable 排行榜、延迟队列、带权重的集合 停车场车位使用率排行榜
HyperLogLog 稀疏/密集编码 UV 统计(基数估算,0.81%误差) 今天大约来了多少不同车主
Bitmap String 的位操作 签到、在线状态、布隆过滤器 365天的签到记录(1位=1天)
Geo ZSet + GeoHash 附近的人、门店定位 停车场周边地图坐标

面试最爱问:ZSet 底层为什么用跳表不用红黑树?

范围查询更优:跳表范围查询 O(logN + M),红黑树需要中序遍历
实现简单:跳表代码量远小于红黑树,便于维护
内存灵活:跳表可通过调整概率控制空间/时间平衡
并发友好:跳表更容易实现并发(虽然 Redis 单线程不需要)

面试说出"跳表范围查询更优 + 实现简单"就能拿到基本分。主动提一句"Redis 作者 antirez 亲自解释过"更加分。

缓存三大问题(面试必考)

穿透

缓存穿透

是什么:查询一个数据库中也不存在的数据,缓存永远 miss,请求全部打到 DB

停车场:有人反复问"有没有特殊车位(如充电桩车位)"——车位表上没有登记,管理员每次都要去现场查看

解决方案:

1. 布隆过滤器:在缓存前加一层,不存在的 key 直接拦截

2. 缓存空值:查不到也缓存 null,设短 TTL(如 60s)

3. 参数校验:非法请求直接拦截

击穿

缓存击穿

是什么:一个热点 key 过期的瞬间,大量并发请求同时打到 DB

停车场:停车场入口的车牌识别系统突然故障,10辆车同时涌向入口

解决方案:

1. 互斥锁:只允许一个线程查 DB 并回填缓存,其他等

2. 逻辑过期:不设 TTL,在 value 中存逻辑过期时间,过期后异步更新

3. 热点 key 永不过期(后台定时刷新)

雪崩

缓存雪崩

是什么:大量 key 同时过期,或 Redis 宕机,请求全打到 DB

停车场:入口系统全面宕机,所有车都涌向同一个入口

解决方案:

1. TTL 加随机值:避免大量 key 同时过期

2. Redis 集群:高可用,避免单点宕机

3. 多级缓存:本地缓存 (Caffeine) + Redis

4. 限流降级:DB 扛不住就限流

对比

三者的核心区别

穿透:数据本来就不存在 → 布隆过滤器 / 缓存空值

击穿一个热点 key 过期 → 互斥锁 / 逻辑过期

雪崩大量 key 同时过期 → 随机 TTL + 集群

记忆口诀:穿=没有的数据,击=一个热点挂了,雪=大面积崩了

互斥锁解决缓存击穿的代码模板

public String getWithMutex(String key) {
  String value = redis.get(key);
  if (value == null) {
    // 尝试获取互斥锁
    Boolean locked = redis.setnx("lock:" + key, "1", 10, SECONDS);
    if (locked) {
      try {
        value = db.query(key);  // 查 DB
        redis.set(key, value, 300 + random(60), SECONDS);
      } finally {
        redis.del("lock:" + key);  // 释放锁
      }
    } else {
      Thread.sleep(100);  // 等一会儿重试
      return getWithMutex(key);
    }
  }
  return value;
}

RDB vs AOF 持久化

全面对比

维度RDB (快照)AOF (追加日志)
原理 定时把内存数据全量快照写入磁盘 每条写命令追加到日志文件
文件 dump.rdb(二进制,紧凑) appendonly.aof(文本,可读)
恢复速度 快(直接加载二进制) 慢(重放所有写命令)
数据安全 可能丢失最后一次快照后的数据 最多丢 1 秒(everysec 策略)
文件大小 小(二进制压缩) 大(但可 BGREWRITEAOF 压缩)
对性能影响 fork 子进程时可能有瞬间卡顿 每秒 fsync,开销稍大
触发方式 save / bgsave / 配置自动触发 always / everysec / no

RDB = 每天打烊时给整个停车场拍一张全景照片(恢复快,但打烊前的订单可能丢了);AOF = 每辆车进出就记一笔流水账(更安全,但翻账本恢复慢)

AOF 三种同步策略

策略含义数据安全性能
always 每条命令都 fsync 到磁盘 最高(不丢数据) 最差
everysec 每秒 fsync 一次 最多丢 1 秒 推荐 ✅
no 交给 OS 决定何时刷盘 可能丢较多 最好

生产环境推荐 RDB + AOF 混合持久化(Redis 4.0+):AOF 重写时把当前数据以 RDB 格式写入 AOF 文件头部,后续命令继续用 AOF 追加。兼顾恢复速度和数据安全。面试说出这个是加分项。

Redis 集群三种模式

基础

主从复制

一主多从,主写从读,数据单向复制

停车场:总停车场有完整车位数据,分停车场同步只读副本

❌ 主挂了需手动切换

常用

哨兵模式 (Sentinel)

在主从基础上加哨兵进程,自动监控 + 故障转移

停车场:哨兵监控主节点,主节点宕机自动选举从节点升主

✅ 自动故障转移

大规模

Cluster 集群

16384 个哈希槽分散到多个节点,每个节点负责一部分

停车场:停车场连锁按区域分流(1-4000号车位A区管,4001-8000号B区管)

✅ 自动分片 + 高可用

必问

为什么是 16384 个槽?

• 节点间心跳包要携带槽信息,槽太多心跳包大
• 16384 个槽用 2KB 就能表示(每个bit=1个槽),压缩好传
• 集群规模一般不超过 1000 节点,16384 够用且不浪费
• Redis 作者原话:选择 16384 是心跳大小和集群规模的平衡

分布式锁

Redis 分布式锁的正确姿势

// ❌ 错误写法(非原子)
redis.setnx("lock", "1");  // 加锁
redis.expire("lock", 10, SECONDS); // 设过期 - 如果这步挂了,锁永不释放!

// ✅ 正确写法(原子操作)
redis.set("lock", uniqueId, "NX", "EX", 10);
// NX=不存在才设置 EX=秒级过期 一步搞定

// ✅ 释放锁也要原子(Lua脚本)
String script = "if redis.call('get',KEYS[1]) == ARGV[1] "
  + "then return redis.call('del',KEYS[1]) "
  + "else return 0 end";
redis.eval(script, 1, "lock", uniqueId);
// 先检查是不是自己的锁,再删除。防止误删别人的锁

Redis 分布式锁的经典陷阱:锁过期了但业务还没执行完 → 别人拿到锁 → 并发安全问题。解决:Redisson 的看门狗机制(Watchdog),自动续期。

Redisson 看门狗原理

加锁,默认 30 秒
后台线程每 10 秒
(1/3 过期时间)
检查锁还被持有?
→ 续期到 30 秒
业务完成
→ 手动释放

只要客户端还活着且没释放锁,看门狗就会一直续期。客户端宕机后看门狗也停了,锁自然过期释放。

RedLock 算法(面试了解)

单节点 Redis 锁在主从切换时可能丢失锁信息。RedLock 的方案:

• 向 N 个独立 Redis 实例(通常 5 个)同时加锁
• 超过半数(N/2 + 1)加锁成功 → 获取锁成功
• 总耗时不能超过锁的过期时间
• 任何一步失败 → 释放所有已加的锁

争议:Martin Kleppmann 认为 RedLock 不安全,antirez 回怼。面试说出"了解争议"即可。

过期删除 & 内存淘汰策略

过期 key 怎么删除?

Redis 采用 惰性删除 + 定期删除 双管齐下:

惰性删除:不主动删,访问 key 时才检查是否过期 → 省CPU,但可能占内存
定期删除:每秒 10 次随机抽取一批 key 检查,过期则删除 → 折中方案

惰性 = 车主来取车时发现过期才删除;定期 = 管理员每隔一段时间检查一批过期车位。两种结合才能既不太累也不浪费

8 种内存淘汰策略

策略含义场景
noeviction不淘汰,内存满了直接报错数据不能丢
allkeys-lru所有key中淘汰最久未使用的最常用 ✅
allkeys-lfu所有key中淘汰使用频率最低的热点数据明显
allkeys-random所有key中随机淘汰无访问热点
volatile-lru设了TTL的key中淘汰最久未用的只淘汰可过期数据
volatile-lfu设了TTL的key中淘汰频率最低的同上+频率优先
volatile-random设了TTL的key中随机淘汰同上+无偏好
volatile-ttl设了TTL的key中淘汰TTL最小的优先淘汰快过期的

面试最常问:"Redis 内存满了怎么办?"→ allkeys-lru。能说出 LRU 和 LFU 的区别(最近最少使用 vs 最不经常使用)就稳了。

高频面试问答 TOP 15

1Redis 为什么快?

基于内存:纳秒级读写,不涉及磁盘IO
单线程模型:避免上下文切换和锁竞争开销
IO 多路复用(epoll):一个线程处理大量并发连接
高效数据结构:跳表、ziplist、quicklist 等专为性能优化

2为什么 Redis 是单线程?6.0 之后呢?

单线程原因:Redis 性能瓶颈是内存和网络而非 CPU,多线程加锁开销反而降低性能。

Redis 6.0 引入 多线程 IO:命令执行仍是单线程,但网络读写用多线程。默认关闭,需配置开启。

3缓存穿透、击穿、雪崩的区别和解决方案?

穿透(数据不存在)→ 布隆过滤器 / 缓存空值
击穿(热点 key 过期)→ 互斥锁 / 逻辑过期
雪崩(大量 key 同时过期)→ TTL 加随机值 / 集群 / 多级缓存
口诀:穿=没有的数据,击=一个热点,雪=大面积崩

4RDB 和 AOF 怎么选?

生产推荐 RDB + AOF 混合持久化(Redis 4.0+):AOF 重写时以 RDB 格式写入头部,兼顾恢复速度和数据安全。配置:aof-use-rdb-preamble yes

5Redis 分布式锁怎么实现?有什么坑?

加锁:SET key value NX EX seconds(原子操作)
释放锁:Lua 脚本先判断是不是自己的锁再删除
坑:① 锁过期业务没完 → 看门狗续期 ② 主从切换丢锁 → RedLock ③ 不可重入 → Redisson

6Redis 的过期删除策略?

惰性删除 + 定期删除
惰性:访问时才检查过期(省CPU但占内存)
定期:每秒10次随机抽样检查删除(折中方案)
不是定时删除:每个key设定时器太耗内存

7内存淘汰策略有哪些?生产用哪个?

8种策略,生产推荐 allkeys-lru(最久未使用淘汰)
如果热点数据明显用 allkeys-lfu(最不经常使用淘汰)
如果数据必须保留用 noeviction

8Redis 集群 16384 个槽怎么来的?

槽数量需要在心跳包大小和集群规模间平衡。16384 个槽只需 2KB 位图表示,心跳包小传输快。集群节点一般不超过 1000,16384 够用。

9缓存和数据库双写一致性?

常用方案:Cache Aside Pattern
• 读:先读缓存,miss 则读 DB 并回填
• 写:先更新 DB,再删除缓存(不是更新缓存!)
为什么删不更:写多读少时频繁更新缓存浪费;并发写可能导致缓存和DB不一致
强一致性:用 Canal 监听 binlog 异步更新缓存

10Redis 实现消息队列?

List + LPUSH/BRPOP:简单,但不支持消费组、消息确认
Pub/Sub:发布订阅,消息不持久化,离线丢失
Stream(5.0+):支持消费组、消息确认、持久化,最接近专业MQ

11BigKey 问题怎么处理?

BigKey = 一个 key 对应的 value 过大(如 100KB+ 的 String,万元素的集合)
危害:阻塞其他命令、网络拥塞、内存不均
解决:① 拆分(大 Hash 拆小 Hash)② 压缩(JSON→MsgPack)③ 本地缓存(不走 Redis)④ 删除用 UNLINK(异步删除)

12HotKey 问题怎么处理?

HotKey = 某个 key 被超高频率访问
发现:redis-cli --hotkeys / monitor 分析 / 业务预判
解决:① 本地缓存(Caffeine)② 读写分离(从节点分担读)③ key 分片(hot_key_1/2/3 随机读)

13Redis 和 Memcached 的区别?

① Redis 支持更丰富的数据结构(Memcached 只有 String)
② Redis 支持持久化(Memcached 纯内存)
③ Redis 单线程(Memcached 多线程)
④ Redis 支持发布订阅/Lua/事务/集群
⑤ Memcached 更适合纯 KV 缓存场景
结论:新项目基本都选 Redis

14Redis 事务怎么用?支持回滚吗?

MULTI → 命令入队 → EXEC 一次性执行。不支持回滚(与 MySQL 不同)。
原因:Redis 作者认为出错应该是编程错误,不需要回滚来恢复,保持简单。
替代:Lua 脚本(原子执行多条命令)比事务更实用。

15Pipeline 有什么用?

将多条命令打包一次发送,减少网络 RTT。
注意:Pipeline 不是原子操作(中间可能插别的命令),只是网络优化。
适合批量操作(如批量 set/get),不适合依赖前一条结果的操作。

速记卡片(面试前 5 分钟看)

缓存三问题:

穿=没有→布隆/空值 | 击=热点→锁 | 雪=大面积→随机TTL

持久化:

RDB=快照(快/可能丢) | AOF=日志(慢/更安全) | 推荐:混合

集群:

主从→哨兵(自动切换)→Cluster(16384槽分片)

分布式锁:

SET NX EX + Lua释放 | 看门狗续期 | RedLock多节点

淘汰策略:

生产=allkeys-lru | 内存满→淘汰最久未用

双写一致性:

更新DB→删缓存 | 不更新缓存 | 强一致用Canal

Redis 面试全攻略 · 停车场类比版 · 全栈资料库
核心三原则速记

Redis三原则:① 合理设置TTL避免内存暴涨 → ② 热点Key做永不过期+异步刷新 → ③ 分布式锁要有看门狗续期