按模块归类:先知识点串讲,后「面经真题」。★ 为面试出现频率。 口语化回答是”面试时怎么说”,不是”书上怎么写”。


一、Redis 为什么这么快

知识点

  • 纯内存操作:数据在内存读写,不走磁盘 I/O,是快的根本原因(单机 10w+ QPS)
  • 单线程命令执行:避免锁竞争与上下文切换,并发模型简单
  • I/O 多路复用:基于 epoll,一个线程监听大量连接,事件就绪再处理
  • 高效数据结构:SDS、跳表、压缩列表、渐进式 rehash 哈希表
  • 加分点(6.0+):多线程只用于网络 I/O 与协议解析,命令执行仍是单线程

面试问答

Q1:Redis 是单线程的,为什么还能这么快?

A: 根本原因是数据全在内存,不碰磁盘,瓶颈在内存容量和网络带宽,不在 CPU。单线程反而省掉了锁竞争和线程切换的开销,再用 epoll 一个线程扛住所有连接,所以单线程不是劣势,是取舍。

Q2:既然是单线程,CPU 打满了怎么办?(追问率极高) A: 单实例确实用不满多核,标准做法是单机部署多实例或者上 Cluster 分片,把 CPU 和内存都横向用起来。

Q3:Redis 6.0 为什么又引入多线程?命令是并行执行的吗?

A: 因为网络带宽上来之后,读写 socket 和协议解析成了新瓶颈,所以把网络 I/O 做成多线程。但命令执行仍然是单线程——这个一定要说准,说成”6.0 变多线程了”就露馅了。

Q4:Redis 和 Memcached 怎么选?

A: 四个维度:数据类型 Redis 丰富(Memcached 只有 kv);持久化 Redis 支持而 Memcached 重启丢数据;集群 Redis 原生支持;Redis 还有发布订阅、Lua、事务。现在基本无脑 Redis。


二、数据类型与应用场景

知识点

  • String:缓存 JSON、计数器(incr)、分布式锁(set nx ex)
  • Hash:字段单独读写;购物车(userId → {商品Id: 数量})
  • List:双向链表;最新列表、简单消息队列(lpop/brpop)
  • Set:去重;抽奖(spop)、共同关注(sinter)
  • ZSet:按 score 排序;排行榜(zincrby/zrevrange)、延时队列(score 存到期时间)
  • BitMap 签到、HyperLogLog UV(12KB 固定内存)、GEO 附近的人、Stream 消息队列

面试问答

Q1:让你设计一个直播间礼物排行榜,怎么实现?

A: ZSet,主播 id 作 key,用户 id 作 member,送礼金额作 score。送礼时 zincrby 原子加分,榜单用 zrevrange withscores 取前 N,实时排名 zrank 直接查。要小时榜就给 key 加过期时间,一小时一个。

Q2:ZSet 底层为什么用跳表,不用红黑树?(大厂高频) A: 三点:范围查询天然占优——zrange 就是沿链表遍历,红黑树要做中序遍历;实现简单得多,插入删除不用旋转变色,排查问题也容易;内存灵活,节点层数按概率生成,属于用概率换复杂度。这是作者 antirez 的原话思路。

Q3:统计一个千万 DAU 产品的 UV,Set 和 HyperLogLog 怎么选?

A: 要精确就用 Set,但一个活动千万用户内存直接爆炸。能接受 0.81% 左右的误差就用 HyperLogLog,固定 12KB,pfadd/pfcount 两条命令。生产上 UV 这种运营指标一般都接受误差换内存。

Q4:怎么用 Redis 存全量用户签到?

A: BitMap,一天一个 key,用户 id 作偏移量,签到就 setbit 为 1。5000 万用户一天才 6MB 左右,bitcount 算总数,bitop and 算连续签到。


三、缓存穿透、击穿、雪崩

知识点

  • 穿透:查”缓存与 DB 都不存在”的数据;解法:布隆过滤器、缓存空值(短 TTL)
  • 击穿:热点 key 过期瞬间并发回源;解法:互斥锁、逻辑过期+异步更新
  • 雪崩:大量 key 同时过期或 Redis 故障;解法:TTL 加随机值、高可用集群、限流熔断降级
  • 口诀:穿透=查”没有的”;击穿=“一个热点”倒了;雪崩=“一大片”全倒

面试问答

Q1:讲讲缓存穿透、击穿、雪崩,分别怎么解决?

A: 先各一句话定义,再逐个给方案,最后补口诀收尾(见上)。关键区别主动点出来:雪崩击穿的数据在 DB 里存在,缓存恢复就没事;穿透的数据压根不存在,必须从入口拦。

Q2:布隆过滤器的原理?它有什么坑?(穿透的标准追问) A: 一个长 bit 数组加 N 个哈希函数,写入时把 N 个哈希位置置 1,查询时 N 个位置全是 1 才可能存在。特性反过来记:说”不存在”就一定不存在,说”存在”可能误判。坑有两个:有误判率;标准实现不能删元素,删了会影响别的 key——要删得上计数布隆过滤器。

