结论:guava cache 与 redis allkeys-lfu 不兼容,会导致缓存命中率暴跌、数据不一致和雪崩;caffeine 的 window-tinylfu 才能与 redis lfu 协同实现可靠多级降压。

直接说结论:用 Guava Cache 承接热点 + Redis 启用 allkeys-lfu 策略,无法实现真正可靠的多级降压,反而容易因淘汰逻辑错位导致缓存命中率暴跌、数据不一致甚至雪崩。Caffeine 才是当前 Java 生态中唯一能与 Redis LFU 协同演进的本地缓存选择。
Guava Cache 的淘汰机制和 Redis LFU 根本不兼容
Guava Cache 默认使用 LRU(最近最少使用),即使你手动配置 maximumSize + expireAfterWrite,它也没有频率感知能力。而 Redis 的 allkeys-lfu 是靠访问频次计数器 + 衰减机制动态调整淘汰优先级的。两者对“热点”的定义完全不同:
- Guava 认为“刚写入且没被访问过”的 key 依然在内存里,但 Redis LFU 可能已把它标记为低频并踢出
- Redis 中高频访问的 key 在 Guava 里可能因写入早、未触发重载而被 LRU 淘汰掉
- 当 Guava 和 Redis 同时淘汰,Guava 踢掉的是“旧但冷”的 key,Redis 踢掉的是“旧且低频”的 key——它们淘汰的不是同一类数据
Guava + Redis LFU 在高并发下会放大缓存穿透风险
典型场景:一个商品详情页 QPS 突增到 2 万,Guava 缓存容量设为 5000,Redis 使用 allkeys-lfu。此时会出现:
- Guava 因 LRU 淘汰策略,把一批“写入早但访问少”的配置类 key 挤出,却把刚批量导入的冷门商品塞进来
- Redis 因 LFU 频率衰减,把一批历史高频但近期沉默的商品 key 清理掉
- 结果是:本地缓存命中率从 95% 直降到 40%,大量请求穿透到 Redis;Redis 又因 LFU 清理了部分 key,进一步穿透到 DB
- 最终形成“Guava 淘汰冷数据 → 请求打 Redis → Redis 淘汰旧热点 → 请求打 DB”的负向循环
Caffeine 的 Window-TinyLFU 才是 Redis LFU 的合理搭档
Caffeine 的淘汰算法本质是 LFU 的增强版,它通过频率素描(Sketch)+ 滑动窗口分区,天然适配 Redis 的 LFU 思路。关键差异点:
-
expireAfterWrite和maximumSize可共存,不会互相干扰淘汰决策 - 窗口区域(Window Region)接纳新 key,主区域(Main Region)保护高频 key,避免突发流量污染缓存
- 频率素描只用极小内存记录访问频次,和 Redis 的 LFU 计数器语义一致,可协同做热点识别
- 配合 Redis 的
OBJECT FREQ命令,能反向校验本地缓存中 key 的真实热度,实现分级保热
示例配置:
Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats() // 开启统计,用于监控淘汰是否合理
.build(key -> loadFromRedisOrDB(key));
如果非要用 Guava,必须放弃 Redis LFU,改用 TTL 或 volatile-lru
硬要保留 Guava,就别碰 Redis 的 LFU 类策略。可行组合只有两种:
- Redis 改用
volatile-lru+ 所有 key 显式设置 TTL,Guava 仅作为纯时效性缓存(比如 session token),靠时间驱动淘汰,双方节奏可对齐 - Redis 改用
noeviction(禁用淘汰),所有 key 过期全靠 TTL 控制,Guava 仅承担“减少网络往返”职责,不做热点判断
注意:allkeys-lru 也不推荐——Guava 的 LRU 是精确链表,Redis 的 LRU 是采样近似,依然存在淘汰错位。
真正难的不是选哪个淘汰策略,而是让两级缓存的“热度判定标准”统一。Guava 没这个能力,Caffeine 有,Redis 也有。三者只有在 LFU 语义上对齐,才谈得上多级降压。否则就是两套平行宇宙的淘汰逻辑,在高并发下必然互撕。











