Redis 面试题


1. Redis 缓存穿透、击穿、雪崩

题目: 请分别说明缓存穿透、缓存击穿、缓存雪崩的概念、产生原因以及各自的解决方案。

追问1:布隆过滤器解决缓存穿透时,如果布隆过滤器判断”存在”就真的一定存在吗?那删除缓存数据时怎么办? 追问2:热点 key 的缓存击穿如果用互斥锁解决,锁的粒度如何设计?会不会变成性能瓶颈?

💡 答案:

主问题: 这三个问题名字相似但场景完全不同。缓存穿透是指查询一个数据库中根本不存在的 key,缓存没有命中,请求直接打到数据库。因为缓存和数据库中都没有,每次查询都会穿透到数据库,恶意攻击者可以用大量不存在的 key 把数据库打死。解决方案有两种:一是缓存空值——对查询结果为空的数据也做缓存,过期时间设短一些;二是使用布隆过滤器,把所有可能的 key 预先加载到布隆过滤器中,查询前先判断 key 是否存在。缓存击穿是指某个热点 key 在缓存过期的瞬间,大量并发请求同时打到数据库,因为缓存刚好在这一刻失效。解决方案:一是互斥锁——让一个线程去查数据库并回写缓存,其他线程等待;二是”永不过期”——物理上不设过期时间,用逻辑过期来异步更新。缓存雪崩是指大量缓存 key 在同一时间段集中过期,或者 Redis 集群宕机,导致所有请求瞬间涌向数据库。解决方案:一是给过期时间加上随机值,避免集中过期;二是使用主从集群加哨兵或 Cluster 模式保证高可用;三是服务降级和限流,在缓存不可用时保护数据库。

追问1: 布隆过滤器有一个关键特性:判断”不存在”则一定不存在,判断”存在”则可能存在(有误判率)。这是因为布隆过滤器通过多个哈希函数把元素映射到位数组中,不同元素可能映射到相同的 bit 位产生碰撞。所以即使布隆过滤器说”存在”,也可能是一个不存在的 key 恰好命中了其他 key 的哈希位置,此时还是需要查缓存和数据库确认。删除更棘手——布隆过滤器的位数组不支持直接删除,因为一个 bit 可能被多个 key 共用,删了会影响其他 key。解决方案是使用计数布隆过滤器(Counting Bloom Filter),把 bit 位扩展为计数器,插入加一、删除减一,但代价是内存占用成倍增长。实际工程中如果数据变化不频繁,更常见的做法是定期重建布隆过滤器,而不是实时删除。

追问2: 互斥锁解决缓存击穿的关键在于锁的粒度。如果对整个查询方法加锁,所有请求都会串行化,性能极差。正确的做法是以”缓存 key”为粒度加锁——不同 key 的请求可以并发执行,只有同一个 key 的请求需要排队。实现上可以用 Redis 的 SETNX 命令:设置一个锁 key(比如 lock:product:1001),设置成功的线程执行数据库查询和回写,其他线程自旋等待一段时间后重新查缓存。这里还有一个细节:如果加锁成功但回写缓存失败,锁没有释放,后续请求就永远拿不到锁。所以锁必须设置过期时间,过期时间不宜太短(否则锁提前释放导致多个线程同时查库),也不宜太长(否则故障恢复慢),一般设个 5-10 秒比较合理。另外可以用 Redisson 这类分布式锁框架,它提供了自动续期机制,能避免业务执行时间超过锁过期时间的问题。

📌 易错点 / 加分项:

  • 三个概念很多人搞混,核心区别:穿透是查”不存在的数据”、击穿是”单个热点 key 过期”、雪崩是”大量 key 同时过期或集群挂”
  • 布隆过滤器不能删除是它的核心缺陷,能用这个话题引出计数布隆过滤器会很加分
  • 能提到逻辑过期 + 异步更新的方案(类似 Caffeine 的 refreshAfterWrite)

2. Redis 过期删除与内存淘汰策略

题目: Redis 过期 key 的删除策略是什么?为什么不用定时全部扫描删除?

追问1:Redis 的内存淘汰策略有哪些?LRU 和 LFU 的核心区别是什么? 追问2:一个 key 设置了过期时间但到时没有立即删除,什么情况下客户端还读到它?这算 bug 吗?

