过期与内存淘汰
两个容易混淆的概念:过期删除处理「设了 TTL 且已到期」的键;内存淘汰处理「内存用满了,不管有没有 TTL 都得腾空间」。
过期键怎么删
Redis 把带 TTL 的键单独放在一个过期字典里(key → 过期时间戳)。删除策略是两种的组合:
1. 惰性删除(被动) —— 访问某个 key 时才检查是否过期,过期了就删并返回 nil。
- 优点:对 CPU 友好,不做无用检查
- 缺点:过期了但再也没人访问的 key 会一直占内存
2. 定期删除(主动) —— 默认每秒 10 次(hz 10),每次从过期字典里随机抽取 20 个 key 检查,删掉过期的;如果这批里过期比例超过 25%,立刻再抽一轮,否则结束。单次循环有 25ms 的时间上限,防止阻塞。
- 这是抽样而非全量扫描,所以不保证及时,只保证过期 key 的比例收敛在 25% 以下
为什么不用「定时删除」(每个 key 设个定时器到点就删)?因为需要大量定时器,CPU 开销与内存开销都不划算。
:::info 主从环境下的过期
从库不会自己删过期 key。从库只在主库发来 DEL 命令时才删,这是为了保证主从一致。但从库读取时会判断逻辑过期,对客户端返回 nil —— 所以不会读到脏数据,只是内存里还留着。
:::
八种内存淘汰策略
当内存达到 maxmemory 时触发。按「作用范围 × 淘汰依据」组合:
| 策略 | 范围 | 依据 |
|---|---|---|
noeviction(默认) | — | 不淘汰,写入直接报错(读正常) |
volatile-lru | 仅设了 TTL 的 key | 最久未使用 |
volatile-lfu | 仅设了 TTL 的 key | 使用频率最低(4.0+) |
volatile-random | 仅设了 TTL 的 key | 随机 |
volatile-ttl | 仅设了 TTL 的 key | 剩余 TTL 最短 |
allkeys-lru | 全部 key | 最久未使用 |
allkeys-lfu | 全部 key | 使用频率最低(4.0+) |
allkeys-random | 全部 key | 随机 |
怎么选:
- 纯缓存场景 →
allkeys-lru(最常用) - 有明显热点、访问频率差异大 →
allkeys-lfu - 缓存与持久数据混存(部分 key 不能丢)→
volatile-lru,只淘汰带 TTL 的 noeviction适合「Redis 当存储用、宁可写失败也不丢数据」的场景,但必须配监控告警
LRU 和 LFU 的近似实现
Redis 的 LRU 不是标准 LRU。标准 LRU 需要维护一个全局链表,每次访问都要移动节点,内存和 CPU 开销都大。Redis 的做法:
- 每个对象头里存一个 24 位的
lru时钟(秒级精度) - 淘汰时随机采样
maxmemory-samples个 key(默认 5),淘汰其中最旧的 - 3.0 起引入候选池:维护一个 16 个 key 的池子,每次采样的结果和池子里的比较,保留更旧的,提升了近似精度
采样数调大更接近真实 LRU,但 CPU 开销上升。默认 5 已经足够接近。
LFU 的实现更精巧,解决 LRU 的一个缺陷:「某个 key 很久以前被疯狂访问过,之后再没人用」在 LRU 下可能长期不被淘汰(如果刚好被扫到过),而「刚被访问一次的冷 key」反而排在前面。
LFU 把 24 位拆成两部分:
- 16 位
ldt(last decrement time)—— 上次衰减的分钟级时间戳 - 8 位
logc(logistic counter)—— 访问频率计数,用对数方式递增
对数递增意味着计数越大、再增加的概率越低,8 位就能表示很大的访问量级(由 lfu-log-factor 控制)。同时计数会随时间衰减(由 lfu-decay-time 控制,默认每分钟减 1),这样历史热点会逐渐冷却。
这个「对数递增 + 时间衰减」的设计是 LFU 题的得分点。只答「LFU 按频率淘汰」是不够的,面试官想听的是怎么用 8 位存下频率和怎么避免历史热点永不淘汰。
相关的坑
不要给所有 key 设相同的 TTL —— 会导致同一时刻大批 key 集中过期,引发缓存雪崩。TTL 上加随机抖动。
EXPIRE 会被某些命令清除 —— 对 key 执行 SET(不带 KEEPTTL)会清掉 TTL 变成永久 key。INCR、LPUSH、HSET 等修改值的命令不影响 TTL。这个差异是线上内存泄漏的常见原因。
PERSIST 可以主动移除 TTL;TTL key 返回 -1 表示无过期时间,-2 表示 key 不存在。