Redis 面试题笔记
个人复习笔记,覆盖 Redis 数据结构、持久化、缓存三大问题、分布式锁、高可用架构的高频面试题。
目录
Redis 基础认知
Redis 是什么?有哪些特点?
基于内存的键值 NoSQL 数据库,常用作缓存、分布式锁、消息队列。
特点:
- 纯内存操作,读写极快(10w+ QPS);
- 丰富的数据结构(String、Hash、List、Set、ZSet 等);
- 支持持久化(RDB/AOF)、主从复制、哨兵、集群;
- 单线程处理命令(6.0 后网络 IO 多线程),无并发竞争问题;
- 支持 Lua 脚本、事务、发布订阅、Pipeline。
Redis 为什么这么快?(高频)
- 纯内存读写,无磁盘 IO;
- 单线程命令处理:无锁、无线程上下文切换开销(瓶颈在网络与内存,不在 CPU);
- IO 多路复用(epoll):单线程高效监听大量连接,非阻塞 IO;
- 高效的数据结构:SDS 动态字符串、跳表、压缩列表/listpack、渐进式 rehash 的哈希表等;
- 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),空间换时间。
不用红黑树的原因:
- 实现更简单,代码好维护;
- 范围查询更方便(ZRANGE 顺着链表遍历即可,红黑树要中序遍历+回溯);
- 插入删除只需改前后指针,无需旋转;
- 按概率维护多层,无需像 AVL/红黑树那样严格平衡。
SDS 相比 C 字符串的好处?
- O(1) 获取长度(结构体存了 len,C 字符串要 strlen 遍历);
- 二进制安全(按 len 读,不怕内容里有 \0,可存图片序列化数据);
- 杜绝缓冲区溢出(扩容前检查 + 空间预分配);
- 惰性释放减少内存重分配次数。
持久化机制
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 采用 惰性删除 + 定期删除 组合:
- 惰性删除:访问 key 时检查是否过期,过期则删除并返回 nil。省 CPU,但冷数据一直占内存;
- 定期删除:默认每 100ms 随机抽查一批设置了过期时间的 key,删除其中的过期键;若过期比例超过 1/4,继续下一轮抽查(限制时间避免阻塞)。
- 兜底:内存达到 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 加锁流程?
tryLock执行 Lua:hash 结构(key=锁名,field=客户端ID:线程ID,value=重入次数)+ pexpire,支持可重入;- 拿不到锁则订阅解锁消息 + 信号量等待 + 自旋重试;
- 看门狗自动续期;
- 释放时 hash 减 1,减到 0 删除锁并发布解锁消息。
RedLock 了解吗?有争议吗?
RedLock(红锁):向 N 个独立 Redis 主节点同时加锁,半数以上 成功且耗时小于锁有效期才算获取成功,防止单点故障导致锁失效。争议(Martin Kleppmann vs antirez 论战):Redis 主从异步复制下,主挂了锁数据没同步到从,新主上锁丢失,可能出现两个客户端同时持锁 ------RedLock 只能缓解不能根除。实践建议 :Redis 锁用于效率场景(防重复),正确性要求高的场景用 ZooKeeper/etcd 锁或数据库唯一约束。
高可用架构
主从复制的原理?(高频)
- 首次全量同步:从库发 psync → 主库 bgsave 生成 RDB 发给从库 → 从库清空数据载入 RDB → 主库把期间的写命令缓冲发给从库;
- 后续增量同步:主库把写命令实时通过 replication buffer 发给从库;断线重连后,根据 offset 和复制积压缓冲区(repl_backlog)尝试部分重同步,offset 被覆盖则重新全量;
- 默认从库只读,读写分离时注意主从延迟导致读到旧数据。
哨兵(Sentinel)的作用和原理?
哨兵是独立的监控进程集群(至少 3 个,奇数):
- 监控:每秒 ping 主从库;
- 主观下线:单个哨兵认为主库无响应;
- 客观下线:quorum 个哨兵都认为下线,才确认故障;
- 选主:哨兵之间 Raft 式选举出 leader 执行故障转移;
- 故障转移:从从库中按优先级/offset/runid 选出新主,其余从库改挂新主,通知客户端(通过发布订阅 +provider)。
Cluster 集群模式?为什么是 16384 个槽?(高频)
- 全部 key 按公式
slot = CRC16(key) mod 16384分到 16384 个槽,节点负责一部分槽;客户端可直连任意节点,节点返回 MOVED/ASK 重定向,智能客户端本地缓存槽位映射表; - 节点间 Gossip 协议 交换状态;主节点挂掉,其从节点自动顶上;
- 不支持多 key 跨槽操作 (可用 hash tag
{user1000}让相关 key 同槽)、只支持 db0。
为什么 16384 而不是 65536(作者回答):
- 心跳包携带槽位图,16384 位 = 2KB,65536 位 = 8KB,心跳太占带宽;
- Redis Cluster 建议节点数不超过 1000,16384 个槽足够分;
- 槽越少,节点少时压缩率越高。
哨兵和 Cluster 怎么选?
- 数据量小、QPS 中等:主从 + 哨兵,架构简单;
- 数据量大(单机内存放不下)、需要水平扩容和分片写入:Cluster。
性能与常见问题
什么是大 key?怎么解决?
定义(经验值):String 超过 10KB 、集合元素超过 5000 或总大小超 10MB。
危害:网络拥塞、单线程阻塞(删除/序列化耗时)、主从同步延迟、内存不均。
解决:
- 拆分:大 Hash 按字段 hash 到多个 key,大 List 分段;
- 压缩 value(先压缩再存);
- 删除用
UNLINK(异步删除)代替 DEL,或SCAN渐进清理; - 监控发现:
redis-cli --bigkeys、RDB 离线分析。
什么是热 key?怎么解决?
某个 key 被超高频率访问(如明星热搜、爆款商品),流量集中打到一个节点。
解决:
- 本地缓存(Caffeine/Guava)挡在最前面;
- 热点 key 多副本 :
key_1、key_2...随机读,分散到不同节点; - 读写分离,从库分担读压力;
- 提前识别(业务预估 + 监控
--hotkeys、代理层统计),预热 + 限流兜底。
深入:删除 DEL 为什么会阻塞?UNLINK 好在哪?
Redis 单线程执行命令,DEL 一个百万元素的集合要同步释放所有节点内存,耗时可达百毫秒级,阻塞其他请求。UNLINK 把键从键空间摘除(O(1)),真正的内存释放在后台线程异步完成。
Pipeline 和事务的区别?
- Pipeline :把 N 条命令一次性打包发送、批量接收响应,节省 N-1 次网络 RTT;不保证原子性,中间可插入其他客户端命令;
- 事务 MULTI/EXEC :命令入队、EXEC 时顺序原子执行 (不被插队),但没有回滚 ------入队阶段语法错误整组不执行;运行时某条出错(如对 String 用 LPUSH),其他命令照常执行;也不提供隔离性保证(watch 除外)。
需要原子+条件判断 → Lua 脚本(EVAL 执行期间原子执行,是分布式锁等场景的标准做法)。
Redis 事务为什么不支持回滚?
作者设计哲学:运行期错误是编程错误(生产环境不该出现),回滚不能拯救逻辑 bug 反而拖慢正常路径;Redis 命令只操作内存数据结构,运行时错误极少。所以干脆不做回滚,保持简单高效。
慢查询怎么排查?
SLOWLOG GET看慢日志(slowlog-log-slower-than 默认 10ms);redis-cli --latency/--bigkeys/--hotkeys定位;- 检查:大 key、复杂命令(KEYS、SMEMBERS 全量拉取)、fork 卡顿(bgsave/AOF rewrite 期间延迟尖刺)、Swap、网络;
- 严禁生产使用 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 做消息队列的几种方式?
- List:LPUSH + BRPOP(阻塞读),简单但无 ACK、无消费者组,可能丢消息(取走未处理完宕机);
- Pub/Sub :广播,不持久化,离线收不到,只适合通知类;
- 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 |