适合人群:好奇
EXPIRE之后 key 什么时候真的消失、内存满了 Redis 会怎么做的同学。
一句话主线:过期删除管”到点的 key”,内存淘汰管”装不下时的整体取舍”,两套机制各管一边。
一、key 过期了,Redis 会立刻删掉它吗?
先看 Redis 怎么”记住”谁过期了:每个库都有一个过期字典(expires dict)——key 指向一个 TTL 时间戳。EXPIRE 命令做的事就是往这个字典里登记,值本身不动。
那到了点,key 会被立刻删除吗?不会立刻删,也做不到立刻删。想象一百万个 key 各带一个定时器、每个都准点触发——光维护定时器就把 Redis 拖死了。
理论上有三种路线:
| 策略 | 做法 | 问题 |
|---|---|---|
| 定时删除 | 每个 key 挂一个定时器,到点删 | CPU 消耗大,不现实 |
| 惰性删除 | 访问时才检查,过期就删 | 不访问就永远占内存 |
| 定期删除 | 周期性随机抽查清理 | 频率是权衡的艺术 |
Redis 的答案是惰性删除 + 定期删除,双管齐下。
二、惰性删除:访问时才检查
所有读写命令执行前,都会先查过期字典:key 已过期 → 当场删除,返回 nil。对 CPU 最友好——不访问的 key 一分 CPU 都不花。
1 | 127.0.0.1:6379> GET user:1001 |
漏洞很明显:一个 key 过期后如果再也没人访问,它就成了躺在内存里的”尸体”,白白占着空间。这就要靠第二套机制兜底。
三、定期删除:后台周期抽查
Redis 默认每秒执行 10 次过期清理(由 hz 参数控制,默认 10;可调到 100),每次清理是一个”自适应节奏”的循环:
- 从过期字典里随机抽取 20 个 key
- 删除其中已经过期的
- 如果过期比例超过 25%,说明过期 key 堆积严重——重复第 1 步再来一轮
- 比例降下来了(或超过时间上限),本轮结束
两个设计精髓:
- 随机抽样而不是全量扫描:每次开销恒定很小,不会卡住单线程
- 25% 阈值自适应:过期 key 越多清得越勤,堆积少了就歇着
即便如此,仍可能漏网之鱼(惰性没触发、抽样也没抽到)。最后一道防线就是:从库不删过期 key(等主库删除命令同步过来),以及本篇主角下半场——内存淘汰。
四、内存满了怎么办:maxmemory 与八种淘汰策略
Redis 是内存数据库,内存不是无限的。设置上限:
1 | maxmemory 2gb |
内存达到 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:
- 淘汰时随机采样
maxmemory-samples个 key(默认 5 个) - 在样本里挑”最久没被访问的”淘汰
- 淘汰后内存还不够,再来一轮
配合一个小的淘汰池(eviction pool):每轮采样的候选者会放进池子排序,让”更老”的 key 有机会跨轮存活下来,提升命中率。采样数调大更精确也更耗 CPU(maxmemory-samples 10 是常见加强档)。
六、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 的看门狗。