拼多多「面试官问我:LRU 和 LFU 你选谁?」我:看场景啊哥!😂

🧠后端面试题:LRU vs LFU,到底谁更聪明?项目里该怎么选?

"面试官问:你知道 LRU 和 LFU 的区别吗?"

我当时心里一紧:这不就是"缓存界的双雄"嘛!结果越答越乱...

------后来我悟了,这俩其实是两种"记仇方式"😂


一、先聊点真实的场景

有没有遇到过这种情况👇

你的接口明明加了缓存,结果访问量一上来,缓存命中率低得可怜;

或者 Redis 内存爆掉后,刚被刷新的热数据居然被踢出缓存了?!

这时候,问题往往不在 Redis 上,而在 「"缓存淘汰策略"」

于是,经典二选一:「LRU or LFU?」


二、他们俩到底是谁?

策略 全称 思想 举个例子
「LRU」 Least Recently Used 最近最少使用 像你微信里最久没联系的人,被清理掉 💔
「LFU」 Least Frequently Used 最少访问次数 就像朋友圈互动少的人,会被算法"雪藏"了 😅

🕵‍♂️ 用一句话总结:

「LRU 看"时间",LFU 看"次数"。」


三、他们的底层机制(图解理解更轻松 👇)

🌀 LRU(时间维度)

css 复制代码
[最近访问] A → B → C → D [最久未访问]
  • 每次访问元素,就把它移到"头部"
  • 缓存满了,就把"尾部"的淘汰

✅ 优点:实现简单,能应对大多数缓存场景 ❌ 缺点:短时间内被访问多次的"热点"可能被误删


🔢 LFU(次数维度)

scss 复制代码
访问次数:
A(5次), B(3次), C(1次), D(2次)
  • 每次访问次数 +1
  • 淘汰时删除访问次数最少的那个

✅ 优点:更能保留真正的"常用数据" ❌ 缺点:计数维护复杂、容易"固化热点"(老数据长期霸占内存)


四、实战代码:手写一个 LRU & LFU 🔧

🧩 Java 版 LRU

其实 Java 自带的 LinkedHashMap 已经帮我们搞定了👇

scala 复制代码
import java.util.*;

class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true); // true 表示访问顺序
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

测试一下👇

typescript 复制代码
public static void main(String[] args) {
    LRUCache<Integer, String> cache = new LRUCache<>(3);
    cache.put(1, "A"); cache.put(2, "B"); cache.put(3, "C");
    cache.get(1); // 访问1,让它变"新"
    cache.put(4, "D"); // 淘汰最久未访问的2
    System.out.println(cache.keySet()); // 输出: [3, 1, 4]
}

🧩 Go 版 LFU

用两个 map + 一个最小堆思想实现👇

go 复制代码
package main

import "fmt"

type entry struct {
 value interface{}
 count int
}

type LFUCache struct {
 data     map[string]*entry
 capacity int
}

func NewLFU(cap int) *LFUCache {
 return &LFUCache{
  data:     make(map[string]*entry),
  capacity: cap,
 }
}

func (c *LFUCache) Get(key string) (interface{}, bool) {
 if e, ok := c.data[key]; ok {
  e.count++
  return e.value, true
 }
 return nil, false
}

func (c *LFUCache) Put(key string, val interface{}) {
 if len(c.data) >= c.capacity {
  // 淘汰最少访问的
  var minKey string
  minCount := int(^uint(0) >> 1)
  for k, e := range c.data {
   if e.count < minCount {
    minCount = e.count
    minKey = k
   }
  }
  delete(c.data, minKey)
 }
 c.data[key] = &entry{val, 1}
}

func main() {
 cache := NewLFU(3)
 cache.Put("A", 1)
 cache.Put("B", 2)
 cache.Put("C", 3)
 cache.Get("A")
 cache.Get("A")
 cache.Put("D", 4) // 淘汰最少访问的 B 或 C
 fmt.Println(cache.data)
}

🧠 小技巧:

LFU 在高并发系统中要注意计数器的线程安全和性能开销。 实际中 Redis 的 volatile-lfu 就做了「衰减 + 近似计数」优化。


五、那项目中我怎么选?

这个问题就像------ 「"要吃炸鸡还是沙拉?"🍗🥗」 得看场景!

场景 推荐策略 原因
Web 缓存、CDN、接口响应缓存 ✅ LRU 热点变化快,最近访问更重要
计算结果、配置缓存、AI 模型缓存 ✅ LFU 稳定的高频访问数据更关键
Redis 缓存 ✅ volatile-lfu(默认推荐) 兼顾"时间 + 次数"两种维度

六、发散一下:Redis 是怎么做的?

Redis 其实在玩"混合流派"👇

  • 「LRU 模式」:按时间踢掉冷数据
  • 「LFU 模式」:用 8 bit 计数器 + 衰减机制(count 越大衰减越慢)
  • 「近似算法」:随机采样几个 key,对比后淘汰,不会全局扫描 🔥

✅ 性能稳定 ✅ 不容易被"老热点"绑架 ✅ 不需要太多内存统计信息

这就是 Redis 为啥能撑得住高并发读写 💪


七、最后总结 🧩

对比点 LRU LFU
核心 最近最少使用 最少访问次数
优点 实现简单、性能高 更聪明地保留热点数据
缺点 可能误删热点 计数维护复杂
应用 Web 缓存、接口缓存 模型、配置、冷数据场景
Redis 中 volatile-lru volatile-lfu

🏁结语:写在最后

其实我也走过弯路------ 以前我啥都上 LRU,后来缓存命中率一塌糊涂, 直到被 Redis 的 LFU 教做人 😂

有时候我们以为的"最近",不一定代表"最常用"; 而系统真正的聪明,不在于算法多复杂,而在于------ 「能不能理解数据的"使用习惯"。」


👋 那你呢? 项目里用的是哪种策略? 评论区见,我们一起聊聊那些"被误删"的缓存吧 💬

复制代码
相关推荐
用户68545375977695 小时前
🔥 服务熔断降级:微服务的"保险丝"大作战!
后端
用户68545375977695 小时前
🎬 开场:RPC框架的前世今生
后端
王中阳Go背后的男人6 小时前
Docker磁盘满了?这样清理高效又安全
后端·docker
用户68545375977696 小时前
🎛️ 分布式配置中心:让配置管理不再是噩梦!
后端
CodeFans6 小时前
Spring 浅析
后端
李广坤6 小时前
Filter(过滤器)、Interceptor(拦截器) 和 AOP(面向切面编程)
后端
oak隔壁找我6 小时前
反向代理详解
后端·架构
YUELEI1186 小时前
Springboot WebSocket
spring boot·后端·websocket
小蒜学长6 小时前
springboot基于JAVA的二手书籍交易系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端