/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
运维或测试 Redis 时,常会遇到两个反直觉现象:给大量键设置了 TTL,时钟早已越过到期点,监控中的内存占用却纹丝不动;而在另一批键上明明配置了 24 小时生存期,刚存入两小时,数据却提前消失了。
这两个现象背后,是两条正交的内核机制:过期删除(Expiration Deletion)与内存淘汰(Memory Eviction)。
前者由数据时效驱动,处理逻辑到期与物理回收的节奏;后者由容量预算驱动,处理内存逼近上限时的空间腾挪。TTL 只是生命周期预期,并不保证数据不会因内存淘汰提前驱逐;而键到期也只代表逻辑失效,物理内存绝非到期瞬间即刻释放。
建立基准:四条不同属性的缓存 Key
在数据库中放置四条具象业务键作为推导基准:
| session:token:901 | 剩余 5 秒到期 | 距上次访问较久,历史频次较低 | db->dict 与 db->expires |
| cache:order:1024 | 剩余 3600 秒到期 | 最近刚被访问,历史频次较高 | db->dict 与 db->expires |
| config:whitelist | 未设 TTL(持久键) | 距上次访问较久,历史频次较低 | 仅 db->dict |
| counter:active_uv | 未设 TTL(持久键) | 最近刚被访问,历史频次较高 | 仅 db->dict |
在内核结构体 redisDb 中,这四条键分别登记在两个核心字典中:
- 全局字典(db->dict):保存当前库内全部键值对,4 个键全量在列;
- 过期字典(db->expires):仅保存设定了存活时间的键,字典的值为 64 位绝对到期毫秒时间戳,只有 session:token:901 与 cache:order:1024 登记于此。
有了这层基准,再来观察时钟越过 5 秒后系统的物理走向。
过期键的回收:逻辑失效与物理释放的割裂
5 秒之后,session:token:901 到期时间已过。在 Redis 内核中,到达截止时间点发生的仅是逻辑失效,而非物理内存的即刻释放。
1. 逻辑失效与副本表现
时钟越过截止点后,客户端发起读取(如 GET / EXISTS)时,主节点核对毫秒时间戳直接判定该键不存在并返回空。
在主从架构中:
- 普通只读副本通常不自主物理删除复制来的键,但可依据本地时钟对读取进行隐藏,返回空或视作不存在;
- 物理删除仍依赖主节点同步。主节点执行删除时,会向从节点与 AOF 传播删除命令,具体为 DEL 或 UNLINK(取决于是否配置了 lazyfree-lazy-expire 等)。
若到期后无人访问,且尚未被主动扫描抽样命中,该键可能尚未被物理清理,继续驻留堆内存中,因此监控内存纹丝不动。
2. 物理回收的两条协同路径
要让超期键占用的内存真正返还,依赖两条协同运行的回收路径:
路径 A:惰性删除(Lazy Deletion)
惰性删除属于常规访问路径上的被动拦截。主线程在常规调用链中会调用 expireIfNeeded()(位于 src/db.c)。
读取命令(如 GET / EXISTS)命中超期键时,主线程将其从全局字典与过期字典脱链摘除,向客户端返回空。惰性删除并不意味着所有命令都会返回空;例如写入命令 SET,检测到旧键过期后会将其视作不存在并覆写新值。
惰性删除将开销分摊在访问中,对 CPU 平滑,但完全不被访问的冷键无法触发。
路径 B:主动过期(Active Expiration)
针对冷键,主线程通过 activeExpireCycle()(位于 src/expire.c)进行周期性增量清理。
Redis 既不为每个键分配独立定时器,也不会定时全库全量扫描。主动过期是主线程在时间预算约束下的增量抽样回收:
- 执行时机:分为慢速周期(SLOW,由 serverCron 驱动,执行频率由 hz 决定且支持 dynamic-hz 动态调整)与快速周期(FAST,在 beforeSleep 中满足特定条件触发,并非每次休眠前都真正扫描);
- 抽样与预算:主线程增量遍历 db->expires 随机抽样,根据抽样中过期键积压程度决定是否继续,同时受可调的时间预算约束。预算检查在分批迭代中推进,单个大对象析构也消耗耗时,并非严格立刻中断或固定的全进程 CPU 上限。
两套机制协同工作:任一路径先摘除该键后,另一路径后续不再重复删除。

