Redis 面试题精讲:过期机制与内存淘汰策略
Redis 面试题精讲:过期机制与内存淘汰策略
60. Redis 是怎么删除过期 key 的?
回答
Redis 采用的是“惰性删除 + 定期删除”的组合策略。在访问 key 时才会检查是否过期并删除,这是惰性删除;而定期删除则是 Redis 每隔一段时间主动扫描一部分 key,发现过期就清除,从而避免内存泄露。
分析
Redis 支持对 key 设置过期时间,但并不是在 key 到期的那一刻就立即删除,而是通过设计合理的“过期删除策略”来在性能和内存之间做平衡。定时删除理论上最及时,key 一到期就删除,但实现成本高,需要为每个 key 创建定时器,反而拖累性能。惰性删除则是当客户端访问一个 key 时,Redis 会先检查其是否过期,若过期就删除并返回空值。这种方式极大节省了 CPU 开销,但会出现“没人访问就不删除”的情况,最终可能积压大量垃圾数据。
为了解决这个问题,Redis 引入了定期删除机制,它会在后台周期性扫描部分已设置过期时间的 key,清理其中已过期的部分。这种机制同样是“采样式”的,每次随机抽取若干个 key 进行检测,并控制 CPU 使用率,比如 Redis 会抽取 20 个 key,如果超过 25% 已过期则继续扫描,直到比例低于阈值。
所以 Redis 并没有选用一种理想但不现实的方式,而是通过惰性 + 定期的组合策略,在 CPU 占用、内存释放、实时性之间找到一个工程上可接受的平衡点。
61. Redis 有几种内存回收策略?
回答
Redis 提供了多达 8 种内存淘汰策略,核心是围绕“是否淘汰数据”以及“优先淘汰什么类型的数据”展开,可按需配置。默认策略是 noeviction,即写入超出内存时直接返回错误。
分析
Redis 的内存控制机制是通过配置 maxmemory 来启用的,一旦内存使用超过上限,就需要执行内存回收策略。不同策略应对的场景和风险也不同。
首先,有一种极端情况是不进行数据淘汰,即 noeviction,这在 Redis 3.0 后成为默认策略,表现是:一旦内存不够,新写入操作将被直接拒绝。这种策略适合“数据不可丢”的业务,但在高写入量场景下风险很高。
其余策略可以分为两大类。一类是针对设置了过期时间的 key,如 volatile-lru(只淘汰设置了过期时间中最近最少使用的)、volatile-ttl(优先淘汰快过期的 key)等,适合缓存类数据;另一类策略作用于所有 key,比如 allkeys-lru、allkeys-random,甚至 allkeys-ttl,更适合数据全量都可丢弃的场景。
Redis 的回收策略本质是提供一种可配置的“压力应对机制”,在写入压力大、内存告急时,通过合理选择淘汰逻辑,让系统能继续运行而不崩溃。企业中最常用的是 allkeys-lru,在 Redis 5.0 后又提供了基于采样优化的 LFU 算法,可用于对抗热点数据频繁被淘汰的问题。
最终选择何种策略,取决于业务能否接受数据丢失、哪些 key 更重要,以及缓存是否设有过期时间,是一种资源与需求之间的博弈结果。
62. 内存回收是什么时候发起?
回答
Redis 的内存回收不是定时任务,而是在每次执行读写命令时触发检查。如果内存不足且配置了淘汰策略,就会在命令处理流程中尝试回收一部分内存。
分析
Redis 的内存淘汰逻辑是内嵌在命令执行路径里的。每次客户端发送读写命令,Redis 会进入核心函数 processCommand,在执行真正的读写之前,它会调用内存检查逻辑,判断当前内存使用是否超过了配置的 maxmemory。
如果检测到超限,Redis 就会调用内存释放逻辑 freeMemoryIfNeeded,这个函数会根据当前配置的淘汰策略,尝试回收一部分 key 来腾出空间。由于这一过程是嵌套在命令执行中的,因此并不是一个后台定时任务,而是随操作触发的“即时检查”机制。
这种设计的好处是避免了不必要的资源开销,只在真正可能产生写入、导致内存超限时才尝试释放资源,同时也能快速响应内存压力,而不会等定时任务延迟处理。
63. 介绍下 Redis 的 LRU 回收算法
回答
Redis 中的 LRU(Least Recently Used)算法用于判断哪些 key 应该被优先淘汰,但 Redis 并不实现严格的 LRU,而是使用了一种近似算法,以降低内存消耗和性能开销。
分析
传统 LRU 算法需要为每个 key 维护精确的访问时间顺序,一般是通过链表或红黑树实现,但这在 Redis 中是不可接受的,因为 Redis 是内存数据库,额外维护链表结构会显著增加内存占用。
为了解决这个问题,Redis 设计了一种近似的 LRU 算法,核心思想是采样 + 淘汰池优化。每当需要回收内存时,Redis 会随机从 key 空间中抽取一组样本(默认是 5 个),然后找出这组中最久未被访问的 key 进行淘汰。如果淘汰后内存仍不足,就继续采样下一轮。
Redis 在 3.0 之后又引入了淘汰候选池机制:每轮采样得到的 key 会放入一个小池子中,后续新的采样如果优于池中最差者,才会替换入池,最终从池中淘汰最久未使用的 key。这有效提升了 LRU 的淘汰质量,同时保持了较低的内存开销。
64. Redis 的 LRU 算法是标准的吗?为什么不用标准的?
回答
Redis 使用的是近似 LRU,而非严格标准的 LRU。原因在于标准 LRU 实现复杂且成本高,不适合高性能内存数据库。Redis 采用采样 + 淘汰池优化的方式,既保留了 LRU 思路,又降低了性能代价。
分析
标准的 LRU 需要用双向链表或红黑树来精确维护 key 的访问顺序,每次读写都要更新结构,代价高昂。在 Redis 的极端高并发场景下,这种方案会拖累整体性能,甚至引发锁竞争和 CPU 上升。
因此,Redis 选择了更轻量的采样淘汰方案:每轮随机选出一批 key,记录它们的访问时间,然后淘汰其中最久未访问者。采样轮次不固定,直到内存足够或淘汰失败。为了增强淘汰效果,还引入了“淘汰池机制”,维持一个候选集合,动态替换,优中择劣。
这种设计虽然不是百分百准确的 LRU,但在实际使用中,已经能非常有效地识别“冷数据”,实现了淘汰效率与系统性能的平衡。
65. 什么是 LFU 算法,为什么 Redis 要引入 LFU?
回答
LFU(Least Frequently Used)是一种淘汰最少访问次数的数据的算法。Redis 引入 LFU,是为了弥补 LRU 无法识别热点数据频率的短板,防止高频数据因最近未访问而被淘汰。
分析
相比 LRU 关注“最后一次访问时间”,LFU 更关注的是“访问频率”。这在某些场景下至关重要:比如一些业务的热点 key 在一分钟内被访问几千次,但一段时间没用就被 LRU 淘汰了。这种淘汰是不合理的,因为该 key 的价值远超其他数据。
Redis 在 4.0 中引入了 LFU 算法,并通过精简的计数方式记录 key 的访问频率。每个 key 都带有一个 8 位的计数器,这个计数在访问时按一定概率增加,并定期衰减,以避免计数膨胀。这样可以在较低成本下判断哪些 key 真的是“常用”而非“偶尔突发”。
在 maxmemory-policy 中可以选择 allkeys-lfu 或 volatile-lfu,用于控制是否对所有 key 或仅有过期时间的 key 应用 LFU 策略。