第 7 篇
讲的是已过期 key
如何被清扫;本篇讲仍有效 key
在内存触顶时如何被踢出。maxmemory 与
maxmemory-policy 是缓存角色 Redis
的核心旋钮,但源码实现的不是操作系统课本里的真
LRU,而是用固定内存的采样 +
淘汰池近似——理解这一层,才能解释「明明开了 LRU
却淘汰了热 key」类故障。
本文锚定 Redis OSS 7.4.x
evict.c、object.c(MEMORY DOCTOR),并对照站内
页面置换算法
与 Buffer
Pool 中的 LRU/CLOCK 讨论。
本文是「Redis / 缓存内核」系列第 8 篇(共 16 篇)。→ 系列目录
版本锚定:Redis OSS 7.4 / 8.x;默认
maxmemory-samples为 5,LFU_INIT_VAL为 5。
一、问题:内存上限下的在线驱逐
当 zmalloc_used_memory() 超过
maxmemory(且非
noeviction),写路径在
processCommand 里调用
performEvictions() 尝试腾出
mem_tofree。这是典型的 demand
paging 式在线决策:只见过去访问,不见未来——与
Bélády OPT 不可实现性同一抽象(详见 页面置换
第一节)。
Redis 与 OS 页置换的关键差异:
| 维度 | OS 页置换 | Redis maxmemory |
|---|---|---|
| 元数据 | 页表 + 硬件引用位 | redisObject.lru 字段复用 |
| 精确 LRU 成本 | 链表 + 锁竞争 | 单线程仍不愿 O(N) |
| 策略族 | LRU/CLOCK/ARC… | 8 种固定 policy + 采样 |
| 驱逐对象 | 有 TTL 与无 TTL 混合 | volatile-* vs allkeys-*
显式分支 |
二、策略矩阵:volatile × allkeys
config.c 注册 8 种
policy(MAXMEMORY_*):
| 策略 | 候选集合 | 选择依据 |
|---|---|---|
volatile-lru |
仅 expires 表中的 key |
近似 LRU(idle 最大) |
volatile-lfu |
同上 | 近似 LFU(频率最低) |
volatile-ttl |
同上 | expire_at 最早 |
volatile-random |
同上 | 随机 |
allkeys-lru |
全 keys |
近似 LRU |
allkeys-lfu |
同上 | 近似 LFU |
allkeys-random |
同上 | 随机 |
noeviction |
— | 不驱逐;写命令可能拒绝 |
volatile-* 只从 db->expires
采样——无 TTL 的 key
永不作为淘汰候选,内存触顶时可能直接 OOM
拒绝写入。纯缓存场景若忘记给 key 设
TTL,volatile-lru 等于「只删有过期时间的
subset」,常踩坑。
allkeys-* 从 db->keys
采样,把 Redis 当整个数据集的上限容器;与
Buffer
Pool 驱逐脏页不同,Redis 驱逐的是应用可见的 key,且会发
evicted 通知并写 AOF/复制。
三、近似 LRU:24 位时钟与淘汰池
3.1 LRU 时钟
真 LRU 需维护全序访问链表。Redis 在
redisObject 里用 24 位 lru
字段存上次访问的压缩时钟:
#define LRU_CLOCK_RESOLUTION 1000 /* 毫秒;1000 表示秒级粒度 */
unsigned int getLRUClock(void) {
return (mstime() / LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
}读/写命中时 LRU_CLOCK() 写入
o->lru。estimateObjectIdleTime()
用当前时钟减 o->lru 得 idle 毫秒——这是
CLOCK 族思路:不移动链表,只打时间戳(对照
页面置换
CLOCK 节)。
当 1000/hz <= LRU_CLOCK_RESOLUTION
时,主循环在 serverCron 预计算
server.lruclock,避免每次访问调
mstime()——精度与 hz 绑定,高
hz 提高时钟分辨率。
3.2 采样与 eviction pool
performEvictions() 在 LRU/LFU/TTL
类策略下调用 evictionPoolPopulate():每次从候选
dict 随机抽 maxmemory-samples 个
key(默认 5,可配 1–64),把 idle
最大的若干名填入固定大小 淘汰池
EvictionPoolLRU[EVPOOL_SIZE](16 槽)。
/* evict.c 文件头注释(节选) */
/* Every time there is a key to expire, we sample N keys (usually ~5)
* to populate a pool of M keys (EVPOOL_SIZE = 16).
* After the pool is populated, the best key in the pool is expired. */然后从池尾(idle 最大)取候选,若 key 仍存在则
dbGenericDelete()。池内可能残留已删 key
的「幽灵」条目,下次再填充。
与真 LRU 的差距:一次驱逐决策只看 \(N\) 个随机样本中的最优,全局热 key 若不在样本中就可能被误踢——扫描型 workload(全表遍历)下近似 LRU 表现差,与 Megiddo & Modha 在 ARC 论文(IBM, 2003)中指出的「纯 LRU 对频率与最近性失衡」是不同维度的问题,但现象类似:单次顺序扫描可冲掉整个淘汰池视角下的「冷」数据。
3.3 LFU 编码
volatile-lfu / allkeys-lfu 复用
lru 字段,高 16 位为衰减时间,低 8
位为对数计数器(LFU_INIT_VAL = 5
起步)。LFUDecrAndReturn()
做时间衰减,LFULogIncr() 在访问时亚线性增频——用
8 位近似 Morris 计数思想,避免 per-key 高频计数器内存。
运维可调
lfu-log-factor、lfu-decay-time(OBJECT FREQ
读当前计数)。LFU
对周期性热点(如每分钟刷新的报表
key)通常优于秒级 LRU 时钟,但参数敏感,无 universal
最优(与 ARC 自适应论点形成对照,Redis 未实现 ARC)。
3.4 volatile-ttl 与 random
volatile-ttl 从 expires
采样后选 剩余 TTL 最短 的
key(evictionTimeProc),适合「优先保留长 TTL
会话」的业务,但不保证保留访问频率高的 key。
random 策略一次随机选一个候选 dict 槽中的
key,无池;实现最便宜,行为最难预测,仅适合均匀访问或调试。
flowchart TD
ov["memory > maxmemory"]
pe["performEvictions()"]
sample["evictionPoolPopulate<br/>N = maxmemory-samples"]
pool["EvictionPool[16]<br/>max idle / min freq"]
del["dbGenericDelete + lazyfree?"]
ov --> pe --> sample --> pool --> del
四、performEvictions 与延迟
performEvictions() 循环驱逐直到
mem_freed >= mem_tofree
或无法继续。每次删除走
dbGenericDelete(..., server.lazyfree_lazy_eviction, DB_FLAG_KEY_EVICTED),大对象可异步释放(第 7
篇)。
注意:
- 驱逐在写命令路径上同步发生,连续驱逐多个
key 会拉长延迟;
latency监控里有eviction-del事件。 - 有从节点时,每 16 个 key 可能
flushSlavesOutputBuffers(),避免复制缓冲堆积。 maxmemory-eviction-tenacity(7.x)控制单次驱逐循环的时间上限,防止极端情况下饥饿。
noeviction 下 performEvictions
返回 EVICT_FAIL,写命令得到 OOM
类错误,读命令仍可服务——这是「缓存」与「存储」角色分界:宁可拒写也不默默丢有效数据(除非业务接受)。
五、MEMORY DOCTOR:概念化信号
MEMORY DOCTOR 调用
getMemoryDoctorReport()(object.c),基于
getMemoryOverheadData()
做规则化启发诊断,不是 AI
分析。核心判定阈值(概念摘要):
| 信号 | 条件(概念) | 含义 |
|---|---|---|
empty |
分配总量 < 5MB | 实例几乎为空 |
big_peak |
peak / used > 1.5 | 峰值远高于当前,可能有大批 key 已驱逐或重启 |
high_frag |
碎片率 > 1.4 且 > 10MB | 分配器碎片,考虑 activedefrag |
high_alloc_frag |
allocator 层碎片 | jemalloc 类问题 |
high_proc_rss |
进程 RSS 相对分配偏高 | 可能是 COW、复制缓冲、lazyfree 积压 |
big_client_buf |
平均每客户端 > 200KB 输出缓冲 | 慢客户端或大响应堆积 |
big_slave_buf |
从节点总缓冲 > 10MB | 复制滞后 |
many_scripts |
缓存脚本 > 1000 | EVAL 脚本过多 |
与淘汰的关系:high_frag /
big_peak 不直接说明 LRU 选错,但可区分「policy
误踢热 key」与「内存碎片/lazyfree
未归还」。驱逐本身会制造大量释放块,加剧碎片——长期高驱逐率应同时看
MEMORY PURGE、activedefrag 与 key
设计(大 key、无 TTL)。
本站不粘贴本机 MEMORY DOCTOR
输出;读者可在实例上执行 MEMORY DOCTOR
对照上表理解字段。
六、学术谱系与争论
谱系:Bélády OPT(1966)→ LRU
链表(教科书)→ CLOCK 近似(Corbató 等)→ Redis
采样池(evict.c 注释自 2014 起演进)→ LFU
对数计数(Redis 4.0+)→ ARC/2Q(Megiddo & Modha
2003;Redis 未实现)。
争论:
- 近似 LRU 是否「够好」:Redis
官方文档明确写的是 approximated LRU。对
heavy-tailed 热点,LFU 或业务侧 TTL 常更稳;对扫描,应靠
allkeys-lfu或客户端避免冷数据大面积进入。 - volatile vs allkeys
语义:
volatile-*隐含「无 TTL 数据更宝贵」;若业务相反,应allkeys-*并给常驻 key 显式长 TTL 或PERSIST。 - 采样数 N
的权衡:
maxmemory-samples增大提高近似质量,但单次performEvictionsCPU 上升——与 PostgreSQL Buffer Pool 的 clock sweep 扫全表不同,Redis 刻意限制为常数级采样。
开放问题:Redis
是否会引入自适应策略(ARC 类)至今无主线承诺;模块与多 DB
下淘汰池跨 slot 公平性在 Cluster 场景依赖
kvstore 分片,极端倾斜 slot 的驱逐公平性需结合
第 13 篇
理解。
七、常见误解
7.1 「开了 LRU 就等于操作系统 LRU」
不对。Redis 是 N 随机样本 + 16 槽池 + 秒级时钟,不维护全局 LRU 序。热 key 不在样本中仍可能被踢。
7.2 「淘汰和过期是一回事」
不对。过期看 expires 时间戳;淘汰看
maxmemory-policy 与访问元数据。过期 key 应先被
第 7 篇
路径删除;若仍超内存,才驱逐未过期
key。
7.3 「volatile-lru 会删所有 key」
只删设了 TTL 的 key。无 TTL 的 key 在
volatile-* 下受保护,可能导致 write OOM。
7.4 「LFU 不需要预热」
新 key 以 LFU_INIT_VAL
起步,需若干次访问才能积累频率;极短生命周期的 key 可能被
LFU 误判为冷数据。
八、小结
三句话小结
- 八种 policy 先分
volatile/allkeys候选集,再在 LRU/LFU/TTL/random 间选 victim。 - 近似 LRU/LFU 靠
redisObject.lru时钟/计数 + 每次约 5 个随机样本填入 16 槽淘汰池,不是全局链表。 MEMORY DOCTOR用碎片、峰值、客户端/复制缓冲等启发规则指路,不替代对maxmemory-policy与 workload 的分析。
上一篇:TTL 与过期
下一篇:RDB 快照
参考资料
- Redis OSS 7.4
源码:
src/evict.c(performEvictions、evictionPoolPopulate、estimateObjectIdleTime)、src/object.c(getMemoryDoctorReport)、src/server.h(MAXMEMORY_*、LRU_CLOCK_RESOLUTION、LFU_INIT_VAL)。 - Redis Documentation v7.x, Key
eviction、
maxmemory-samples、lfu-log-factor。 - Megiddo & Modha, ARC: A Self-Tuning, Low Overhead Replacement Cache, USENIX FAST 2003(对照自适应替换;Redis 未实现 ARC)。
- 页面置换算法、Buffer Pool。
- 系列 index、第 7 篇 TTL。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Redis / 缓存内核】TTL 与过期:expires dict · 惰性/主动删除 · lazyfree
拆解 Redis 7.4 的 TTL 机制:expires 辅助表、lookup 路径惰性删除、activeExpireCycle 自适应采样,以及 lazyfree 异步释放大对象对延迟的影响。
【Redis / 缓存内核】RDB:fork COW · BGSAVE · 快照格式
从 rdb.c 拆解 SAVE/BGSAVE 的 fork 写时复制语义、子进程快照路径,以及 REDIS 魔数与 opcode 锚定的 RDB 格式边界。
【Redis / 缓存内核】AOF:命令日志 · rewrite · appendfsync
从 aof.c 拆解 AOF 多段清单、feedAppendOnlyFile 写路径、后台 rewrite 与 appendfsync always/everysec/no 各自的丢失窗口及 fsync 失败语义。
【Redis / 缓存内核】混合持久化与崩溃语义
说明 Redis 7.4 默认开启的 RDB preamble + INCR AOF 如何组合恢复路径,结合 fsyncgate 与数据完整性给出 appendfsync 与混合模式的配置决策树及耐久争论边界。