Q3:互斥锁重建缓存,有什么代价?

A: 拿不到锁的线程要阻塞等待,吞吐下降;锁实现不好还可能死锁。所以一般是”锁 + 短超时 + 拿不到锁先返回旧值或空值”的组合,不会硬等。


四、持久化:RDB 与 AOF

知识点

  • RDB 快照:save 阻塞 / bgsave fork 子进程(主流);文件小恢复快,两次快照间丢数据
  • AOF 日志:先执行命令成功再记录(防记错命令、不阻塞当前写)
  • 刷盘:always / everysec(默认推荐)/ no
  • AOF 重写:bgrewriteaof fork 子进程,用”等价最小命令集”重建,解决膨胀
  • 混合持久化(4.0+):重写时 RDB 全量 + 增量命令,生产常用

面试问答

Q1:AOF 是先写日志还是先执行命令?和 MySQL 一样吗?(爱考) A: Redis 是先执行成功再记日志,MySQL 的 redo log 正好相反,先写日志再改数据(WAL)。Redis 这么选是为了不记录可能出错的命令、也不阻塞当前写命令,代价是宕机可能丢最后一条。

Q2:bgsave 期间还能写数据吗?怎么保证快照数据一致?

A: 能写。fork 之后父子进程靠写时复制共享内存页,子进程看到的是 fork 那一刻的快照,主进程要改哪页就复制哪页再改。所以快照一致性天然成立。可以补一句:fork 瞬间要复制页表,主线程会短暂阻塞,大内存实例要留余量——这句是加分点。

Q3:生产上 RDB 和 AOF 怎么选?

A: 能接受分钟级丢失用纯 RDB;一点不能丢用 AOF everysec;4.0 以后标准答案是混合持久化,恢复快丢得少。


五、过期删除与内存淘汰

知识点

  • 过期删除 = 惰性 + 定期:惰性=访问时检查;定期=默认每 100ms 随机抽 20 个带过期的 key,过期比例超 25% 就继续抽,单轮有时间上限
  • 内存淘汰 8 种,重点:noeviction(默认,写报错)、allkeys-lru(缓存最常用)、volatile-lru、allkeys-lfu
  • Redis 是近似 LRU:随机采样若干 key(默认 5 个)淘汰最久未用的,不用链表,省内存
  • LFU(4.0):频次计数会随时间衰减,专治”偶发批量访问赖着不走”的缓存污染
  • 对比:InnoDB 是严格 LRU 链表 + new/old 分区,防全表扫描冲掉热点页

面试问答

Q1:给 key 设了过期时间,到点就会被删吗?

A: 不一定。Redis 是惰性删除加定期删除的组合:访问时顺手删,加上默认每 100ms 抽样检查。所以过期 key 可能晚删,这就是两种策略互相打配合的原因——惰性删除兜底精确性,定期删除兜底内存占用。

Q2:Redis 的 LRU 和教科书 LRU 有什么区别?为什么这么做?

A: 教科书 LRU 要全局双向链表,每次访问都移动节点,内存和 CPU 开销都大。Redis 用近似 LRU:对象头 24 个 bit 记最后访问时间,淘汰时随机采样 5 个 key 挑最久没用的。省内存省 CPU,代价是不那么精确——工程上的合理取舍。

Q3:什么场景你会把淘汰策略从 allkeys-lru 换成 allkeys-lfu?

A: 出现缓存污染的时候——偶发的批量扫描把一堆冷数据拉进了缓存,LRU 会误以为它们是热的。LFU 计数会衰减,冷数据很快掉出去,适合”有明显热点 + 偶发扫全量”的业务。


六、高可用:主从 · 哨兵 · 集群

知识点

  • 主从复制:replicaof 后先全量(RDB),再增量(repl_backlog 环形缓冲区);缺点是无自动故障转移
  • 哨兵:主观下线(单个哨兵 ping 不通)→ 客观下线(超 quorum 个同意)→ 哨兵选 leader 执行转移 → 按优先级、复制偏移量、runid 选新主
  • Cluster:16384 槽分给多主;CRC16(key) 对 16384 取模定位;gossip 通信;key 不在本节点返回 moved 重定向

面试问答

Q1:完整说一下主从第一次同步的过程。

A: 三阶段:①从库发 psync 带主库 id 和 offset,主库判断是第一次,回复全量同步;②主库 bgsave 生成 RDB 发过去,这期间的新写命令攒在 replication buffer 里;③从库清空旧数据、载入 RDB,主库把攒的命令补发,之后进入命令传播长连接。

Q2:全量同步期间主库的新写命令会不会丢?(追问率极高) A: 不会,先攒在 replication buffer,从库载入完 RDB 再补发。断线重连则是靠 repl_backlog 环形缓冲区做增量,offset 还在里面就只补差值,被覆盖了才重新全量——所以写流量大的集群要调大 repl_backlog(默认 1MB)。

