Redis 从零到一(04):过期删除与内存淘汰
2026-10-12 09:40:00

适合人群:好奇 EXPIRE 之后 key 什么时候真的消失、内存满了 Redis 会怎么做的同学。
一句话主线:过期删除管”到点的 key”,内存淘汰管”装不下时的整体取舍”,两套机制各管一边。

一、key 过期了,Redis 会立刻删掉它吗?

先看 Redis 怎么”记住”谁过期了:每个库都有一个过期字典(expires dict)——key 指向一个 TTL 时间戳。EXPIRE 命令做的事就是往这个字典里登记,值本身不动。

那到了点,key 会被立刻删除吗?不会立刻删,也做不到立刻删。想象一百万个 key 各带一个定时器、每个都准点触发——光维护定时器就把 Redis 拖死了。

理论上有三种路线:

策略 做法 问题
定时删除 每个 key 挂一个定时器,到点删 CPU 消耗大,不现实
惰性删除 访问时才检查,过期就删 不访问就永远占内存
定期删除 周期性随机抽查清理 频率是权衡的艺术

Redis 的答案是惰性删除 + 定期删除,双管齐下。

图1:惰性删除与定期删除的配合

二、惰性删除:访问时才检查

所有读写命令执行前,都会先查过期字典:key 已过期 → 当场删除,返回 nil。对 CPU 最友好——不访问的 key 一分 CPU 都不花。

1
2
127.0.0.1:6379> GET user:1001
(nil) # 内部其实做了:发现过期 → 删除 → 返回空

漏洞很明显:一个 key 过期后如果再也没人访问,它就成了躺在内存里的”尸体”,白白占着空间。这就要靠第二套机制兜底。

三、定期删除:后台周期抽查

Redis 默认每秒执行 10 次过期清理(由 hz 参数控制,默认 10;可调到 100),每次清理是一个”自适应节奏”的循环:

  1. 从过期字典里随机抽取 20 个 key
  2. 删除其中已经过期的
  3. 如果过期比例超过 25%,说明过期 key 堆积严重——重复第 1 步再来一轮
  4. 比例降下来了(或超过时间上限),本轮结束

两个设计精髓:

  • 随机抽样而不是全量扫描:每次开销恒定很小,不会卡住单线程
  • 25% 阈值自适应:过期 key 越多清得越勤,堆积少了就歇着

即便如此,仍可能漏网之鱼(惰性没触发、抽样也没抽到)。最后一道防线就是:从库不删过期 key(等主库删除命令同步过来),以及本篇主角下半场——内存淘汰。

四、内存满了怎么办:maxmemory 与八种淘汰策略

Redis 是内存数据库,内存不是无限的。设置上限:

1
2
maxmemory 2gb
maxmemory-policy allkeys-lru # 触发淘汰时用什么策略

内存达到 maxmemory 后,按 maxmemory-policy 决定”谁给新数据腾地方”:

策略 候选范围 规则
noeviction —— 不淘汰,内存满了直接报错(写请求 OOM)
volatile-lru 设置了 TTL 的 key 淘汰最久未使用
allkeys-lru 全部 key 淘汰最久未使用(缓存最常用)
volatile-lfu 设置了 TTL 的 key 淘汰使用频率最低(4.0+)
allkeys-lfu 全部 key 淘汰使用频率最低
volatile-random 设置了 TTL 的 key 随机淘汰
allkeys-random 全部 key 随机淘汰
volatile-ttl 设置了 TTL 的 key 淘汰剩余存活时间最短的

记忆窍门拆成三段:noeviction(默认,不作为);范围二选一(volatile- 只动会过期的,allkeys- 全体公平竞争);规则四选一(lru / lfu / random / ttl)。

最常考的坑:用了 volatile-* 系列但库里没有一个 key 设置 TTL → 候选集为空,效果等同 noeviction,写满了照样报错。

选型建议一句话:

  • 纯缓存实例:allkeys-lru 或 allkeys-lfu——数据丢了无所谓,留最热的
  • 混合用途(有不能丢的 key):volatile-lru——只从”可过期”的缓存数据里挤地方,永久 key(锁、配置)不受影响

五、Redis 的 LRU 不是真 LRU

教科书 LRU 需要一个全局双向链表,每次访问都要移动节点——对单线程的 Redis 来说开销不可接受。Redis 用的是近似 LRU:

  1. 淘汰时随机采样 maxmemory-samples 个 key(默认 5 个)
  2. 在样本里挑”最久没被访问的”淘汰
  3. 淘汰后内存还不够,再来一轮

配合一个小的淘汰池(eviction pool):每轮采样的候选者会放进池子排序,让”更老”的 key 有机会跨轮存活下来,提升命中率。采样数调大更精确也更耗 CPU(maxmemory-samples 10 是常见加强档)。

图2:近似 LRU 随机采样与 LFU 计数衰减

六、LFU:4.0 引入的”按频率”淘汰

LRU 有个盲区:一个 key 偶然被访问一次,”最近被访问”的光环就把它保护起来了,哪怕它一共就被访问过那一次。LFU(Least Frequently Used)按访问频率说话:老老实实的热点 key 留下,昙花一现的让路。

Redis 的 LFU 两个精巧设计:

1. Morris 近似计数器:每个 key 只有 8 个 bit 存访问计数,最大只能到 255。访问越多,计数增长的概率越低——越热越难加一,既省空间又体现了”相对频率”。

2. 衰减机制:长时间不访问的 key 计数会周期性衰减(lfu-decay-time,默认每分钟 -1),昨天的热点冷却后自动让位,解决”历史热点赖着不走”。

LFU 的开关藏在编码里:OBJECT FREQ 可以看 key 的频率计数。有明显冷热分层的业务(商品缓存、feed 流)优先 allkeys-lfu,比 LRU 更贴合”热”的定义。

总结

  • 过期 key 靠”惰性删除 + 定期删除(随机抽 20 个、25% 阈值循环)”组合拳,不追求准点,只追求够用
  • 内存满了按 maxmemory-policy 淘汰:缓存用 allkeys-lru/allkeys-lfu,混合场景用 volatile-* 保住永久 key
  • Redis 的 LRU 是随机采样的近似实现;LFU 用 8-bit 概率计数 + 衰减,专治”偶然访问的伪热点”
  • 记住分界线:过期删除是”到点了”(对单个 key 的约定),内存淘汰是”装不下了”(对全局的取舍)

下一篇进入分布式战场:多实例部署后,单机的 synchronized 失效了,分布式锁怎么从一条 SETNX 命令进化到 Redisson 的看门狗。

上一页
2026-10-12 09:40:00
下一页