💡 答案:

主问题: Redis 采用”惰性删除 + 定期删除”两种策略组合。惰性删除是指客户端访问 key 时,Redis 先检查 key 是否过期,如果过期了就删除并返回空,不主动触碰。定期删除是指 Redis 每秒执行 10 次(由 hz 参数控制)周期性任务,每次从设置了过期时间的 key 中随机抽取若干个检查并删除过期的 key,每次任务有最大执行时间限制(25ms),如果到时间还没删完就等下一次。仅靠惰性删除不够——有些 key 过期后再也没有被访问过,如果没被别的机制清理就会一直占用内存;仅靠定期删除也不够——定时全量扫描过期字典性能代价太大(可能有几十万个过期 key,一次全扫描会导致 Redis 的单线程被长时间阻塞)。两者组合加上内存淘汰策略形成了一个分层的过期处理机制。

追问1: Redis 提供了八种内存淘汰策略,核心分为三大类:不淘汰(默认的 noeviction,内存满后写操作直接报错)、全量淘汰(allkeys-lruallkeys-lfuallkeys-random,对所有 key 按对应算法淘汰)、带过期时间的淘汰(volatile-lruvolatile-lfuvolatile-randomvolatile-ttl,只淘汰设了过期时间的 key)。LRU(Least Recently Used)的核心思想:最近被访问过的 key 更有可能再次被访问,淘汰最近最少使用的 key。Redis 实现的不是严格 LRU,而是近似 LRU——随机采样 N 个 key(由 maxmemory-samples 控制,默认 5)淘汰其中最久未被访问的那个。LFU(Least Frequently Used)引入了”访问频率”维度,不仅看最近访问还要看访问次数,通过一个 8bit 的计数器统计频率,这个计数器会有衰减机制——随时间流逝频率值自动降低。LFU 解决了 LRU 的一个盲点:那些偶尔被大量访问的冷数据在 LRU 下可能比真正热数据更”新”而逃过淘汰,LFU 能更好地区分”真正的热数据”和”偶发性访问的冷数据”。

追问2: 会读到,这不算是 bug,而是过期删除机制的必然结果。惰性删除只在访问时触发,定期删除是随机采样而非全量扫描——一个过期的 key 可能恰好没有被采样到,也没有被访问,就一直留在内存中。当客户端访问它时,惰性删除机制先发现它过期、删除、返回空——这个过程对客户端来说是正常的。但如果 key 已经逻辑过期但物理上还在内存中,主库里的 key 过期后会写入一条 del 命令到 AOF 和同步到从库保证主从一致。在 Redis 4.0 之前从库没有惰性删除——即使 key 已过期,从库必须等待主库同步 del 命令,如果有客户端读从库可能读到过期数据。Redis 4.0 后从库也加入了惰性删除检查机制。

📌 易错点 / 加分项:

  • Redis 不用定时全量扫描的核心原因是单线程——不能因为清理操作阻塞命令执行
  • maxmemory 设为 0 在 64 位系统表示”不限制内存”,32 位系统隐式为 3GB
  • LFU 的 8bit 计数器如何在有限精度下平衡新旧数据是一个很好的算法设计题

3. Redis 持久化:RDB vs AOF

题目: 请详细对比 Redis 的 RDB 和 AOF 两种持久化方式,各自适用于什么场景?

追问1:AOF 的重写机制是如何工作的?重写过程中新来的写请求会丢失吗? 追问2:Redis 7.0 中 AOF 做了哪些改进?为什么去掉了 AOF 重写的缓冲区?

💡 答案:

主问题: RDB 是快照持久化,将某个时间点 Redis 的内存数据完整写入磁盘文件。触发方式可以是 SAVE(阻塞主线程立刻执行)或 BGSAVE(fork 子进程后台写入)。优势是文件紧凑、恢复速度快,适合做灾备和冷备。劣势是 fork 子进程时如果内存大的话可能造成毫秒级阻塞,而且两次快照之间的数据如果宕机会全部丢失。AOF 是追加写日志,将每一条修改 Redis 数据的写命令追加记录到文件中。优势是数据安全性高(可配置每命令 fsync、每秒 fsync),丢失数据最多一秒;劣势是文件体积大、恢复速度慢。生产环境典型配置是两者同时开启:用 RDB 做冷备保证恢复速度,用 AOF 保证数据安全性。

