Redis 面试题笔记

Redis 面试题笔记

个人复习笔记,覆盖 Redis 数据结构、持久化、缓存三大问题、分布式锁、高可用架构的高频面试题。

目录


Redis 基础认知

Redis 是什么?有哪些特点?

基于内存的键值 NoSQL 数据库,常用作缓存、分布式锁、消息队列。

特点:

  1. 纯内存操作,读写极快(10w+ QPS);
  2. 丰富的数据结构(String、Hash、List、Set、ZSet 等);
  3. 支持持久化(RDB/AOF)、主从复制、哨兵、集群;
  4. 单线程处理命令(6.0 后网络 IO 多线程),无并发竞争问题;
  5. 支持 Lua 脚本、事务、发布订阅、Pipeline。

Redis 为什么这么快?(高频)

  1. 纯内存读写,无磁盘 IO;
  2. 单线程命令处理:无锁、无线程上下文切换开销(瓶颈在网络与内存,不在 CPU);
  3. IO 多路复用(epoll):单线程高效监听大量连接,非阻塞 IO;
  4. 高效的数据结构:SDS 动态字符串、跳表、压缩列表/listpack、渐进式 rehash 的哈希表等;
  5. 6.0 引入多线程处理网络 IO(读写、协议解析),命令执行仍是单线程,进一步提升吞吐。

Redis 和 Memcached 的区别?

Redis Memcached
数据结构 丰富(5 大类型 + 特殊类型) 只支持 KV 字符串
持久化 RDB、AOF 不支持
高可用 主从、哨兵、Cluster 无原生支持
线程模型 命令单线程 + IO 多线程 多线程
附加能力 Lua、事务、Pub/Sub、Geo 无

有持久化、丰富结构、高可用需求选 Redis;纯缓存且超简单 KV 可用 Memcached。


数据类型与应用场景

五大基本数据类型及底层结构?(高频)

类型 底层结构 典型场景
String SDS(简单动态字符串),int/embstr/raw 编码 缓存对象 JSON、计数器(incr)、分布式锁、session
Hash ziplist/listpack(小数据)→ hashtable 存对象字段(可单独读写某个字段)、购物车
List quicklist(双向链表 + ziplist/listpack 节点) 消息队列(lpush/rpop)、最新列表、时间线
Set intset(全整数)→ hashtable 标签、共同好友(交集 sinter)、抽奖去重
ZSet ziplist/listpack → skiplist 跳表 + 哈希表 排行榜、延时队列、范围排名

特殊类型及场景?

  • BitMap:位图,签到(1 亿用户只占约 12MB)、活跃标记、布隆过滤器底层;
  • HyperLogLog:基数估算(误差 0.81%),UV 统计,固定 12KB;
  • GEO:基于 ZSet,附近的人/门店;
  • Stream:消息队列(消费者组、ACK、Pending 列表),对标 Kafka 的轻量替代。

跳表(ZSet 为什么用跳表不用红黑树)?(高频)

跳表:多层有序链表,查找 O(log n),空间换时间。

不用红黑树的原因:

  1. 实现更简单,代码好维护;
  2. 范围查询更方便(ZRANGE 顺着链表遍历即可,红黑树要中序遍历+回溯);
  3. 插入删除只需改前后指针,无需旋转;
  4. 按概率维护多层,无需像 AVL/红黑树那样严格平衡。

SDS 相比 C 字符串的好处?

  1. O(1) 获取长度(结构体存了 len,C 字符串要 strlen 遍历);
  2. 二进制安全(按 len 读,不怕内容里有 \0,可存图片序列化数据);
  3. 杜绝缓冲区溢出(扩容前检查 + 空间预分配);
  4. 惰性释放减少内存重分配次数。

持久化机制

RDB 和 AOF 的区别?(高频)

RDB AOF
内容 某时刻全量数据的二进制快照 每条写命令的追加日志
触发 save(阻塞)、bgsave(fork 子进程)、配置自动触发 appendfsync always/everysec/no
文件 紧凑、小、恢复快 大、有重写机制(bgrewriteaof)压缩
数据丢失 两次快照之间的数据 always 不丢;everysec 最多丢 1 秒(默认);no 看操作系统
性能影响 fork 瞬间可能卡顿 写盘频繁,文件大后重写耗 IO

RDB 的 fork + COW 原理?

bgsave 时主进程 fork 出子进程 ,子进程遍历内存生成 RDB 文件。fork 利用操作系统的写时复制(COW):父子进程共享物理内存页,主进程此时继续处理写请求,被修改的页才复制一份。因此快照期间服务不停,但写入量大时内存可能翻倍、fork 本身有毫秒级阻塞。

