过期删除与内存淘汰
内存是 Redis 最贵的资源。有两套机制在管理它:过期删除处理"到点的 key",内存淘汰处理"内存满了怎么办"。搞不清这两套机制的区别与配置,轻则缓存命中率莫名下降,重则写入报错、实例 OOM。
1. 过期时间:EXPIRE 家族
# 写入时直接带 TTL(推荐)
SET token:abc "data" EX 300
TTL token:abc
# 重设为 600 秒
EXPIRE token:abc 600
TTL token:abc
# 毫秒版
PEXPIRE token:abc 60000
TTL token:abc
# 指定绝对时间戳(秒),这里用 2100-01-01
EXPIREAT token:abc 4102444800
# 移除 TTL,之后 TTL 返回 -1
PERSIST token:abc
TTL token:abcRedis 7 的 EXPIRE 支持条件参数:NX(没有 TTL 才设置)、XX(有 TTL 才设置)、GT/LT(新 TTL 更长/更短才设置):
SET token:abc "data"
# NX:当前没有 TTL,设置成功
EXPIRE token:abc 100 NX
# NX:已经有 TTL 了,这次失败返回 0
EXPIRE token:abc 200 NX
# GT:50 小于 100,条件不满足
EXPIRE token:abc 50 GT
TTL token:abc
# LT:50 小于 100,条件满足
EXPIRE token:abc 50 LT
TTL token:abc注意会清除 TTL 的操作:SET 覆盖同名 key(除非带 KEEPTTL)、GETSET。而 INCR、HSET、LPUSH 等修改值内容的命令不影响 TTL。RENAME 会把 TTL 带到新名字。跑一遍就记住了:
SET counter 1 EX 300
TTL counter
# INCR 只改内容,不影响 TTL
INCR counter
TTL counter
# SET 覆盖会清掉 TTL,变成 -1
SET counter 100
TTL counter
# 带 KEEPTTL 则保留原有 TTL
SET counter 5 EX 300
SET counter 50 KEEPTTL
TTL counter
# RENAME 会把 TTL 带到新名字
RENAME counter counter2
TTL counter22. 过期的 key 什么时候真正被删?
设置了 10 秒过期,第 10 秒 Redis 并不会精确地删掉它。实际是两种策略配合:
2.1 惰性删除(lazy expiration)
访问 key 时先检查是否过期,过期就删掉并返回 nil。零后台成本,但永远不被访问的过期 key 会一直占内存。
2.2 定期删除(active expiration)
后台任务默认每秒跑 10 次(hz 10),每次:
1. 从"设置了 TTL 的 key"里随机抽 20 个
2. 删掉其中已过期的
3. 如果过期比例 > 25%, 回到第 1 步继续抽
(单次循环有时间上限, 防止阻塞主线程过久)这是一个概率算法:不保证及时删除每个过期 key,但保证过期 key 的比例长期维持在较低水平。
key 过期后的三种"死法":
① 被客户端访问 -> 惰性删除
② 被定期任务抽中 -> 定期删除
③ 一直没被碰到 -> 占着内存, 直到 ①/② 发生或被内存淘汰清走大批 key 设置了相同的过期时间(比如凌晨活动结束时 100 万 key 同时到期),定期删除任务会连续多轮抽样都超过 25% 过期比例,长时间占用主线程,表现为周期性延迟尖刺。解决:TTL 加随机抖动,如 EX 3600 + rand(0, 300)。这也是缓存雪崩的预防手段之一(第 18 章)。
从库不主动删除过期 key,而是等主库同步 DEL 命令;读从库时会检查过期时间并返回 nil(3.2+),但 key 仍占用从库内存直到主库的 DEL 到达。
3. maxmemory 与八种淘汰策略
过期机制管不了"没设 TTL 的 key 越写越多"。当内存达到 maxmemory 上限,淘汰策略决定 Redis 的行为:
maxmemory 4gb
maxmemory-policy allkeys-lru| 策略 | 范围 | 规则 |
|---|---|---|
noeviction | - | 不淘汰,写命令直接报错(默认) |
allkeys-lru | 所有 key | 淘汰最久未访问的 |
allkeys-lfu | 所有 key | 淘汰访问频率最低的 |
allkeys-random | 所有 key | 随机淘汰 |
volatile-lru | 仅带 TTL 的 key | 最久未访问 |
volatile-lfu | 仅带 TTL 的 key | 频率最低 |
volatile-random | 仅带 TTL 的 key | 随机 |
volatile-ttl | 仅带 TTL 的 key | 剩余 TTL 最短的先淘汰 |
选型指南:
- 纯缓存(所有数据可从数据库重建):
allkeys-lru,或有明显冷热频率差异时allkeys-lfu; - 缓存与持久数据混部:
volatile-lru,给缓存 key 设 TTL、持久 key 不设,淘汰只发生在缓存上(更好的做法是拆成两个实例); - 不允许丢任何数据:
noeviction+ 容量监控告警,满了报错总好过悄悄丢数据。
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lfu
OK
127.0.0.1:6379> INFO stats
# ...
evicted_keys:12034 # 被淘汰的 key 总数, 持续增长说明内存不够很多人给缓存实例忘了改策略,内存一满,所有写命令返回 OOM command not allowed when used memory is greater than maxmemory,业务直接故障。部署缓存实例的 checklist 里必须有:设 maxmemory + 改淘汰策略。
4. 近似 LRU 与 LFU
4.1 为什么是"近似"LRU
标准 LRU 需要维护一个全局双向链表,每次访问都要移动节点——内存和 CPU 开销都不小。Redis 的做法:
- 每个 key 的对象头里记录 24 bit 的最后访问时间戳(lru 字段);
- 淘汰时随机抽 5 个 key(
maxmemory-samples 5),淘汰其中最久未访问的; - 维护一个淘汰候选池(16 个),跨多次抽样积累"更老"的 key,逼近真实 LRU。
真 LRU: 全局链表精确排序 成本高
近似 LRU: 随机抽 5 个挑最老的 成本 O(1)
samples=10 时效果已非常接近真 LRU4.2 LFU:频率比"最近"更公平
LRU 有个缺陷:一个几小时才被扫一次的冷 key,恰好在淘汰前被访问了一下,就躲过了淘汰;而高频访问的热 key 只因最近 1 分钟没被访问就被淘汰。LFU(Least Frequently Used)按访问频率淘汰,更符合缓存直觉。
Redis 的 LFU 把 24 bit 对象头拆成两半:
+------------------+----------+
| 16bit 分钟级时钟 | 8bit 频率 |
+------------------+----------+
频率计数是对数增长的: 访问时以概率 1/(counter * lfu_log_factor + 1) 才 +1
-> 8bit 就能表示百万级访问量
频率会随时间衰减: 每过 lfu-decay-time 分钟 counter 减 1
-> 曾经的热 key 凉了以后也能被淘汰lfu-log-factor 10 # 越大, counter 增长越慢
lfu-decay-time 1 # 每 1 分钟衰减 1 点用 OBJECT FREQ(LFU 模式下)或 OBJECT IDLETIME(LRU 模式下)可以观察 key 的热度:
# LRU 模式下用 IDLETIME 看"多久没被访问"(秒)
CONFIG SET maxmemory-policy allkeys-lru
SET cold:key "v"
OBJECT IDLETIME cold:key
# 切到 LFU 模式后用 FREQ 看访问频率计数
CONFIG SET maxmemory-policy allkeys-lfu
SET hot:key "v"
GET hot:key
GET hot:key
OBJECT FREQ hot:key小结
- 过期删除 = 惰性删除(访问时查)+ 定期删除(每秒 10 次随机抽样),过期不等于立即删除
- 大批 key 集中过期会造成延迟尖刺,TTL 要加随机抖动
- maxmemory 满了走淘汰策略:缓存场景 allkeys-lru/lfu,默认的 noeviction 会写报错
- 近似 LRU 靠随机抽样 + 候选池;LFU 用对数计数 + 时间衰减,更适合频率差异明显的场景
- 下一章:数据的最后防线——持久化 →
- 给一个 key 设 5 秒 TTL,过期后立即 GET 与 TTL,解释返回值;再验证 SET 覆盖会清掉 TTL 而 INCR 不会。
- 在测试实例上 CONFIG SET maxmemory 10mb 且策略为 noeviction,用脚本灌数据直到写入报错,再改成 allkeys-lru 观察 evicted_keys 增长。
- 你的业务里哪些 key 该设 TTL、哪些不该?如果两类混在一个实例,选哪种淘汰策略?写出你的推理。