当内存逼近 maxmemory:8 种内存淘汰策略
如果大量键尚未到期(如 cache:order:1024 还剩 3600 秒),或根本未设置 TTL(如持久键 config:whitelist 与 counter:active_uv),过期删除机制便无能为力。
当内存占用逼近上限时,**内存淘汰机制(Eviction)**接管。
1. 触发时机与候选范围
内存淘汰并不只限于下一条写命令到达时。在常规命令路径 processCommand 中,符合条件的主线程会在执行命令前调用 performEvictions()(位于 src/evict.c);此外,在后续时间事件等调度中,Redis 也会持续检测并尝试淘汰。
策略的候选范围分为两类:
- allkeys-*:候选范围来自 db->dict 全量字典,基准 4 键全部纳入候选视野;
- volatile-*:候选范围来自 db->expires 过期字典。既包含尚未到期的键,也不排除逻辑上已到期但尚未被物理清理的键;它并不专指已到期键,但不能将其限定为“尚未到期的合法键”。若库内全为持久键,volatile-* 抓不到候选,无法腾出空间。
2. 八种淘汰策略对比
Redis 7.2.4 提供了 8 种淘汰策略,核心差异在于候选范围与淘汰规则的组合:
| noeviction | 不淘汰键 | 不驱逐任何数据,内存超限时拒绝部分写入命令 |
| allkeys-lru | db->dict (全量键) | 优先淘汰空闲时间最长(距上次访问最久远)的键 |
| allkeys-lfu | db->dict (全量键) | 优先淘汰近似访问频率最低的键 |
| allkeys-random | db->dict (全量键) | 随机抽取键进行驱逐 |
| volatile-lru | db->expires (带TTL键) | 优先淘汰带 TTL 键中空闲时间最长的键 |
| volatile-lfu | db->expires (带TTL键) | 优先淘汰带 TTL 键中近似访问频率最低的键 |
| volatile-random | db->expires (带TTL键) | 随机抽取带 TTL 的键进行驱逐 |
| volatile-ttl | db->expires (带TTL键) | 优先淘汰剩余存活时间最短的键 |
3. 空间不足时的拒绝表现
默认策略是 noeviction。
当配置为 noeviction,或策略在候选集合中无法腾出足够内存时,Redis 不会停止服务:
- 内核不会在执行前精确预估该命令实际将分配多少字节;
- 而是根据命令标志位,直接拦截带有 CMD_DENYOOM(denyoom)标志的命令(如常见的 SET、HSET、LPUSH 等可能增加内存占用的操作),返回错误:OOM command not allowed when used memory > 'maxmemory';
- 读命令(如 GET、EXISTS)以及释放命令(如 DEL)不带该标志,通常依然可以正常执行;
- 内核亦不承诺每次淘汰都一定能将水位彻底压回达标线以下。

