Theory · Redis · Memory

过期删除与内存淘汰

两套独立机制各管一摊 —— TTL 到期靠惰性+定期回收,内存超限靠 8 种策略近似 LRU/LFU 淘汰

过期删除

定时/惰性/定期三策略取其二:惰性保延迟、定期控内存泄漏;从库不主动删、靠主库 DEL 同步

内存淘汰

maxmemory 超限触发;近似 LRU 用 24bit 时钟 + 随机采样 + 淘汰池,LFU 用 8bit 对数计数器 + 分钟衰减

内存碎片

mem_fragmentation_ratio 判断健康度;activedefrag 用 jemalloc 元数据低延迟搬迁整理

这份 deck 处理 Redis 内存管理的两套机制。第一套过期删除:key 有 TTL,到期怎么删——三种策略选两种的取舍。第二套内存淘汰:内存到 maxmemory 了删谁——近似 LRU 和 LFU 的实现细节。两套机制常被混为一谈,面试第一步就是把它们分开:过期删除管"该不该死",内存淘汰管"内存不够谁先死"。所有数字都有出处:20 个采样、25% 时间预算、5 个采样、100 万次饱和、log-factor 表格,都对照官方文档和源码核过。

Two Mechanisms

先分层:过期删除 ≠ 内存淘汰

维度过期删除(expiry)内存淘汰(eviction)
触发条件key 的 TTL 到期(db->expires 字典记录绝对毫秒时间戳)used_memory 超过 maxmemory(写命令前检查)
作用对象仅设置了过期时间的 key按策略:全部 key 或仅带 TTL 的 key
删除方式惰性删除 + 定期删除(本页后展开)按 maxmemory-policy 采样淘汰
无 TTL 的影响永不过期(内存泄漏风险源头)volatile-* 系列无键可淘汰时退化为 noeviction
主从行为从库不主动删,主库删除后同步 DEL官方文档:该限制只对主库生效(副本跟随删除)
面试第一句:"Redis 的内存管理是两条线:过期删除解决'到期该死没死'的回收,内存淘汰解决'内存不够了谁先走'——触发条件、对象、策略完全不同。"
面试里最先把两套机制分开的人就赢了一半。过期删除的触发是 TTL 到期,expires 字典存的是绝对毫秒时间戳;内存淘汰的触发是 used_memory 超 maxmemory,在写命令前检查。一个关键交叉点:volatile 系列策略只淘汰带 TTL 的 key,如果一个实例没有任何 TTL key,volatile 策略直接退化成 noeviction,写入全报错——这是生产事故的经典来源,必须记住。

Expiry Strategies

过期三种策略:Redis 取惰性 + 定期的组合

策略做法问题Redis 的取舍
定时删除每个 key 设过期时建定时器,到期立即删大量定时器抢 CPU;创建/管理开销大不采用(内存友好但 CPU 不友好)
惰性删除访问 key 时检查 expires 字典,已过期才删并返回 nil从不被访问的过期 key 永远占内存采用(CPU 友好;配合定期补漏)
定期删除周期性随机采样过期字典,删掉命中的过期 key,限时限量单次做得太重阻塞、太轻漏删采用(SLOW/FAST 双模式 + 时间预算,下一页)

惰性删除的完整路径

读/写 key → expireIfNeeded 检查 → 已过期:同步删(或按 lazyfree-lazy-expire 异步删)→ 返回不存在。另外 RDB/AOF 重写、键空间通知、STATS 的 expired_keys 计数都挂在这条路径上。

为什么组合拳是必然

只用惰性:冷 key 过期不删 → 内存持续涨(面试常问"过期 key 会不会泄漏"的答案)。只用定期:采样覆盖不到的 key 仍要靠访问兜底。二者互补:定期批量回收 + 惰性兜底准确。

过期策略经典三选二:定时删除 CPU 不友好不采用;惰性删除访问时才检查,CPU 友好但冷 key 会泄漏;定期删除周期采样限量删,补惰性的漏。面试标准答案是"惰性加定期组合"。惰性删除的入口是 expireIfNeeded,所有读写和重写路径都会过它,开 lazyfree-lazy-expire 时过期删除还能异步化。记住反向问题:如果面试官问"过期 key 会内存泄漏吗",答的就是只用惰性删除的风险,而定期删除正是解药。

activeExpireCycle · src/expire.c

定期删除的引擎:activeExpireCycle 的 SLOW / FAST