AOF 重写是什么?

AOF 文件越来越大,重写(bgrewriteaof)fork 子进程根据当前内存数据 生成等价的最小命令集(如 100 次 incr 合并成 set counter 100)。重写期间新写入命令同时写入 AOF 缓冲区和重写缓冲区,子进程完成后追加增量部分并原子替换旧文件。

4.0 的混合持久化?

aof-use-rdb-preamble=yes:AOF 重写时,前半段存 RDB 全量二进制 (恢复快),后半段追加增量 AOF 命令。兼顾恢复速度和数据完整性,5.0 后默认开启。


过期删除与内存淘汰

过期键的删除策略?(高频)

Redis 采用 惰性删除 + 定期删除 组合:

  1. 惰性删除:访问 key 时检查是否过期,过期则删除并返回 nil。省 CPU,但冷数据一直占内存;
  2. 定期删除:默认每 100ms 随机抽查一批设置了过期时间的 key,删除其中的过期键;若过期比例超过 1/4,继续下一轮抽查(限制时间避免阻塞)。
  3. 兜底:内存达到 maxmemory 时触发内存淘汰策略。

内存淘汰策略有哪些?(高频)

maxmemory-policy 配置,8 种:

策略 说明
noeviction(默认) 内存满后拒绝写入,报错
allkeys-lru 所有 key 中淘汰最近最少使用的(缓存场景最常用)
allkeys-lfu(4.0+) 所有 key 中淘汰访问频率最低的
allkeys-random 随机淘汰
volatile-lru 只在设置了过期时间的 key 中 LRU
volatile-lfu 同上,LFU
volatile-random 同上,随机
volatile-ttl 优先淘汰剩余存活时间最短的

LRU vs LFU:LRU 淘汰最近未访问(偶发扫描会污染);LFU 淘汰访问频次低的(适合热点数据稳定场景)。


缓存三大问题

缓存穿透、击穿、雪崩的区别和解决方案?(超高频)

问题 定义 解决方案
穿透 查询根本不存在的数据,缓存永远不命中,请求全打到数据库(恶意攻击常见) ① 缓存空值 (短 TTL,如 60s);② 布隆过滤器在缓存前拦截;③ 接口层参数校验、限流、风控
击穿 某个热点 key 过期瞬间,大量并发同时打到数据库 ① 互斥锁 :只放一个线程去重建缓存(setnx),其他等待;② 热点 key 逻辑过期(物理不过期,value 里存过期时间,异步更新);③ 热点数据预热、永不过期
雪崩 大量 key 同时过期 或 Redis 宕机,数据库被冲垮 ① 过期时间加随机值打散;② 缓存高可用(哨兵/集群);③ 限流+降级兜底;④ 多级缓存(本地缓存 Caffeine + Redis)

记忆:穿透=查不存在的;击穿=一个热点过期;雪崩=一片倒。

布隆过滤器原理?

位数组 + 多个 hash 函数:写入时把元素 hash 到多个位上置 1;查询时所有对应位都为 1 才"可能存在",任一位为 0 则一定不存在 。特点:可能误判(说有其实没有)、不能删除、空间极省。用于缓存穿透防护、爬虫去重等。误判率换内存,Redis 4.0 后可用 RedisBloom 模块,或自己用 BitMap 实现。


缓存与数据库一致性

先删缓存还是先更新数据库?(高频)

推荐 Cache Aside(旁路缓存)模式:读------先读缓存,未命中读库并回填;写------先更新数据库,再删除缓存。

两种错误写法:

  • 先删缓存再更新数据库:删完到更新完成之间,读请求把旧值重新灌入缓存,脏数据长期存在(概率高);
  • 更新缓存而不是删除缓存:并发写时乱序,且写了不一定有人读,浪费。

先更新数据库再删缓存仍有的小概率不一致 :读请求在缓存未命中后读到了旧库值,此时写请求更新库+删缓存完成,读请求才把旧值回填。概率极低(要求"读比写慢"),可接受;强一致场景加延迟双删或订阅 binlog 补偿。

延迟双删是什么?

写操作时:删除缓存 → 更新数据库 → 休眠一小段时间(读请求回填旧值的窗口期) → 再删一次缓存。第二次删除清掉并发读回填的脏数据。延时长短要估算读请求耗时,一般几百毫秒到 1 秒,常用异步/消息队列实现第二次删除。

订阅 binlog 方案(最终一致,生产常用)

用 Canal 伪装 MySQL 从库订阅 binlog → 解析出数据变更 → 发 MQ → 消费者删除/更新 Redis。把一致性从业务代码里剥离,保证最终一致,是主流方案。


分布式锁

用 Redis 怎么实现分布式锁?(高频)