追问1: AOF 重写的核心思想不是”读原 AOF 文件再压缩写入”,而是”读取当前数据库的快照状态,反向生成写入命令”。Redis fork 出一个子进程,子进程读取当前内存中的数据状态,生成一串可以重建这些数据的最小化命令集合写入新的 AOF 文件。在子进程重写期间,主进程继续接收写请求,这些新命令会被写入”AOF 重写缓冲区”。子进程写完新文件后,主进程将重写缓冲区中积累的增量命令追加到新文件末尾,然后原子地把新文件替换旧文件(rename)。因为重写过程中主进程把新增命令写入了重写缓冲区,所以不会丢失。这个过程中 fork 子进程的那一刻内存数据和当前内存数据是独立的(copy-on-write),父进程和子进程互不干扰。

追问2: Redis 7.0 最大的改进是引入了 Multi-Part AOF。之前 AOF 是单文件,重写时同时存在旧文件和新文件 + 重写缓冲区。7.0 将 AOF 设计为 base + incr 的多文件架构:base 文件是某一时刻的完整快照,incr 文件记录后续的增量变化。文件数量由 aof-use-rio 参数控制。去掉了重写缓冲区的主要原因是:在旧方案中重写缓冲区本质上是内存中的缓冲(重写期间积累的命令),如果增量命令很多会消耗大量内存。7.0 的 Multi-Part AOF 方案中,重写时直接将增量写入新的 incr AOF 文件,不需要在内存中缓冲。还有一个改进是 manifest 文件——类似”目录”来管理哪些 base 和 incr 文件组成当前 AOF,管理更清晰,恢复时也更快。

📌 易错点 / 加分项:

  • AOF 重写是由 auto-aof-rewrite-percentageauto-aof-rewrite-min-size 两个参数触发的,不是定时触发
  • 不能只用 RDB——万一宕机丢数据很严重,至少配合每秒 fsync 的 AOF
  • 混合持久化(RDB preamble in AOF)是 Redis 4.0 引入的,将 RDB 数据部分作为 AOF 文件的开头

4. Redis 主从复制与哨兵机制

题目: Redis 主从复制的工作流程是怎样的?复制过程中主库宕机了怎么办?

追问1:哨兵(Sentinel)模式是如何判断主库下线的?主观下线和客观下线有什么区别? 追问2:Redis Cluster 和哨兵方案各自适用于什么规模?Cluster 的数据分片机制是怎样的?

💡 答案:

主问题: Redis 主从复制分为全量复制和增量复制两个阶段。从库首次连接主库,或者主从断开时间过长导致复制积压缓冲区被覆写时,执行全量复制:主库 fork 子进程生成 RDB 快照发给从库,从库清空旧数据加载 RDB,在此期间主库新增的命令写入 client-output-buffer 缓存,RDB 加载完后主库把缓存的增量命令发给从库执行,从库追上主库状态。之后进入增量复制阶段:主库每执行一条写命令,将命令写入自己的复制积压缓冲区并通过网络发给所有从库,从库接收并执行。主库宕机时,需要哨兵或手动将从库提升为新主库,这个过程涉及选举、配置更新和数据同步。

追问1: 哨兵判断主库下线分为两步:主观下线(SDOWN)和客观下线(ODOWN)。主观下线是单个哨兵通过定期 PING 主库判断——如果在 down-after-milliseconds 时间内主库无响应,这个哨兵认为主库”主观下线”。但这只是一个哨兵的看法,可能是网络抖动误判。客观下线是这个哨兵向其他哨兵发起询问(SENTINEL is-master-down-by-addr),如果达到法定人数 quorum 认为主库也下线了,则判定为”客观下线”,开始执行故障转移。故障转移流程:哨兵 leader 选举(Raft 算法选出一个哨兵来执行),选出一个最优的从库提升为新主库(优先级最高、复制偏移量最大、runid 最小),通知其他从库改认新主库。