// src/expire.c · 关键常量(7.x/8.x)
// 每轮采样键数
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP = 20
// 可接受的"过期键占比"——超过则加速
ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALENESS = 25%
// SLOW:每周期 CPU 时间预算占比
ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC = 25%
// FAST:单次硬上限(微秒)
ACTIVE_EXPIRE_CYCLE_FAST_DURATION = 1000

// 每个库的采样循环:
do {
    if (expireIfNeeded(...)) // 删 20 个采样键中的过期键
while (过期比例 > 25% && 时间预算未用完);
模式运行位置与约束
SLOW每轮事件循环的 beforeSleep 执行;时间预算 = CPU 的 25% ÷ hz(hz 默认 10,即单次约 2.5ms 量级)——渐进式吃掉固定配额
FASTserverCron 每周期执行;硬上限 1ms;仅当 SLOW 落后(上轮过期比例 >10% 或过期键数量超预期)时才启动
自适应逻辑:过期比例 >25% → 立刻再来一轮(说明积压严重);≤25% → 收手等下个周期。用"占比"驱动加速而不是无脑全扫——这就是限时限量的精髓。
定期删除的实现是 activeExpireCycle,两个模式四个数字。SLOW 模式挂在 beforeSleep,每轮预算是 CPU 的百分之二十五除以 hz,hz 默认十;FAST 模式挂在 serverCron,硬上限一毫秒,只在 SLOW 落后时才启动。每次采样二十个键,删掉其中的过期键,如果过期比例超过百分之二十五说明积压严重,立刻再来一轮,直到预算用完或比例达标。这套设计的本质:用过期占比驱动自适应加速,限时限量保证不阻塞主线程。

Expiry × Replication

主从架构下的过期:从库只"装死",删除靠主库

为什么不主从各自删

若从库独立执行过期删除,主从时钟偏差、命令到达顺序差异会造成数据不一致(主库还在用的 key 从库已删)。所以官方设计:过期删除的决策权在主库——主库惰性/定期删除后,把 DEL 作为写命令同步给从库与 AOF。

从库的读体验(3.2+ 的修正)

旧版本从库读已过期 key 会返回旧值(物理未删)。3.2 起从库读命令也会检查 TTL:已过期的 key 逻辑上视为不存在(返回 nil),但物理删除仍等主库 DEL 传来——"逻辑过期 + 物理跟随"的两层模型。

追问答案
主库挂了,从库升主,过期键会瞬间涌出吗?不会:expires 字典随全量 RDB 同步(生成时未过期的键 + 过期时间戳一起带过去),升主后走正常的惰性+定期删除
Lua 脚本里读"已过期但未删"的 key?主库上脚本执行前也会走 expireIfNeeded——主库视角一致;从库(只读脚本)按 3.2+ 逻辑过期返回不存在
怎么观察过期删除情况?INFO stats 的 expired_keys(已删数);INFO keyspace 各库 keys 与带 TTL 的 expires 数对比,判断"死键积压"
主从下的过期是个高频追问。核心设计:删除决策权只在主库,主库删完把 DEL 当写命令同步,避免主从时钟偏差导致的不一致。从库侧要分两层答:3.2 之前读已过期 key 会返回旧值,这是知名坑;3.2 起从库读也检查 TTL,逻辑上返回不存在,但物理删除仍等主库 DEL。这个"逻辑过期加物理跟随"的两层模型要说全。追问方向:升主后过期键不会瞬间涌出,因为 expires 字典随 RDB 全量同步了。

Expiry × RDB / AOF

过期键在 RDB / AOF 里的生命周期

文件生成时加载/恢复时追加行为
RDBBGSAVE 时已过期的键不写入(rdbSave 检查 expires)加载时跳过已过期键(从库加载例外:主从同步场景不跳过,保留过期时间戳由主从机制接管)—(RDB 是快照无追加)
AOF重放时按时间戳判断,已过期键重建后被惰性删除键被惰性/定期删除时追加一条 DEL(lazyfree-lazy-expire 开启时同步 UNLINK 语义);AOF 重写时已过期键不写入
复制流主库的过期删除以 DEL 写命令传播给从库——与 AOF 同源同序,保证主从一致

推论:RDB 天然"自清洁"

每次 BGSAVE 都相当于对过期键做了一次物理压缩——长期不重启的实例,RDB 体积里也不会积累死键。这也是"定期快照对内存友好"的一个冷门佐证。

推论:AOF 里过期键"先活着后死"

AOF 不在写入时过滤过期键(写命令时键还活着),而是在键真正被删时补一条 DEL——所以 AOF 重放结束时,残留的过期键由过期机制兜底删除,恢复出的数据集与宕机前一致。

过期键和持久化的交互是查漏补缺型考点,背三句话:RDB 生成时不写已过期键、加载时跳过,所以快照天然自清洁;AOF 不是写入时过滤,而是键真被删时补一条 DEL,重写时才过滤;复制流和 AOF 同源同序,主库过期删除以 DEL 传播。有个例外细节:从库加载主从同步来的 RDB 时不过滤过期键,要保留过期时间戳由主从机制统一接管,这个反直觉的点能答出来很加分。

maxmemory-policy

内存淘汰:8 种策略全景(noeviction 默认)

策略淘汰范围与依据适用
noeviction(默认)不淘汰;写命令报 OOM error(读命令不受影响)数据不可丢的存储型用法
allkeys-lru全体 key,最久未访问优先缓存默认推荐(官方:帕累托分布下接近最优)
allkeys-lfu全体 key,访问频率最低优先(4.0+)访问频次差异大、有突发扫描流量
allkeys-random全体 key 随机访问近似均匀分布时
volatile-lru仅带 TTL 的 key,最久未访问优先缓存 key 与持久 key 混存一个实例
volatile-lfu仅带 TTL 的 key,频率最低优先同上 + 频次敏感
volatile-random仅带 TTL 的 key,随机同上
volatile-ttl仅带 TTL 的 key,剩余存活时间最短优先业务能按 TTL 语义合理区分重要性

三条官方规则

maxmemory 0 = 不限(64 位系统默认;32 位隐式 3GB);② volatile-* 在没有 TTL key 时退化为 noeviction——只设 maxmemory 不给 key 设 TTL 的实例别选 volatile 系;③ 淘汰与持久化缓冲:复制/AOF 缓冲不计入 maxmemory 判定(mem_not_counted_for_evict)。

版本备注

LFU 系(volatile/allkeys-lfu)4.0 引入。8.6 起新增 allkeys-lrm / volatile-lrm(least recently modified:只在时更新时间戳,读不刷新——适合"想保留常读数据、淘汰久未改数据"的场景)。面试答 8 种是经典口径,LRM 属于加分补充。

八种策略 = noeviction 加三乘三减一:allkeys 和 volatile 两大族,各配 lru、lfu、random,volatile 独有 ttl,共八种。背的时候记住三条规则:maxmemory 默认零不限制;volatile 系没有 TTL key 时退化成 noeviction 直接报错;复制和 AOF 缓冲不计入 maxmemory 判定。选型按官方建议:缓存默认 allkeys-lru,频次敏感上 allkeys-lfu,混合存储用 volatile 系。加分补充是 8.6 的 LRM 策略,只在写时刷新时间戳,面试提一嘴显版本敏感度。

Approximated LRU · src/evict.c

近似 LRU:24bit 时钟 + 随机采样 + 淘汰池

近似 LRU 的采样与淘汰池机制 淘汰时从键空间随机采样 maxmemory-samples 个 key,读取每个 redisObject 的 24bit lru 时钟计算空闲时间,维护一个 16 容量的按空闲时间排序的淘汰池,每次淘汰取池中最久未访问者。 随机取 5 入池比拼 键空间(百万级 key) 蓝色 = 本轮随机采样 5 个 逐个读 redisObject.lru(24bit) key_a · idle = now - lru = 12s key_b · idle = 3s key_c · idle = 86400s(冷) key_d · idle = 45s key_e · idle = 120s lru 为秒级时钟(24bit 约 194 天回绕,时钟倒退有兜底) eviction pool(容量 16,3.0+) 按 idle 从大到小排序的候选数组 head:idle 最大 → 淘汰它 次大候选 … 新采样比池尾更冷才入池 池让"冷 key"跨轮次累积, 5 个采样也能逼近真 LRU maxmemory-samples 5(默认) 10 更准、CPU 更贵 流程:内存超限 → 随机采样 → 读 lru 算 idle → 入池比拼 → 淘汰池首 → 再检查内存 → 循环直到达标 官方对比图结论:5 采样的近似 LRU 与理论 LRU 几乎重合(power-law 访问分布下差异可忽略)
近似 LRU 三件套:24bit 时钟、随机采样、淘汰池。每个 redisObject 里有 24 位秒级时钟,访问时刷新;淘汰时随机采 maxmemory-samples 默认 5 个 key,算出各自空闲时间,扔进容量 16 的淘汰池里比拼,比池里最冷的还冷才入池,真正淘汰时取池首。池的关键作用是跨轮次累积冷 key,让五个采样也能逼近真 LRU,官方对比图显示五采样下和理论值几乎重合。内存中不维护任何全局链表,每个 key 只多花 3 字节,这是整个设计的灵魂。

Design Trade-off

为什么不用"真 LRU":一次内存换精度的谈判

真 LRU 的代价清单

维护全局双向链表:每个 key 额外 2 个指针(16B);每次都要摘链 + 头插(把"读"变成写,缓存行失效、锁竞争);百万 key 的链表操作对内存带宽的挤占远超 3 字节时钟字段。Redis 的选择:每个对象只用 lru:24(3 字节),随机采样近似。

官方实证:近似 ≈ 理论

官方文档的对比实验(power-law 访问分布):2.8 无池版本与真 LRU 有差距;3.0 引入淘汰池后 5 采样的近似已"virtually equivalent";10 采样几乎完全重合。结论:精度损失在真实访问分布下不可观测,省下的内存却能装更多数据。

追问:访问时间怎么算答案
lru 字段什么时候刷新?对象被读或写时(lookupKey 内 touch);但注意:被共享的整数对象(0–9999)不刷新——共享对象-clock 失真是官方接受的代价
24bit 秒级时钟会不会回绕?约 194 天回绕一次;计算 idle 时若 obj.lru > 当前时钟则按回绕补偿(源码 estimationOfIdleTime 的兜底分支)
LFU 模式下这 3 字节去哪了?同一字段语义复用:16bit 分钟时间戳 + 8bit 对数计数器(下一页)——没有为 LFU 增加任何存储
答题收束:"近似 LRU = 用采样统计换链表内存与写放大;redisObject.lru 24bit 是唯一存储成本;淘汰池是让小样本逼近全局最优的关键修正。"
为什么不用真 LRU,要能算账:真 LRU 每个对象两个指针十六字节,而且每次读都要摘链头插,把读操作变成写操作;近似方案每对象只加三字节时钟。官方实验证明五采样加池的近似和理论值几乎等价,十采样完全重合。三个追问要备好:lru 字段在读或写时刷新,但共享整数对象不刷新;24 位秒级时钟约 194 天回绕,源码有兜底;LFU 模式下同一字段直接复用,不增加任何存储。最后一问正好引出下一页的 LFU。

LFU · 4.0+ · Morris Counter

LFU:24bit 拆两半,8bit 计数器装下 100 万次

LFU 的 24bit 字段拆分与对数计数器 redisObject 的 lru 字段在 LFU 模式下拆为 16bit 分钟时间戳 ldt 与 8bit 对数计数器 logc;计数器以概率 1/(counter×factor+1) 递增,factor 默认 10 时 100 次访问计数 10、十万次计数 142、百万次饱和 255;空闲衰减按 lfu-decay-time 每分钟减一。 redisObject.lru:24(LFU 模式语义复用) ldt:16bit · 最后衰减时间(分钟,约 45 天回绕) logc:8bit · 0–255 访问时计数器怎么涨(Morris 算法) P(counter+1) = 1/(counter×factor+1) lfu-log-factor 默认 10 · 初值 LFU_INIT_VAL=5 factor=10 时的官方数值表(访问次数 → 计数器值) 100 hits 1000 hits 100K hits 1M hits 10 18 142 255(饱和) factor 越小:低频段分辨率越高(100 hits 就 104);factor 越大:越能区分高频 key factor=0:100 hits 即 104、1000 hits 即 255 —— 表格取自 redis.io eviction 文档(lfu-log-factor 章节) logc=0 语义特殊:表示"从未被访问",不参与概率增长(LFU_INIT_VAL=5 保证新 key 有初始频次) 衰减:ldt 与 lfu-decay-time 访问时先衰减再增长: elapsed_min = now_min - ldt counter -= elapsed_min / decay_time decay_time 默认 1(每闲置 1 分钟减 1) 0 = 永不衰减 效果:昨天 100 万次的爆款,停访 一天后频率暴跌 → 新热 key 能上位
LFU 的精妙全在这一页。24 位字段语义复用:高 16 位是分钟时间戳,低 8 位是计数器,没有增加任何存储。计数器用 Morris 概率算法:以一除以计数乘因子加一的概率递增,因子默认十,所以 100 次访问才到 10,十万次到 142,一百万次饱和 255——8 位装下一百万次访问的区分度。衰减在每次访问时先做:闲置每分钟减一,衰减时间默认一分钟,零表示永不衰减。这保证昨天的爆款今天不霸内存。面试报出 100 次对 10、十万次对 142 这组数,就是真看过官方文档。

Tuning · lru vs lfu

LFU 调参与 LRU/LFU 选型:什么流量该用谁

lfu-log-factor 与 lfu-decay-time 的调法

想更细腻区分高频 key(如计数值差异大的业务):调大 factor(100 → 100 万次才 143);想更快"降温":调小 decay-time(0 永不衰减只适合访问模式恒定的场景)。官方默认 factor=10 / decay=1 是通用起点,生产上用 OBJECT FREQ key 观察实际分布再调。

什么时候 LFU 反而输给 LRU

周期性批量任务(报表/预热)会瞬间抬高一批 key 的计数并长期驻留——LFU 记"频率"不记"最近",靠 decay 缓解但分钟粒度较粗;而 LRU 天然偏向最近访问。突发爬虫扫库:LFU 完胜(扫过的冷 key 计数极低被秒杀;LRU 反而刚把它们摸热)。

场景推荐策略理由
典型缓存(帕累托访问)allkeys-lru官方默认推荐,实现最简、表现稳定
有明显"一次性扫描/回溯"流量allkeys-lfu扫描 key 计数低,立即被淘汰,不冲掉热数据
缓存 + 永不过期的配置/元数据混存volatile-lru/lfu只动 TTL key;注意"无 TTL 键退化为 noeviction"的坑
业务能显式表达"谁先死"volatile-ttl剩余 TTL 最短优先,与业务语义对齐
OBJECT 系列查看工具:OBJECT FREQ key(LFU 计数值,仅 lfu 策略下可用)、OBJECT IDLETIME key(空闲秒数,仅 lru 语义;注意查询本身会刷新 lru,LFU 模式才不污染观察)。
LFU 调参两句口诀:factor 越大越能区分高频,decay 越小降温越快,默认十和一是通用起点,调之前用 OBJECT FREQ 看实际分布。选型的反例要会讲:周期性批量任务会把一批 key 计数抬高,LFU 不记最近所以可能久驻,LRU 反而合适;而突发扫描流量 LFU 完胜,爬虫摸过的冷 key 计数极低被秒杀。volatile 系的退化坑再强调一次。最后是观察工具的小陷阱:OBJECT IDLETIME 本身会刷新 lru,污染观察,LFU 模式才没有这个问题。

Eviction Flow · performEvictions

淘汰在哪一刻发生:写命令前的内存闸门

内存淘汰执行流程 写命令执行前检查 used_memory 是否超过 maxmemory:未超直接执行;超过则进入淘汰循环——按策略采样候选、维护淘汰池、批量删除直到内存达标或无可淘汰键,无可淘汰时返回 OOM 错误;被淘汰键以 DEL 同步给从库与 AOF。 no yes 写命令到达(SET / LPUSH / …) used > maxmemory ? 写命令前检查(读命令不检查) 直接执行命令 大块写入可能瞬间超出 maxmemory(官方注明) 淘汰循环:采样 → 更新淘汰池 → 淘汰 每次批量删除若干(无上限渐增),删除以 lazyfree 判定 内存达标 or 无键可淘汰? 未达标且仍有键 → 继续循环 执行原命令 (淘汰腾出的空间) noeviction / volatile-*无键可淘汰 → 返回 OOM 错误 "OOM command not allowed when used memory > 'maxmemory'" 被淘汰的 key 作为 DEL 写命令传播: → 从库同步删除 → 追加 AOF(持久化) → 触发 keyspace 通知
淘汰的执行时机:官方文档口径是每当客户端执行会增加数据的命令,先检查内存是否超限,超了就先淘汰再执行;读命令不触发。淘汰循环里采样、入池、批量删,删除动作本身按 lazyfree 判定可异步。三个结果分支:达标后执行原命令;无键可淘汰或 noeviction 返回 OOM 错误,那个报错原文最好能背。别忘了被淘汰的 key 会作为 DEL 传播给从库和 AOF——淘汰不是本地行为,是主从一致的写事件。还有个官方注明的边界:单条大块写入可能瞬时超出 maxmemory。

Fragmentation · INFO memory

内存碎片:mem_fragmentation_ratio 与 activedefrag

怎么读这个指标

mem_fragmentation_ratio = used_memory_rss / used_memory(OS 实际分配 ÷ Redis 自报)。官方理想区间 1.0–1.5:>1.5 碎片偏高(频繁增删改 + 删除大 key 的宿命);<1 说明用了 swap——比碎片严重得多的红色警报(Redis 内存被换出到磁盘)。

成因:分配器按大小类(size-class)分配 + key 的生死交错,页内空洞无法还给 OS。

activedefrag(4.0+):边运行边整理

原理一句话:利用 jemalloc 的 arena/bin 元数据找出"碎片率高的页",把存活数据搬迁到连续空间再归还页——搬迁以微秒级步长分摊在事件循环里(active-defrag-cycle-min/max 限 CPU 1%–75%),不阻塞服务。依赖 jemalloc(默认编译带)。

开启:CONFIG SET activedefrag yes(默认 no)。触发阈值:active-defrag-ignore-bytes 100mb、threshold-lower 10%(碎片比 RSS 超 10% 起步)、threshold-upper 100% 全力整理。

手段对比说明
activedefrag(在线)不重启、不复制数据,代价是少量 CPU;适合大实例无法重启的生产库
重启加载 RDB/AOF(离线)最彻底的"碎片整理"——重启后数据从文件紧凑重建(这也是恢复速度快的另一个收益)
预防:减少交错生死TTL 打散(加随机抖动避免同刻批量过期)、避免大 value 反复写删——治本
碎片这页记三个数:ratio 是 RSS 除以自报内存,理想区间一到一点五;大于一点五碎片偏高;小于一是红色警报,说明用了 swap,这比碎片严重得多。activedefrag 四点零引入,原理一句话:借助 jemalloc 的元数据找碎片页,把存活数据搬进连续空间,搬迁步长微秒级分摊在事件循环里,默认关闭要手动开。两个备用方案:重启加载 RDB 是最彻底的碎片整理;治本是让 TTL 加随机抖动、避免同刻批量过期和大 value 反复写删。

Interview QA · 1/2

过期删除 8 连问

1 · 过期删除有哪三种策略?Redis 怎么选?

定时/惰性/定期取其二

定时删除(每个 key 挂定时器)CPU 不友好,不采用;惰性删除(访问时 expireIfNeeded 检查)+ 定期删除(activeExpireCycle 限时限量的随机采样删除)组合——CPU 与内存的平衡,冷 key 靠定期补漏。

2 · 定期删除的时间预算怎么控制?

SLOW 25%/hzFAST 1ms采样 20

SLOW 挂 beforeSleep,每轮预算为 CPU 25% ÷ hz(hz 默认 10);FAST 挂 serverCron,硬上限 1ms,仅在 SLOW 落后时启动。每轮采样 20 个键,过期占比 >25% 就立刻再来一轮(自适应加速),否则收手。

3 · 从库会主动删过期 key 吗?读已过期 key 返回什么?

不主动删3.2+ 逻辑过期

不主动删:删除决策权在主库,主库删除后以 DEL 同步(避免时钟偏差不一致)。从库读侧 3.2 起也检查 TTL——已过期键逻辑上返回 nil(逻辑过期),物理删除等主库 DEL。

4 · RDB 和 AOF 分别怎么处理过期键?

RDB 生成即过滤AOF 补 DEL

RDB:BGSAVE 时不写已过期键,加载时跳过——快照自清洁。AOF:写入时不过滤(当时键还活着),键真被删时追加 DEL;AOF 重写时过滤。主从同步场景从库加载 RDB 不过滤(要保留过期时间戳)。

5 · TTL 是怎么存的?精度多高?

expires 字典绝对毫秒时间戳

db->expires 字典:key → 绝对毫秒时间戳(EXPIRE 会换算);PERSIST 删除该条。因存绝对时间,主从时钟偏差影响的是"什么时候该删"的判断,这正是从库不主动删的原因。7.0 起 EXPIRE 支持 NX/XX/GT/LT 选项;7.4 起 hash field 级 TTL(HEXPIRE)。

6 · 大量 key 同一时刻过期会怎样?怎么避免?

过期风暴TTL 随机抖动

定期删除会因占比>25% 进入加速模式,但集中失效仍可能造成延迟毛刺与(淘汰策略下)连锁淘汰。避免:TTL 加随机抖动(基础值 ± 随机偏移);避开热点写入同一批相同 TTL;监控 expired_keys 曲线。

7 · 惰性删除会内存泄漏吗?谁兜底?

单用会定期删除兜底

只靠惰性删除:从不被访问的过期 key 永不回收——泄漏。定期删除的采样回收正是兜底机制;二者组合下"过期但未删"只是短暂状态。运维可对比 INFO keyspace 的 keys 与 expires 数、观察 expired_keys 增长判断积压。

8 · lazyfree-lazy-expire 是干什么的?默认开吗?

异步释放默认 no

过期键的内存释放交给 BIO_LAZY_FREE 后台线程(删除决策仍是主线程),防过期风暴时批量 free 卡主线程;默认 no,大 value 场景建议 yes。与 UNLINK 的 user-del 开关同属 lazyfree 配置族(thread-model deck)。

过期侧 QA 的得分点在数字和边界。第二题四个数要连报:采样二十、占比二十五加速、SLOW 预算百分之二十五除以 hz、FAST 一毫秒。第五题的绝对毫秒时间戳是理解主从设计的关键,还能顺带提 7.0 的 NX/XX/GT/LT 和 7.4 的 HEXPIRE 显版本感。第六题过期风暴给生产解法:TTL 随机抖动。第八题连接 thread-model 的 lazyfree 体系,答"删除决策在主线程、释放在后台线程"这个精确表述。

Interview QA · 2/2

内存淘汰与碎片 8 连问

9 · 8 种淘汰策略背一遍?哪个是默认?

noeviction 默认allkeys/volatile × lru/lfu/random + ttl

noeviction(默认,写报 OOM)+ allkeys 系(lru/lfu/random)+ volatile 系(lru/lfu/random/ttl)。规律:allkeys 全库找牺牲者;volatile 只动带 TTL 的 key,无 TTL 键时退化为 noeviction。8.6 起另有 lrm(最近未修改)作补充。

10 · 近似 LRU 怎么实现?为什么不用真 LRU?

lru:24 时钟采样 5池 16

每对象 24bit 秒级时钟(3 字节);淘汰时随机采样 maxmemory-samples(默认 5)个 key,按 idle 维护容量 16 的淘汰池,取池首淘汰。不用真 LRU:全局双向链表每对象 2 指针 + 每次读都写链表;官方实验证明 5 采样+池在 power-law 分布下几乎等价。

11 · maxmemory-samples 调大有什么影响?

精度 ↑ CPU ↑

官方口径:5 是精度/成本平衡点;10 接近真 LRU(文档原话"very close to the theoretical performance"),代价是每次淘汰更多采样 CPU。可 CONFIG SET 在线调整并用命中率(keyspace_hits/misses)对比验证。

12 · LFU 的 24bit 怎么拆?计数器怎么涨怎么衰?

16bit 分钟 + 8bit 计数Morris 概率增长

高 16 位 ldt(分钟时间戳),低 8 位 logc。访问时先衰减(闲置每 decay-time 分钟减 1,默认 1)再按概率 P=1/(counter×factor+1) 增长(factor 默认 10);初值 5,0 表示从未访问,上限 255 ≈ 100 万次访问饱和。

13 · factor=10 时,100 次和 10 万次访问的计数是多少?

10 / 142官方表格

官方表格:100 hits → 10;1000 → 18;100K → 142;1M → 255 饱和。factor 越大低频分辨率越低、高频区分越细;factor=0 时 100 次就到 104。能报这组数说明真读过 eviction 文档而非背二手结论。

14 · 淘汰发生在什么时机?淘汰的 key 会同步给从库吗?

写命令前检查DEL 同步

官方口径:客户端执行"会增加数据"的命令前检查 used_memory,超限先淘汰再执行(读命令不触发)。被淘汰 key 以 DEL 写命令传播给从库、写入 AOF——淘汰是主从一致的写事件,不是本地私刑。

15 · mem_fragmentation_ratio 怎么算?<1 说明什么?

RSS / used_memory<1 = swap

INFO memory:ratio = used_memory_rss / used_memory,理想 1.0–1.5;>1.5 碎片偏高;<1 表示 OS 把 Redis 内存换出到 swap——比碎片更严重的警报(延迟暴涨)。成因是分配器 size-class 与 key 生死交错的页内空洞。

16 · activedefrag 的原理?什么时候开?

jemalloc 元数据微步搬迁默认 no

4.0+:利用 jemalloc arena/bin 元数据找碎片页,把存活数据微步搬迁到连续页并归还(步长分摊在事件循环,active-defrag-cycle 限 CPU 1%–75%),不阻塞服务。ratio 持续 >1.5 且无法重启时开启;治本靠 TTL 抖动与减少大 value 反复写删。

淘汰侧 QA 的硬核点:第十题池容量 16 和三字节成本连着说;第十三题报出官方数值表 10、18、142、255;第十四题强调淘汰以 DEL 传播,是主从一致的写事件;第十五题 ratio 小于一是 swap 警报,这个比碎片本身更重要。第十六题 activedefrag 的关键词是 jemalloc 元数据和微步搬迁,加 CPU 预算参数。整组答完,内存管理的追问链基本封死。

Related · Cross-links

相关知识点

Redis 领域 · 同批 deck

data-structures · 对象系统与底层数据结构(redisObject.lru:24 字段的出处;编码切换影响单 key 内存)
cache-patterns · 缓存模式与一致性(TTL 设计与淘汰策略是缓存架构的运行时半边)
persistence · RDB / AOF / 混合持久化(过期键在 RDB/AOF 中的处理、COW 与 maxmemory 联动)
thread-model · 单线程模型(lazyfree 线程、beforeSleep 里的 SLOW 过期)

跨领域 · 答题串联

MySQL MVCC:Redis 惰性删除 ↔ InnoDB delete-mark + purge——"先标记后回收"的同构
Kafka Internals:Kafka 日志保留(时间/大小)↔ Redis 过期+淘汰;都把"删除"设计成后台渐进动作
GMP 调度器:Go GC 的 mark 后台化 ↔ Redis 限时限量清理——共同哲学:不给主路径添堵
OS · 页面置换:Clock / 双链 / MGLRU——Redis 采样 LRU 的内核侧同族

收尾串联三条跨领域线:惰性删除和 InnoDB 的 delete-mark 加 purge 是同构的"先标记后回收";Kafka 的日志保留和 Redis 的过期淘汰都在做后台渐进删除;Go GC 的后台标记与 Redis 限时限量清理共享"不给主路径添堵"的哲学。串联读完,翻下一页对照参考来源回到原文。

References

参考来源

本 deck 全部结论可溯源至下列一手材料——数字与版本结论以原文为准。

redis.io/docs/latest/develop/reference/eviction/8 种策略清单、volatile 退化行为、近似 LRU 与 pool、LFU 数值表(100→10 / 100K→142 / 1M→255)、LRM(8.6+)
redis.io/docs/latest/commands/info/(memory/stats 段)mem_fragmentation_ratio、expired_keys、evicted_keys、mem_not_counted_for_evict
github.com/redis/redis · src/evict.cEVPOOL_SIZE=16、estimationOfIdleTime(回绕兜底)、LFULogIncr / LFUDecrAndReturn、performEvictions
github.com/redis/redis · src/expire.cactiveExpireCycle 常量:LOOKUPS_PER_LOOP=20、ACCEPTABLE_STALENESS=25%、SLOW_TIME_PERC=25%、FAST_DURATION=1000μs
github.com/redis/redis · redis.conf(8.2)maxmemory-samples 5、lfu-log-factor 10、lfu-decay-time 1、hz 10、activedefrag no 与阈值组、lazyfree-lazy-* 默认 no
redis.io/docs/latest/operate/oss_and_stack/management/persistence/过期键与 RDB(生成/加载过滤)、AOF(DEL 追加与重写过滤)的交互
redis.io/docs replication · 3.2 release notes主从过期删除权在主库;3.2 起副本读已过期键返回 nil(逻辑过期)
antirez 博客《Random notes on improving Redis's LRU approximation》(2014)采样 + pool 的设计动机与实验(3.0 引入池的原始出处)
这份 deck 的所有数字(20、25%、16、5、10、142、255)都对应上表列出的源码常量和官方文档表格,复习时优先回到 eviction 文档和 evict.c 原文。