最简单:SET lock_key uniqueValue NX EX 30(NX 不存在才设置=互斥;EX 加过期=防死锁;value 放唯一标识=防误删)。

释放锁必须原子:Lua 脚本先判断 value 是自己的再 DEL:

lua 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

为什么 value 要唯一(如 UUID+线程ID):A 业务超时锁被自动释放、B 拿到锁后,A 恢复执行 DEL 时不能删掉 B 的锁。

锁过期了业务还没执行完怎么办?------看门狗

方案一:Redisson 的看门狗(watchdog) :加锁不指定过期时间时,默认锁 30s,后台线程每 10s(1/3)续期,业务执行完主动释放,进程挂掉续期停止、锁到期自动释放。

方案二:业务侧把临界区控制在锁 TTL 内,或过期后自旋等待锁重建。

Redisson 加锁流程?

  1. tryLock 执行 Lua:hash 结构(key=锁名,field=客户端ID:线程ID,value=重入次数)+ pexpire,支持可重入;
  2. 拿不到锁则订阅解锁消息 + 信号量等待 + 自旋重试;
  3. 看门狗自动续期;
  4. 释放时 hash 减 1,减到 0 删除锁并发布解锁消息。

RedLock 了解吗?有争议吗?

RedLock(红锁):向 N 个独立 Redis 主节点同时加锁,半数以上 成功且耗时小于锁有效期才算获取成功,防止单点故障导致锁失效。争议(Martin Kleppmann vs antirez 论战):Redis 主从异步复制下,主挂了锁数据没同步到从,新主上锁丢失,可能出现两个客户端同时持锁 ------RedLock 只能缓解不能根除。实践建议 :Redis 锁用于效率场景(防重复),正确性要求高的场景用 ZooKeeper/etcd 锁或数据库唯一约束。


高可用架构

主从复制的原理?(高频)

  1. 首次全量同步:从库发 psync → 主库 bgsave 生成 RDB 发给从库 → 从库清空数据载入 RDB → 主库把期间的写命令缓冲发给从库;
  2. 后续增量同步:主库把写命令实时通过 replication buffer 发给从库;断线重连后,根据 offset 和复制积压缓冲区(repl_backlog)尝试部分重同步,offset 被覆盖则重新全量;
  3. 默认从库只读,读写分离时注意主从延迟导致读到旧数据。

哨兵(Sentinel)的作用和原理?

哨兵是独立的监控进程集群(至少 3 个,奇数):

  1. 监控:每秒 ping 主从库;
  2. 主观下线:单个哨兵认为主库无响应;
  3. 客观下线:quorum 个哨兵都认为下线,才确认故障;
  4. 选主:哨兵之间 Raft 式选举出 leader 执行故障转移;
  5. 故障转移:从从库中按优先级/offset/runid 选出新主,其余从库改挂新主,通知客户端(通过发布订阅 +provider)。

Cluster 集群模式?为什么是 16384 个槽?(高频)

  • 全部 key 按公式 slot = CRC16(key) mod 16384 分到 16384 个槽,节点负责一部分槽;客户端可直连任意节点,节点返回 MOVED/ASK 重定向,智能客户端本地缓存槽位映射表;
  • 节点间 Gossip 协议 交换状态;主节点挂掉,其从节点自动顶上;
  • 不支持多 key 跨槽操作 (可用 hash tag {user1000} 让相关 key 同槽)、只支持 db0。

为什么 16384 而不是 65536(作者回答):

  1. 心跳包携带槽位图,16384 位 = 2KB,65536 位 = 8KB,心跳太占带宽;
  2. Redis Cluster 建议节点数不超过 1000,16384 个槽足够分;
  3. 槽越少,节点少时压缩率越高。

哨兵和 Cluster 怎么选?

  • 数据量小、QPS 中等:主从 + 哨兵,架构简单;
  • 数据量大(单机内存放不下)、需要水平扩容和分片写入:Cluster。

性能与常见问题

什么是大 key?怎么解决?

定义(经验值):String 超过 10KB 、集合元素超过 5000 或总大小超 10MB。

危害:网络拥塞、单线程阻塞(删除/序列化耗时)、主从同步延迟、内存不均。

解决:

  1. 拆分:大 Hash 按字段 hash 到多个 key,大 List 分段;
  2. 压缩 value(先压缩再存);
  3. 删除用 UNLINK(异步删除)代替 DEL,或 SCAN 渐进清理;
  4. 监控发现:redis-cli --bigkeys、RDB 离线分析。

什么是热 key?怎么解决?

某个 key 被超高频率访问(如明星热搜、爆款商品),流量集中打到一个节点。