近似机制的设计权衡:LRU 与 LFU 的内核实现
如果实现严格的 LRU 或 LFU,需要维护覆盖全库的全局链表或排序树,每次读写都要调整节点,单线程模型下内存与 CPU 开销沉重。
Redis 采用的是小样本随机抽样与局部候选池的近似机制。
1. LRU 与 LFU 的考量维度
在对象内部,Redis 维护了访问与时间相关的元数据:
- LRU(最近最少使用):关注对象的空闲时长(Idle Time)。通过当前时钟减去对象记录的访问时间戳,差值越大代表越久未被使用,在抽样中优先被淘汰;
- LFU(最不经常使用):综合考量访问频次与时间衰减。每次访问时按对数概率提升计数,同时根据距离上次访问跨越的时间执行衰减。突发批量访问但后续沉寂的冷键,其计数会随时间迅速衰减,从而避免冲刷掉真正长期高频的热点键。
需要强调的是,同一对象的元数据是根据当前生效的淘汰策略来解释的,内核不会在同一时刻同时维护精确的 LRU 绝对空闲时长与 LFU 访问频次。
2. 抽样与淘汰候选池
为了逼近全局排序,Redis 采用抽样候选机制:
- 对于 LRU、LFU 与 volatile-ttl 策略,主线程每次从目标字典随机抽取若干样本,计算淘汰权重,并按升序维护在固定容量的局部有序候选池中,随后弹出池中最优的键执行删除;
- volatile-ttl 仅是在抽样样本中偏向淘汰剩余存活时间更短的键,并不保证找到全库绝对最短 TTL;
- 对于 random(allkeys-random 与 volatile-random)策略,则是直接随机抽取键进行物理驱逐,不经过有序候选池的排序。
通过这种局部样本抽样,Redis 避免了维护全局链表的沉重负担,以极低的性能开销获得了逼近全局排序的驱逐效果。
物理内存的真实边界与底层避坑
围绕内存管理,有四个容易混淆的物理边界:
1. maxmemory 不是操作系统 RSS 的硬上限
maxmemory 限制的是 Redis 内部统计的已用内存(used_memory),并在核算时扣除了部分不计入淘汰的特定缓冲(例如主从复制输出缓冲等)。它既不是纯数据集占用,更不是操作系统层面的常驻物理内存(RSS)。进程 RSS 还包含内存分配器的碎片、输入输出网络缓冲等。物理内存 RSS 明显高于 maxmemory 属于正常现象,切勿将 maxmemory 设为物理机总内存的 100%。
2. 执行大命令允许短时超出
Redis 检查淘汰发生在处理命令的过程中。单条命令写入超大对象时,分配操作可能导致内存瞬间超出 maxmemory。这种超出并非无限制,在后续常规命令处理路径及时间事件调度中,内核会持续检测并继续尝试淘汰。
3. 异步释放(Lazy Free)的权责边界
配置项 lazyfree-lazy-expire 与 lazyfree-lazy-eviction 在 7.2.4 默认均为 no。即便开启,后台 I/O 线程负责的仅仅是符合条件的大对象底层内存块的解构与销毁。而过期的判定、淘汰候选池的采样计算、以及将键从字典中解绑摘除,依然由主线程同步完成。
4. 对象解绑不等于操作系统 RSS 立即回落
一个键从字典摘除后,其内存释放经历不同阶段:
- 若交由 lazyfree 后台析构,后台完成释放前 used_memory 并不会立即下降;
- 即使底层的内存调用被执行、used_memory 回落,进程 RSS 也往往平稳不变。这是由于内存分配器(如 jemalloc)为了兼顾后续分配效率,会将内存保留在其内存池中复用,不会立刻归还给操作系统。
总结
淘汰策略的选型应结合数据可恢复性、访问模式以及对写入中断的容忍度综合考量:
- 数据可完全由后端存储重建,且访问存在明显二八倾斜:选用 allkeys-lru,让冷数据自然让出空间;
- 业务存在周期性批量扫库或突发性遍历:选用 allkeys-lfu,避免一次性突发流量冲刷掉长期高频热点;
- 缓存与必须保留的持久数据混部:可选用 volatile-lru 或 volatile-lfu。未设 TTL 的键不参与淘汰候选,但并不构成全局“绝对不丢”;若希望写入严格受控且业务要求明确报错,可保留默认 noeviction,通过报警触发人工介入或集群扩容;
- 带 TTL 的键需要按时效偏向清理:可选用 volatile-ttl,但需明确,它并不保证键能完好存满其生命周期,在容量吃紧时依然会被提前驱逐。
在监控排查中,INFO stats 输出的两个核心计数器需理性研判:
- expired_keys:累计通过过期机制正常终结的键总数;
- evicted_keys:累计因触碰内存上限被淘汰机制强制驱逐的键总数。
这两个指标均为单调递增的累积计数,单看绝对值无法定性健康状况,必须观察单位时间内的增量速率,并结合缓存命中率与内存水位综合分析。evicted_keys 的持续增长反映出活跃工作集可能正在挑战配置的容量边界;若因此导致后端数据库查询增多,属于缓存缺失引发的常规回源,不应混同于针对不存在数据的“缓存穿透”。
网硕互联帮助中心


评论前必须登录!
注册