内存淘汰策略与过期删除
Redis 的内存不是无限的。当写入量超过 maxmemory,或者大量 key 到期却迟迟不释放,表现就是内存告警、写入报错、延迟升高。本文区分两件常被混淆的事:过期键怎么删和内存满了淘汰谁,并给出 Redis 7.x 下的配置与排查方法。
一、过期键的两种删除方式
EXPIRE 并不立即删除 key,只是在 redisDb 的 expires 字典里记一个毫秒时间戳。真正的删除由两条路径完成。
惰性删除:每次访问 key 前先检查是否过期,过期则删除并返回空。实现简单、不浪费 CPU,但如果某个 key 过期后再也没人访问,内存就一直被占着。
定期删除:serverCron 默认每 100 ms 执行一次 activeExpireCycle,从 expires 字典中随机抽取 20 个 key,删除其中已过期的;如果过期比例超过 25%,就继续抽,直到比例降下来或达到时间上限(默认不超过 CPU 时间的 25%)。
两者结合仍有漏网之鱼:既没被抽到、也没被访问的 key 会一直残留。因此从库不会主动删除过期键,只等主库发来 DEL;主库删除时生成的 DEL/UNLINK 命令会同步到从库。这也解释了一个现象:从库上 DBSIZE 可能比主库大,读到已过期 key 时返回的是过期数据(Redis 3.2 起从库读会返回空值,但 key 本身还在)。
二、8 种内存淘汰策略
maxmemory-policy 有 8 个取值,分三组:
| 策略 | 淘汰范围 | 算法 | 适用场景 |
| --- | --- | --- | --- |
| noeviction | 不淘汰 | 写入直接报错 | 数据不可丢,必须告警扩容 |
| allkeys-lru | 所有 key | 近似 LRU | 通用缓存,最常用 |
| allkeys-lfu | 所有 key | 近似 LFU | 有明显热点、长尾访问 |
| allkeys-random | 所有 key | 随机 | 访问分布均匀,几乎不用 |
| volatile-lru | 设了 TTL 的 key | 近似 LRU | 缓存与持久数据混存 |
| volatile-lfu | 设了 TTL 的 key | 近似 LFU | 同上,且热点明显 |
| volatile-random | 设了 TTL 的 key | 随机 | 同上,访问均匀 |
| volatile-ttl | 设了 TTL 的 key | 优先淘汰剩余 TTL 最短的 | 希望尽快释放短期数据 |
注意:
volatile-*策略在没有任何 key 设置 TTL 时,行为等价于noeviction,写入会报OOM command not allowed when used memory > 'maxmemory'。
三、近似 LRU:采样而不是链表
标准 LRU 需要维护一条双向链表,每次访问都要移动节点,内存和 CPU 开销都大。Redis 改用采样法:redisObject.lru 字段记录最后一次访问的时钟(秒级),淘汰时随机抽 maxmemory-samples 个 key(默认 5),挑 lru 最小的那个删除。
采样数越大越接近真实 LRU,但 CPU 开销也越大。官方压测显示 5 个样本已能接近理论 LRU 的效果,10 个样本几乎无差别。可以通过 CONFIG SET maxmemory-samples 10 微调。
这种方法的问题是对"曾经热、现在冷"的数据不友好:一个 key 只要在采样中被看到一次较新的访问时间,就能躲过淘汰,形成缓存污染。
四、LFU:用计数器对抗缓存污染
allkeys-lfu / volatile-lfu 复用 lru 字段的 24 位:高 16 位存访问时间(分钟级),低 8 位存访问频率(0~255 的对数计数器)。
频率不是简单加一,而是概率递增:计数越大,加一概率越低,公式近似 p = 1 / ((counter - 5) * lfu_log_factor + 1)。lfu-log-factor 默认 10,值越大增长越慢、能区分的访问量范围越大。
还有衰减机制:lfu-decay-time 默认 60 分钟,表示每过这么久计数减一,防止历史热点永远占着位置。
maxmemory-policy allkeys-lfu
lfu-log-factor 10
lfu-decay-time 60
maxmemory-samples 10
LFU 适合"少数 key 承担绝大部分流量"的场景,比如商品详情页;如果访问分布比较均匀,LRU 反而更简单可控。
五、maxmemory 该怎么设
maxmemory 4gb
maxmemory-policy allkeys-lru
关键点:
maxmemory限制的是used_memory,不含 fork 产生的 COW 内存、复制缓冲区、AOF 缓冲区。所以实际占用会高于这个值。- 单机部署时,
maxmemory建议设为物理内存的 50%~70%,给 fork、碎片和其他进程留余量。 - 容器里要显式设置
maxmemory,不要依赖 cgroup 自动识别,否则可能被 OOM Killer 杀掉。 used_memory_rss与used_memory的比值超过 1.5 时说明碎片严重,可开启activedefrag yes。
127.0.0.1:6379> INFO memory
used_memory:4294967296
used_memory_human:4.00G
used_memory_rss:5368709120
used_memory_rss_human:5.00G
mem_fragmentation_ratio:1.25
maxmemory:4294967296
maxmemory_policy:allkeys-lru
evicted_keys:12034
expired_keys:88231
evicted_keys 持续增长说明容量不足或策略不当;expired_keys 增长慢但内存不降,说明有大量过期 key 没被及时清理,需要检查是否集中写入、TTL 是否合理。
六、持久化与复制中的过期键
过期键在不同角色下的处理规则并不一致,这点在排查"从库内存不降"时很关键。
RDB 生成时,主节点会跳过已过期的 key,快照里不包含它们;从节点则不做过滤,因为从节点没有删除权,必须等主节点同步 DEL。
RDB 加载时,如果当前实例是主节点,加载过程会丢弃已过期的 key;如果是从节点,则全部载入,等主节点通知。所以一次全量同步后,从库的 used_memory 可能暂时高于主库。
AOF 重写时,主节点同样会过滤已过期的 key,重写后的文件更小;AOF 正常追加过程中,过期 key 会以显式的 DEL/UNLINK 命令写入,保证重放结果一致。
主从复制时,从节点收到 DEL 才删除,自身不主动清理。如果主节点因为淘汰或过期删除了 key,这个删除动作会作为命令传播下去。反过来说,从节点上 expired_keys 指标长期为 0 是正常的,不要据此判断过期机制失效。
七、主动清理过期键
如果业务会在短时间内写入大量带 TTL 的 key,比如整点刷新一批报表缓存,可以主动扫描清理,避免集中过期造成的内存尖峰:
# 扫描并删除已过期的 key(示例:前缀 report:)
redis-cli --scan --pattern 'report:*' | while read -r key; do
ttl=$(redis-cli TTL "$key")
if [ "$ttl" -eq -2 ] || [ "$ttl" -eq -1 ]; then
redis-cli DEL "$key"
fi
done
更稳的做法是用 UNLINK 代替 DEL,把内存释放交给后台线程,避免删除大 key 时阻塞主线程。
扫描本身也有代价。--scan 的 COUNT 默认是 10,遍历百万级 key 会产生大量往返,建议加 --count 1000,并在低峰期执行。如果只是为了清理某类临时数据,更好的做法是从写入侧控制:统一 TTL、控制批量写入的速率,而不是事后扫描补救。
常见坑 / 最佳实践
- 别用
volatile-lru却只给部分 key 设 TTL。 没设 TTL 的数据永远不会被淘汰,内存满了照样报 OOM,且报错信息容易误导排查方向。 - 过期时间要加随机抖动。 大量 key 在同一秒过期会触发集中删除,主线程出现周期性延迟尖峰,TTL 加
random(0, 300)秒可以打散。 - 不要依赖 TTL 做精确的业务时效控制。 过期删除有延迟,最长可能几秒;需要强时效的场景应在读取时校验业务时间字段。
- 淘汰策略切换不会立即释放内存。 只影响后续淘汰行为,内存回收是渐进的,观察
evicted_keys而不是立刻看used_memory。 - 大 key 用
UNLINK。DEL一个百万元素的 Hash 会阻塞主线程数百毫秒,UNLINK先摘链再异步释放。
小结
- 过期删除 = 惰性删除 + 定期删除,两者都不保证及时,残留 key 依赖淘汰策略兜底。
- 8 种淘汰策略的核心区别是「淘汰范围」(allkeys / volatile)和「算法」(LRU / LFU / TTL / random)。
- 近似 LRU 靠随机采样,成本低但怕缓存污染;LFU 用对数计数器和衰减,更适合有明显热点的场景。
maxmemory要按物理内存的 50%~70% 设置,并关注mem_fragmentation_ratio与evicted_keys。- 集中过期的业务应加 TTL 抖动,并用
UNLINK或主动扫描清理大 key。
- 点赞
- 收藏
- 关注作者
评论(0)