Redis近似LRU是工程最优解:用24位秒级时间戳替代链表,实现零指针开销、无锁访问与常数淘汰;严格LRU因内存膨胀、缓存失效和非原子操作不可行。

Redis 的近似 LRU 不是“妥协”,而是针对真实缓存负载做的工程最优解:它用 lru 字段 24 位存储秒级时间戳(而非维护链表节点),换来零链表指针开销、无锁访问、常数级淘汰判定 —— 这正是高并发缓存场景真正需要的。
为什么严格 LRU 在 Redis 里不可行
严格 LRU 要求每次 GET 或 SET 都移动节点位置,这在 Redis 单线程模型下看似可行,但实际会引发三个硬伤:
- 每个 key 都得额外携带两个指针(
prev/next),在亿级 key 场景下,内存膨胀直接多出 16–24 字节/键,远超lru字段本身 - 链表操作虽为 O(1),但涉及 cache line 失效和内存跳转,实测在 10 万 QPS 下,链表更新耗时占比超 12%
- 哪怕单线程,也要在哈希表查找后做链表重排 —— 这两步无法原子合并,导致
OBJECT IDLETIME等命令读到的“最近访问时间”可能滞后于真实顺序
maxmemory-samples 怎么调才不瞎抽
近似 LRU 的精度全靠随机采样,但默认 maxmemory-samples 5 在热点倾斜明显时容易误判。关键不是盲目加数字,而是看业务特征:
- 如果缓存中存在少量高频 key(如用户 session key 前缀集中),建议设为
10~15:样本量翻倍,误淘汰率下降约 40%,且对 CPU 影响微乎其微 - 若数据访问高度均匀(如 CDN 缓存静态资源),保持
5即可 —— 样本再多也难分伯仲,纯属浪费周期 - 绝对不要设成
1:此时等效于随机淘汰;也别设 >20:采样开销开始线性上升,evict.c中的循环判断反而成为瓶颈
LFU 模式下 lru 字段被复用,但 LRU 逻辑仍存在
Redis 4.0+ 的 LFU 并非抛弃时间维度,而是在同一 lru 字段里拆分为两段:
- 高 16 位存“上次衰减时间”(单位:分钟),用于控制计数器衰减节奏
- 低 8 位存“访问频次对数”,不是原始计数,避免溢出且天然抑制刷量
这意味着:即使你配了 allkeys-lfu,Redis 内部仍依赖时间信息做衰减和冷热校准。所以 OBJECT IDLETIME 在 LFU 模式下依然有效,只是语义从“距上次访问秒数”变成“距上次衰减分钟数 + 频次权重”。这点常被监控脚本忽略,导致误判 key “已冷”。
真正容易被忽略的点:TTL 键与 LRU/LFU 的混合行为
当同时存在带 TTL 和不带 TTL 的 key,且你用了 volatile-lru 这类策略时,Redis 淘汰逻辑会先过滤出所有 ttl > 0 的 key,再在其中跑近似 LRU —— 但注意:
- 一个 key 若刚被
EXPIRE设置了 3600 秒,它的lru字段不会重置,仍沿用上次访问时间戳 - 如果该 key 已闲置 2 小时,此时它在 volatile 集合里就是“最久未用”,哪怕 TTL 还剩 3500 秒,也会优先被淘汰
- 换句话说:
volatile-lru不保证“快过期的先淘汰”,只保证“在所有将过期的 key 里,挑最久没碰过的” —— 这和直觉相反,却恰恰是设计本意











