如果你在面试中被问到 Redis 的缓存淘汰策略,大概率会听到这样的回答:“Redis 支持 LRU 和 LFU,可以通过 maxmemory-policy 配置。”但真正拉开差距的追问往往是:为什么 Redis 不直接用标准的 LRU 或 LFU,而是搞了一套“近似 LRU”和“近似 LFU”,甚至把两者的思想揉在一起?
这个问题看似在问淘汰算法,实际上考的是你对 Redis 内存模型、性能取舍和工程实现的理解。本文从标准 LRU/LFU 的局限讲起,逐步拆解 Redis 的设计动机。
一、先厘清:Redis 到底提供了哪些淘汰策略
在 maxmemory 被设置后,Redis 提供了 8 种淘汰策略,其中和 LRU/LFU 直接相关的有:
allkeys-lru:在所有 key 中淘汰最近最少使用的volatile-lru:在设置了过期时间的 key 中淘汰最近最少使用的allkeys-lfu:在所有 key 中淘汰最不经常使用的volatile-lfu:在设置了过期时间的 key 中淘汰最不经常使用的
注意措辞:Redis 官方文档里用的是 approximated LRU 和 approximated LFU,也就是“近似”。这不是谦虚,而是事实——Redis 根本没有维护一个完整的 LRU 链表或 LFU 计数器表。
二、标准 LRU 的问题:内存与指针开销
标准 LRU 的经典实现是“哈希表 + 双向链表”:哈希表保证 O(1) 查找,双向链表保证 O(1) 移动和删除。每次访问一个 key,就把它移到链表头部;淘汰时从尾部删除。
听起来很完美,但放到 Redis 里就有两个致命问题:
- 每个 key 需要额外的两个指针。Redis 的每个对象本身已经带有
redisObject结构,如果再挂一个双向链表指针,在几千万甚至上亿 key 的场景下,内存开销非常可观。Redis 是内存数据库,内存就是成本。 - 每次访问都要修改链表。这看似是 O(1),但在高并发下,对共享链表的修改需要加锁或使用无锁结构,会带来竞争和 CPU 缓存不友好的问题。Redis 单线程模型下,频繁的指针操作也会挤占本已紧张的执行时间。
所以 Redis 选择了“采样近似”:不维护全局链表,而是给每个对象记录一个最后一次访问的时间戳(lru 字段,24 bit),淘汰时随机采样若干 key,从中挑出最久未使用的那个。
这就是“近似 LRU”的本质——用随机采样 + 时间戳比较替代精确链表。
三、近似 LRU 的缺陷:只看“最近”,不看“频率”
近似 LRU 解决了内存和性能问题,但它继承甚至放大了 LRU 本身的缺陷:LRU 对“偶发批量访问”非常脆弱。
举个例子:一个 key 平时被访问得很频繁,是典型的热点数据。某天来了一次全量扫描或批量任务,大量冷 key 被顺序访问了一遍。在 LRU 视角下,这些冷 key 的“最近访问时间”变得很新,而真正的热点 key 反而显得“很久没被访问”。如果此时触发淘汰,热点 key 可能被误杀。
这就是经典的 LRU 缓存污染问题。而 LFU(Least Frequently Used)按访问频率淘汰,天然对这种场景更鲁棒——偶尔被访问一次的冷 key,频率计数很低,不会因为“刚被访问过”就获得高优先级。
但标准 LFU 也有自己的问题:
- 需要为每个 key 维护一个计数器,内存开销更大;
- 频率是只增不减的,一个曾经的热点 key 即使后来不再被访问,也会因为历史高频率而长期占坑,这就是“频率僵化”;
- 新 key 天然处于劣势,容易饿死。
四、Redis 的解法:LFU 是对 LRU 字段的“复用与改造”
Redis 4.0 引入 LFU 时,做了一个非常巧妙的设计:它没有新增字段,而是复用了原来 LRU 的 24 bit lru 字段。
这 24 bit 被拆成两部分:
- 高 16 bit:访问时间(ldt,last decrement time),记录上一次频率衰减的时间;
- 低 8 bit:访问频率计数器(logc,logistic counter)。
频率计数器不是简单加一,而是采用概率递增:计数器越大,继续增加的概率越低,上限为 255。这样既能区分冷热,又不会让热点 key 的计数无限膨胀。
更关键的是衰减机制:LFU 会按时间对计数器做衰减。如果一个 key 长时间不被访问,它的频率计数会逐渐降低。这直接解决了标准 LFU 的“频率僵化”问题——曾经的热点如果不再被访问,会慢慢让出位置。
淘汰时,Redis 同样采用随机采样:从待淘汰集合中采样若干 key,比较它们的频率计数(必要时再比较访问时间),淘汰频率最低的。
五、为什么说“结合在一起”:LRU 与 LFU 在 Redis 中的统一
到这里,答案已经比较清晰了。Redis 并不是简单地在 LRU 和 LFU 之间二选一,而是:
- 共用同一套近似淘汰框架:随机采样 + 24 bit 元数据字段 + 候选池比较。
- LRU 模式:24 bit 全部用作时间戳,比较谁最久未访问。
- LFU 模式:24 bit 拆成时间 + 频率,比较谁频率最低,频率还会随时间衰减。
- LFU 内部仍然保留时间维度:当频率相同时,用时间戳做次级比较,避免平局时无法决策。
换句话说,Redis 的 LFU 并不是纯粹的“频率优先”,而是频率为主、时间为辅,并且在频率计数中引入了时间衰减。这本质上是一种 LRU 与 LFU 思想的融合:用 LFU 抵抗缓存污染,用时间衰减和次级时间比较避免 LFU 的僵化,用近似采样控制内存和 CPU 开销。
六、面试怎么答:三层递进
如果面试官问“Redis 为什么把 LRU 和 LFU 结合”,可以按三层来回答:
第一层,标准算法不可用:标准 LRU 需要双向链表,每个 key 多两个指针,内存开销大;每次访问改链表,并发和性能成本高。所以 Redis 用随机采样 + 时间戳做近似 LRU。
第二层,近似 LRU 不够用:LRU 只看最近访问时间,批量扫描会污染缓存,误杀热点 key。LFU 按频率淘汰更抗污染,但标准 LFU 有计数器内存开销和频率僵化问题。
第三层,Redis 的融合设计:复用 24 bit 字段,LFU 模式下拆成 16 bit 时间 + 8 bit 对数频率;频率概率递增、随时间衰减;淘汰时随机采样,先比频率、再比时间。既保留了近似框架的低开销,又结合了 LRU 的时间敏感和 LFU 的频率敏感。
七、小结
Redis 选择“近似 + 融合”的淘汰策略,根本原因在于它是一个内存敏感、单线程、高吞吐的系统。精确 LRU 的内存和指针开销不可接受,纯 LRU 又抵抗不了缓存污染,纯 LFU 则有僵化和饿死问题。把两者结合,用同一套 24 bit 元数据和采样框架承载两种语义,是在工程约束下做出的最优折中。
理解这一点,比背下 8 种淘汰策略的名字更有价值——它体现的是 Redis 一贯的设计哲学:在理论最优和工程可行之间,永远选择后者,但把后者的效果做到足够接近前者。
未经允许不得转载:任鹏个人博客 » Redis 为什么把 LRU 和 LFU 结合在一起做近似淘汰

