Redis 是内存数据库,内存是它最宝贵也最稀缺的资源。当你给 key 设了过期时间,它什么时候真正被删除?当内存写满了,Redis 又如何决定"牺牲"谁?这两个问题分别对应过期策略和内存淘汰算法。它们是两套独立但常被混淆的机制,搞清楚它们才能避免线上"内存莫名涨满"或"数据莫名消失"的事故。
过期策略:key 到期后怎么删
给 key 设过期时间很简单(EXPIRE key seconds),但"到期"不等于"立即被删除"。删除一个过期 key 有三种思路。
惰性删除(Lazy Expiration)。 key 过期后并不主动删,而是等下次有人访问它时才检查:如果发现已过期,就删掉并返回 nil。
- 优点:CPU 友好,只在访问时才花一次检查的代价。
- 缺点:内存不友好。如果一个过期 key 再也没人访问,它会永远占着内存。
定时删除(Active Expiration)。 给每个 key 配一个定时器,到点就删。
- 优点:内存最友好,过期即清理。
- 缺点:CPU 不友好。海量 key 意味着海量定时器,删除操作会和读写请求抢 CPU,在高负载时拖慢主线程。Redis 并没有采用这种纯定时方案。
Redis 的实际策略:惰性删除 + 定期删除(Periodic)。 Redis 折中地把两者结合:
- 惰性删除兜底:访问 key 时顺手清理过期的。
- 定期删除主动扫描:Redis 后台周期性运行,从设置了过期时间的 key 集合(
expires字典)里随机抽样一批检查,删除其中过期的。
定期删除的关键在于"随机抽样"而非全量扫描。它的大致逻辑是:每次抽取一定数量的 key,如果这批里过期比例超过某个阈值(比如 25%),就再抽一批,直到过期比例降下来或达到时间上限。这样既能控制单次执行时间(不阻塞主线程太久),又能保证过期 key 不会无限堆积。
1 | 定期删除循环(简化): |
踩坑提醒: 因为是随机抽样,理论上总有"漏网之鱼"的过期 key 没被定期删除扫到,又恰好没人访问。这些 key 会持续占内存,直到触发内存淘汰。所以仅靠过期策略并不能保证内存不被过期数据占满。
内存淘汰:内存写满了删谁
当 Redis 内存用量达到 maxmemory 上限,且有新的写入请求时,就会触发内存淘汰(eviction)。注意这和过期是两码事:被淘汰的 key 不一定设置了过期时间,可能是正常的热数据。
通过 maxmemory-policy 配置淘汰策略,主要有以下几类:
1 | # 配置内存上限和淘汰策略 |
按"作用范围"和"算法"两个维度组合:
- noeviction:不淘汰,内存满后写操作直接报错(读和删除仍可执行)。默认策略,适合不能丢数据的场景。
- volatile-lru / allkeys-lru:按 LRU(最近最少使用)淘汰。
volatile-只在设了过期时间的 key 里挑,allkeys-在全部 key 里挑。 - volatile-lfu / allkeys-lfu:按 LFU(最不经常使用)淘汰。
- volatile-random / allkeys-random:随机淘汰。
- volatile-ttl:在设了过期时间的 key 里,优先淘汰剩余 TTL 最短的。
LRU 与 LFU 的算法权衡
LRU(Least Recently Used) 淘汰"最久没被访问"的 key。直觉上很合理——最近没用的将来大概也不会用。
但精确 LRU 需要维护一个按访问时间排序的链表,每次访问都要把节点移到表头,这在百万级 key 下开销不小。Redis 用的是近似 LRU:每个对象头里存一个 24 位的时间戳(lru 字段),记录最后访问时间。淘汰时随机采样若干 key(采样数由 maxmemory-samples 控制,默认 5),从样本里挑时间戳最旧的删。采样数越大越接近精确 LRU,但 CPU 开销也越高,这是典型的精度与性能权衡。
LFU(Least Frequently Used) 淘汰"访问频率最低"的 key,解决 LRU 的一个痛点:某个 key 被偶然访问一次就"续命",但其实它是冷数据。LFU 关注的是访问次数而非时间。
Redis 的 LFU 实现很精巧。它不可能给每个 key 存一个真实的访问计数(占内存且会无限增长),而是用 24 位字段拆成两部分:高 16 位是上次访问时间(分钟级),低 8 位是一个对数计数器(logc)。这个计数器有两个特点:
- 概率式递增:访问次数越多,计数器越难再涨(用概率控制),让 8 位就能表示很大的访问量级,避免计数溢出。
- 随时间衰减:根据距上次访问的时间,计数器会逐渐降低,这样历史上的热点不会永远赖着不走。
衰减速度和递增难度分别由 lfu-decay-time 和 lfu-log-factor 调节。
工程实践与误区
- 过期策略 ≠ 淘汰策略。 前者管"设了 TTL 的 key 何时删",后者管"内存满了删谁"。一个 key 可能因 TTL 到期被删,也可能在没过期时因内存压力被淘汰。
- noeviction 下的坑。 用 Redis 做缓存却配了 noeviction,内存满后所有写入报错,业务直接雪崩。缓存场景通常应选
allkeys-lru或allkeys-lfu。 - 大 key 拖累惰性删除。 删除一个巨大的集合或 hash 会阻塞主线程。Redis 后来引入了惰性释放(
lazyfree,异步删除大对象),生产环境处理大 key 时应开启。 - 采样数的取舍。
maxmemory-samples调大能让 LRU/LFU 更准,但每次淘汰都要多扫几个 key,高写入压力下会增加 CPU。默认值 5 是大多数场景的平衡点。 - 从 LRU 迁到 LFU。 如果你的访问模式里存在"扫描型"流量(一次性遍历大量 key),LRU 会被污染——这些只访问一次的 key 把真正的热点挤出去了。这种场景 LFU 明显更优。
小结
Redis 用"惰性 + 定期"的过期策略,在 CPU 和内存之间取平衡;用近似 LRU/LFU 的内存淘汰,在精度和性能之间取平衡。两套机制的设计哲学是一致的:绝不为了完美而阻塞主线程,宁可用采样、对数计数、概率递增这些"不精确但够用"的手段,守住 Redis 单线程模型下的低延迟。理解这层取舍,才能在配置 maxmemory-policy 时做出正确选择。