5.1 过期策略与内存淘汰算法 本节摘要:过期删除与内存淘汰是两套独立机制:前者处理"到了死期的键",用惰性加定期两段式删除;后者处理"内存满了怎么办",由 maxmemory-policy 从八种策略里选牺牲者。分清这两层,内存告警的排查才有章法。 过期不是倒计时器 TTL 家族还有两个易混成员记一下:PTTL 返回毫秒精度,定时精确的场景用它;EXPIREAT 接收绝对时间戳,适合"所有环境统一在凌晨四点失效"这类与负载无关的时点。另外对已有 TTL 的键再次 SET(不带 EX 参数)会悄悄抹掉过期时间——值更新连带"续命"是 SET 的默认行为,想保住原 TTL 得改用 SET 加 KEEPTTL 选项,这个坑在新人代码里出现的频率高得惊人。
本节摘要:过期删除与内存淘汰是两套独立机制:前者处理"到了死期的键",用惰性加定期两段式删除;后者处理"内存满了怎么办",由 maxmemory-policy 从八种策略里选牺牲者。分清这两层,内存告警的排查才有章法。
> SET token:a8f3 "uid:1001" EX 3600 > TTL token:a8f3 (integer) 3599 > PERSIST token:a8f3 # 反悔:变成常驻键
TTL 家族还有两个易混成员记一下:PTTL 返回毫秒精度,定时精确的场景用它;EXPIREAT 接收绝对时间戳,适合"所有环境统一在凌晨四点失效"这类与负载无关的时点。另外对已有 TTL 的键再次 SET(不带 EX 参数)会悄悄抹掉过期时间——值更新连带"续命"是 SET 的默认行为,想保住原 TTL 得改用 SET 加 KEEPTTL 选项,这个坑在新人代码里出现的频率高得惊人。
EXPIRE 之后键并未挂上一个到点响铃的定时器。服务端只是把它记进过期字典(又是那张 dict:键映射到期时间戳)。真正的删除发生在两处:
惰性删除:任何命令访问键时先查过期字典,过期就地删除并返回空。代价几乎为零,但没人访问的过期键会一直占着内存。
定期删除:弥补上面的漏洞。默认每秒 10 次,每次从设置了过期时间的键里随机抽 20 个检查,删掉过期的;若过期占比超过四分之一,立刻再抽一轮,直到本轮时间配额(默认 25 毫秒)用完。

maxmemory 触顶后,写入命令触发淘汰逻辑。八种策略按两个维度组合:候选范围(全部键还是只限设了过期时间的键)与挑选规则(LRU、LFU、TTL、随机):
| 策略 | 候选 | 规则 | 适用 |
|---|---|---|---|
| noeviction | 不淘汰 | 拒绝写入 | 默认;存储型实例 |
| allkeys-lru | 全部 | 最久未用 | 通用缓存首选 |
| allkeys-lfu | 全部 | 最少使用 | 有明显冷热分层的缓存 |
| allkeys-random | 全部 | 随机 | 键访问概率均等时 |
| volatile-lru | 有 TTL | 最久未用 | 常驻数据与缓存混部 |
| volatile-lfu | 有 TTL | 最少使用 | 同上偏频次 |
| volatile-ttl | 有 TTL | 剩余时间最短 | 越早过期越先走 |
| volatile-random | 有 TTL | 随机 | 少用 |
实现细节值得知道:Redis 的 LRU 是近似 LRU——每个键对象头里记一个 24 位的最近访问时钟,淘汰时随机抽 N 个(默认 5)挑最旧的逐出,不是全局排序。省下的内存远比精确性损失值钱。LFU 同样近似:4 位计数器对数式增长,长期不访问会衰减,防止"上古爆款"占着茅所。
LFU 的两个参数直接控制它的"记性"。lfu-log-factor 越大,计数器增长越慢——同样访问一万次,因子 10 时计数大约停在两位数,热度排行仍然有效;lfu-decay-time 是衰减周期,默认 1 表示隔一分钟不访问就减一档。调参的验证手段是 OBJECT FREQ,它能直接读出某键的当前频率计数:
> CONFIG SET maxmemory-policy allkeys-lfu > SET hot:page home # 模拟访问若干次后 > OBJECT FREQ hot:page (integer) 5 # 频次计数,随访问对数增长、随空闲衰减 > OBJECT IDLETIME hot:page (integer) 12 # 空闲秒数,LRU视角的另一个体检指标
这两条只读命令是淘汰策略的显微镜:切 LFU 前抽样看热键的 FREQ 是否分层明显,切 LRU 后看 IDLETIME 是否符合访问间隔直觉。策略配得对不对,不用等线上出问题,抽查几个键就有答案。
maxmemory 4gb maxmemory-policy allkeys-lru maxmemory-samples 10 # 抽样加大一点,换更准的淘汰
⚠️ 常见坑:缓存与常驻数据混在一个实例又用 volatile 系策略时,常驻键不设 TTL 就永远安全——但如果有人误设了 TTL,核心数据可能被逐出。混部实例宁可拆开,也别赌配置永远不被人改错。
再展开一个真实事故形状:过期风暴。背景:活动预热时批量写入五十万个验证码键、TTL 统一十分钟,到期时刻集中;过程:到期瞬间定期删除一轮抽样里过期占比远超四分之一,循环加抽、单轮时间配额被吃满,主线程在删除循环上耗时抬升,同期的业务命令排队变长;INFO stats 里 expired_keys 出现每秒数万的跳涨,正是这场的指纹。对策在写入侧:TTL 统一加随机抖动(比如基础值加零到九十秒的随机量),把到期时刻摊开;已经埋下的集中到期,用 SCAN 配合 TTL 找出同秒到期的批次提前分批 UNLINK,别等风暴自己来。
两个相关联的边界认知。其一,淘汰与过期是两套独立机制:没设 TTL 的键也会被 allkeys 系策略淘汰;设了 TTL 的键到期前也可能先被淘汰出局。其二,删除键本身有成本:同步 DEL 一个百万元素的集合要毫秒到百毫秒级,这也是 UNLINK 存在的理由——把释放内存的活交给后台线程,主线程只摘索引。批量清理脚本的铁律:SCAN 加 UNLINK 加每批小睡,三件套缺一不可。