LRU在扫描式查询下必然误淘汰热点数据,因其仅依据最后访问时间判断冷热,导致批量读取的冷key集体“变热”挤占热key空间;LFU通过counter+ldt机制按访问频次衰减,可天然过滤单次访问,但需配合key生命周期分层与合理配置才能有效防污染。

LRU在扫描式查询下必然误淘汰热点数据
当业务执行全表扫描、导出任务或批量同步时,大量冷 key 被一次性读取——它们的 lru 字段被统一刷新为最新时间戳。这些 key 在 Redis 近似 LRU 淘汰中会集体“变热”,挤占真正高频访问的热 key 空间。这不是配置错误,而是算法本质缺陷:LRU 只看“最后访问时间”,不区分“访问是一次性还是重复性”。
常见错误现象包括:
- 缓存命中率骤降,
INFO stats中keyspace_hits下滑、evicted_keys持续上涨 - 慢日志里频繁出现大 key 读取(如
HGETALL user:profile:*),但后续无任何访问 - 用
redis-cli --bigkeys扫出大量刚写入就再未触达的 key
LFU通过访问频次衰减机制天然过滤单次访问
LFU 的核心判断依据是 counter(8bit 访问计数)+ ldt(16bit 最后衰减时间戳)。它不会因为一次访问就把冷数据“洗白”,而是依赖两个关键配置实现抗污染:
-
lfu_log_factor:控制计数器增长斜率。默认 10,值越小,低频访问越难提升计数(比如访问 10 次才到 counter=2) -
lfu_decay_time:单位分钟,决定计数衰减节奏。设为 1,表示每过 1 分钟,counter 就按规则衰减;设为 0 则完全不衰减(慎用)
举例:一个临时订单 key order:tmp:789 被读取一次后,counter = 1;若 5 分钟内无二次访问,在 lfu_decay_time=1 下,它会被衰减掉全部计数,回归为 0。而真实热 key(如 product:1001)因持续访问,counter 始终维持高位,自然获得保留优先级。
allkeys-lfu 不等于防污染,混用长期/临时 key 反而加剧问题
很多人开了 allkeys-lfu 就以为万事大吉,结果发现冷数据还是赖着不走——根本原因是没做 key 生命周期分层。LFU 对所有 key 一视同仁,但业务上:
- 长期有效 key(如用户基础信息)适合进
volatile-lfu,配合setex显式设 TTL - 临时 key(如
tmp:export:20260525、batch:sync:123)必须带明确过期时间,否则它们的 counter 永远卡在 1,却和高频 key 同池竞争,稀释 LFU 效果 - 命名无规律的临时 key 会让
SCAN清理失效,必须约定前缀并配合EXPIRE或SETEX
性能影响方面:allkeys-lfu 比 volatile-lfu 内存开销略高(因统计全量 key),且每次访问都要更新 counter 和 ldt,CPU 占用稍升;但相比误淘汰导致的 DB 回源,这点开销通常可接受。
验证 LFU 是否真正在起作用,别只看 config
仅执行 CONFIG GET maxmemory-policy 看到 allkeys-lfu 并不能说明问题。必须结合运行时行为观测:
- 查
INFO memory确认maxmemory已启用,且used_memory接近上限 - 查
INFO stats关注evicted_keys增速,同时比对expired_keys—— 若后者占比长期低于 5%,说明过期机制没兜底,淘汰压力全压在 LFU 上 - 用
redis-cli --scan --pattern "tmp:*" | head -n 100 | xargs -I{} redis-cli ttl {}快速抽检临时 key 是否真有过期时间 - 监听
__keyevent@0__:expired事件,确认 key 到期后是否被真正回收(而非堆积在内存里等 LFU 淘汰)
真正容易被忽略的是:LFU 的 counter 衰减不是实时触发的,它只在 key 被访问或淘汰采样时顺带计算。如果某批冷 key 自始至终无人访问,它们的 counter 就永远停在 1,也不会自动衰减——这时只能靠业务侧强制加 EXPIRE 或定时脚本清理。


