解决:

  1. 本地缓存(Caffeine/Guava)挡在最前面;
  2. 热点 key 多副本 :key_1、key_2... 随机读,分散到不同节点;
  3. 读写分离,从库分担读压力;
  4. 提前识别(业务预估 + 监控 --hotkeys、代理层统计),预热 + 限流兜底。

Redis 单线程执行命令,DEL 一个百万元素的集合要同步释放所有节点内存,耗时可达百毫秒级,阻塞其他请求。UNLINK 把键从键空间摘除(O(1)),真正的内存释放在后台线程异步完成。

Pipeline 和事务的区别?

  • Pipeline :把 N 条命令一次性打包发送、批量接收响应,节省 N-1 次网络 RTT;不保证原子性,中间可插入其他客户端命令;
  • 事务 MULTI/EXEC :命令入队、EXEC 时顺序原子执行 (不被插队),但没有回滚 ------入队阶段语法错误整组不执行;运行时某条出错(如对 String 用 LPUSH),其他命令照常执行;也不提供隔离性保证(watch 除外)。

需要原子+条件判断 → Lua 脚本(EVAL 执行期间原子执行,是分布式锁等场景的标准做法)。

Redis 事务为什么不支持回滚?

作者设计哲学:运行期错误是编程错误(生产环境不该出现),回滚不能拯救逻辑 bug 反而拖慢正常路径;Redis 命令只操作内存数据结构,运行时错误极少。所以干脆不做回滚,保持简单高效。

慢查询怎么排查?

  1. SLOWLOG GET 看慢日志(slowlog-log-slower-than 默认 10ms);
  2. redis-cli --latency / --bigkeys / --hotkeys 定位;
  3. 检查:大 key、复杂命令(KEYS、SMEMBERS 全量拉取)、fork 卡顿(bgsave/AOF rewrite 期间延迟尖刺)、Swap、网络;
  4. 严禁生产使用 KEYS * ,用 SCAN 渐进遍历(游标式,不阻塞)。

其他高频知识点

Redis 单线程为什么还要考虑并发安全?

虽然命令执行单线程,但一个业务操作 = 多条命令(如先 GET 再 SET),两条命令之间会被其他客户端插入,产生竞态。解决:Lua 脚本原子执行、INCR/DECR 等原子命令、分布式锁。

如何用 Redis 实现排行榜?

ZSet:ZINCRBY rank:20261008 1 userId 加分,ZREVRANGE rank:20261008 0 9 WITHSCORES 取 Top10,ZREVRANK 查自己排名。分数更新 O(log n),天然有序。

如何用 Redis 实现签到和统计?

BitMap:SETBIT sign:{userId}:{yyyyMM} day 1,BITCOUNT 统计月签到天数,BITOP 可以做多天活跃交集。连续签到可用位运算或 Stream 记录。

Redis 做消息队列的几种方式?

  1. List:LPUSH + BRPOP(阻塞读),简单但无 ACK、无消费者组,可能丢消息(取走未处理完宕机);
  2. Pub/Sub :广播,不持久化,离线收不到,只适合通知类;
  3. Stream(5.0+):消费者组、ACK、Pending 列表重试,最接近 MQ 语义。

对比:可靠性要求高还是用 RocketMQ/Kafka,Redis MQ 适合轻量场景。

布隆过滤器之外,还有哪些"缓存友好"的数据结构技巧?

  • 统计 UV:HyperLogLog(PFADD/PFCOUNT),12KB 固定内存;
  • 附近的人:GEOADD + GEORADIUS;
  • 延时任务:ZSet 分数存执行时间戳,定时任务 ZRANGEBYSCORE 轮询取到期任务。

Redis 有哪些应用场景?一图流总结

场景 实现
缓存 Cache Aside + 过期策略
分布式锁 SETNX + Lua + Redisson
计数器/限流 INCR + EXPIRE
排行榜 ZSet
签到/活跃 BitMap
UV 统计 HyperLogLog
附近的人 GEO
消息队列 List / Stream
全局 ID INCR
会话共享 Spring Session + Redis
相关推荐
Azure++2 天前
Spring 面试题笔记
个人笔记
Azure++5 天前
Spring Boot 面试题笔记
个人笔记
Azure++5 天前
MyBatis 面试题笔记
个人笔记
Azure++5 天前
JVM 面试题笔记
个人笔记
我的golang之路果然有问题9 个月前
linux 个人笔记导出之网络,防火墙,定时,权限,后台
linux·运维·服务器·网络·笔记·个人笔记
我的golang之路果然有问题9 个月前
mysql 个人笔记导出之-数据库时间戳问题以及增删改查
数据库·笔记·学习·mysql·分享·个人笔记