Q3:为什么判断下线要分主观和客观两步?

A: 单个哨兵 ping 不通可能只是它自己网络抖,直接切主会误伤。问一圈其他哨兵,超过 quorum 个都说下线才算真挂——多数派排除个体误判,这和分布式共识的思路一致。

Q4:为什么哈希槽是 16384 个?moved 和 ask 有什么区别?

A: 16384 是因为心跳包里要带槽位表 bitmap,16384 个槽只要 2KB,65536 要 8KB,而集群设计上限约 1000 节点,够用了(antirez 本人的回答)。moved 是槽已经确定归别的节点,客户端要更新本地的槽位表;ask 是槽正在迁移中的临时重定向,不更新缓存。


七、缓存与数据库一致性(真题出现率极高,别漏)

知识点

  • 主流方案 Cache Aside:读=先缓存后 DB;写=先更新 DB 再删缓存
  • 为什么删缓存不更新缓存:避免白写 + 并发写把旧值写回
  • 为什么先库后缓存:反过来会并发读回旧值常驻缓存;先库后缓存不一致窗口极小
  • 兜底:延迟双删;订阅 binlog(canal)异步删 + 消息队列重试

面试问答

Q1:更新数据时,是先更新数据库还是先删缓存?

A: 先更新数据库再删缓存。反过来的话,删掉缓存的一瞬间来了个并发读,会把 DB 旧值写回缓存常驻;而先库后缓存的不一致只在”缓存恰好失效且读比写还慢”的极小概率下出现。最后补一句:只能保证最终一致,强一致要上锁但得不偿失。

Q2:删缓存失败了怎么办?

A: 把删除动作塞进消息队列重试;更稳的是订阅 binlog 异步删,天然保证最终一致——数据库变更一定触发删除,链路还解耦。


八、分布式锁(手撕级别高频)

知识点

  • 加锁:set key 唯一值 nx px 过期时间(一条原子命令)
  • 解锁:Lua 脚本”get 校验是自己的值,再 del”两步原子
  • 续期:看门狗定时续期(Redisson 默认每 1/3 锁周期续一次)
  • 主从切换丢锁 → Redlock(多主多数派),有争议(Martin Kleppmann vs antirez)

面试问答

Q1:用 Redis 怎么实现分布式锁?说说演进过程。

A: 按踩坑顺序讲:setnx + expire 两条命令 → 中间宕机死锁 → 合成 set nx px 一条;业务没跑完锁过期 → 误删别人的锁 → value 放唯一随机值 + Lua 原子解锁;还是怕业务超时 → 看门狗自动续期。把”坑 → 修”的因果链讲出来就是满分答案。

Q2:解锁为什么必须用 Lua?

A: get 校验和 del 是两步,中间锁可能恰好过期被别人抢走,你就删了别人的锁。Lua 脚本在 Redis 里原子执行,校验和删除不可分割。

Q3:主从架构下分布式锁还有什么问题?

A: 主库加锁成功还没同步给从库就挂了,从库顶上来没有这把锁,两个客户端同时持锁。Redlock 向多个独立主节点加锁、多数成功才算持有,但它的安全性有过著名争议(GC 停顿、时钟跳变都可能破坏它),生产更常见单实例 + 看门狗,或者对正确性要求高就换 etcd/zookeeper。


九、大 Key 与热 Key(近年真题新宠)

知识点

  • 大 key 经验值:String 价值上 MB 级、集合类元素数万级就算大(各厂标准不同,说”经验值”)
  • 危害:删除阻塞主线程(del 同步释放)、网卡被打满、集群数据倾斜、主从同步延迟
  • 处理:拆分(大集合按 hash 分桶)、删除用 unlink 异步、遍历用 SCAN 不用 keys、redis-cli —bigkeys 巡检
  • 热 key:单 key QPS 高到打满单节点;本地缓存兜一层 / 多副本打散

面试问答

Q1:什么是大 key?怎么发现、怎么治理?

A: 发现:redis-cli —bigkeys 采样巡检,或 RDB 离线分析工具。治理三板斧:拆——大 JSON 拆成 Hash 按字段存,大集合按分桶拆到多个 key;删——用 unlink 交给后台线程,别用 del 卡主线程;防——代码评审卡集合类无上限增长。危害要能说出来:删除卡顿、同步延迟、集群倾斜。

Q2:某个明星官宣,一个 key 每秒几十万读,怎么扛?

A: 这是典型热 key。两层方案:读走本地缓存——应用内存里兜一层 Caffeine,热点 key 秒级失效策略;或者 key 打散——写多份 user:1000:1 到 :N,读的时候随机挑一个,流量摊到多个节点。发现手段是 redis-cli —hotkeys 或客户端埋点统计。