追问2: 哨兵方案适用于”一个主库 + 多个从库”的架构,数据全部在主库上,从库做备份和读分流。当数据量超过单机内存极限时哨兵就不好使了。Redis Cluster 是去中心化的分布式方案,通过哈希槽(Hash Slot,一共 16384 个槽)将数据分片到多个主节点,每个主节点负责一部分槽。客户端连接任意节点,通过 MOVED 重定向和 ASK 转向最终路由到正确的节点。Cluster 方案的扩展方式是增加节点,槽和数据自动迁移。生产选型上:数据量小于单机内存上限时用哨兵方案足够,简单运维成本低;数据量超过单机上限、或者业务需要水平扩展时上 Cluster。

📌 易错点 / 加分项:

  • client-output-buffer 溢出会导致全量复制失败并重试,这是主从复制一个常见的坑
  • 哨兵不是三个就够——quorum 设 2 哨兵数设 5 的配置在生产中错误案例很多
  • Cluster 模式下多 key 操作(mget、事务)要求所有 key 在同一个 slot,跨 slot 不能做

5. Redis 分布式锁的正确实现

题目: 用 Redis 实现一个分布式锁需要考虑哪些问题?SET lock_key value NX EX 10 够吗?

追问1:Redisson 的看门狗(WatchDog)机制解决了什么问题?其原理是什么? 追问2:Redis Cluster 环境下分布式锁有什么额外风险?RedLock 算法是如何尝试解决的?

💡 答案:

主问题: SET lock_key value NX EX 10 实现了最基本的功能——NX 保证互斥,EX 10 防止锁不释放造成死锁。但在生产环境中,至少有四个问题需要解决。第一是”误删锁”:线程 A 的锁过期后被线程 B 获取,A 恢复后执行 unlock 会误删 B 持有的锁。解决方案是 value 设唯一标识(UUID 或线程 ID),删除时用 Lua 脚本原子地判断 if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1])。第二是”锁过期但业务没执行完”:设置固定的 10 秒过期,但业务逻辑可能因为 GC 停顿或网络延迟超出 10 秒,锁被自动释放后其他线程获取了锁导致并发安全问题。第三是”可重入”:同一个线程多次获取同一把锁需要支持。第四是”获取锁失败时的等待策略”:自旋、阻塞带超时等。

追问1: 看门狗就是解决”锁过期但业务没执行完”问题的。Redisson 获取锁后,如果未指定锁的过期时间,看门狗会以锁过期时间的 1/3 为间隔(默认锁过期 30 秒,每 10 秒检查一次),检查当前线程是否还持有这把锁,如果还持有着就自动续期(将过期时间重新设为 30 秒)。续期操作通过 Netty 的定时任务 + Redis 的 Lua 脚本完成,只有锁的持有者才有权续期。如果业务执行完手动 unlock,看门狗自动停止续期。如果进程崩溃,定时任务停了,30 秒后锁自然过期,不会死锁。这本质上是一个”心跳续约”模式,保证了锁只有在持有者存活时才有效。

追问2: Redis Cluster 的异步复制带来了安全风险:主节点获取锁成功但这条数据还没来得及同步到从节点,主节点宕机,从节点提升为新主——锁在新主上丢失,另一个线程可能再次获取同一把锁。RedLock 算法是 Redis 创始人提出的方案:在 N 个独立的主节点(注意是独立主节点、不是主从)上依次获取锁,如果能在大多数节点(≥ N/2 + 1)上获取成功且总耗时不长于锁的有效期,则认为获取成功。实施时需要所有节点都用相同配置、物理独立(非同一集群),且获取锁的启动时间需要时钟协调。但 RedLock 在实际中争议很大——分布式系统领域普遍认为它依赖时钟同步,而时钟是不可靠的。Martin Kleppmann 曾发文质疑 RedLock 的安全性,这也是面试中的一个加分谈资。生产实践中,对一致性要求极高的场景建议使用 ZooKeeper 的临时顺序节点或 etcd 的 lease 机制实现分布式锁。

📌 易错点 / 加分项:

  • 误删锁用 UUID + Lua 脚本解决,这个回答几乎是面试标配了
  • 能说清楚 RedLock 的局限性和争议点说明有自己判断力,而不是人云亦云
  • Zookeeper 锁的临时节点方案利用了 ZK 的 session 管理——断开连接自动删锁