谈谈 Redis 的内存淘汰策略?
结论:当
used_memory超过maxmemory时,Redis 按maxmemory-policy选择"牺牲谁"。八种策略可以拆成三组:不淘汰(noeviction)、按 LRU/LFU 淘汰(allkeys- 与 volatile-)、按随机或剩余 TTL 淘汰**。选型的核心问题是两个:数据能不能丢?过期时间是否可信?前者决定 allkeys 还是 volatile,后者决定 LRU/LFU 还是 TTL。
一、八种策略一览
| 策略 | 候选集合 | 淘汰依据 | 适合场景 |
|---|---|---|---|
| noeviction | 不淘汰 | 写入直接报 OOM 错误 | 数据不可丢、把 Redis 当存储 |
| 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 最短 | 希望优先淘汰快过期的 |
二、LRU 是近似的,LFU 是有代价的
严格 LRU:双向链表,每次访问把节点移到头部 —— 内存开销大(每 key 两个指针)
Redis 近似 LRU:
每个对象头里存 24 bit 的 lru 时钟(秒级精度,约 194 天回绕)
淘汰时随机采样 maxmemory-samples(默认 5)个 key,选出 lru 最小的淘汰
→ 采样越大越接近真 LRU,CPU 开销也越大
LFU(4.0+):
8 bit 计数器 + 8 bit 衰减时间,计数器按概率增长(lfu-log-factor)
长时间不访问会按 lfu-decay-time 衰减,避免"历史热点"永远占位
近似带来的典型现象:一次全表扫描(MGET/KEYS 遍历)之后,真正热的数据可能被冲掉,因为采样只看到"刚被访问过"的冷 key 反而显得新。LFU 通过频率计数缓解这个问题,但计数器只有 8 bit,需要理解其增长曲线。
三、配置与观测
maxmemory 4gb
maxmemory-policy allkeys-lfu
# LRU/LFU 采样精度,默认 5;调到 10 命中率更好但更费 CPU
maxmemory-samples 5
# LFU:计数器增长因子(越大增长越慢)与衰减时间(分钟)
lfu-log-factor 10
lfu-decay-time 1
# 当前策略与内存水位
redis-cli CONFIG GET maxmemory maxmemory-policy
redis-cli INFO memory | grep -E "used_memory_human|maxmemory_human|maxmemory_policy"
# 被淘汰的 key 数量:突增说明容量不足
redis-cli INFO stats | grep evicted_keys
# 观察 key 的空闲时间与访问频率(LRU/LFU 辅助字段)
redis-cli OBJECT IDLETIME mykey # 仅 LRU 策略有意义
redis-cli OBJECT FREQ mykey # 仅 LFU 策略有意义
evicted_keys 是容量规划最重要的指标:持续大于 0 说明一直在"边写边丢",此时加内存或拆分实例才有意义,调策略只是改变"丢谁"。
四、常见追问
- 问:过期策略和淘汰策略是一回事吗?答:不是。过期是"到 TTL 该删",淘汰是"内存满了该挤"。key 可能既没过期也没被访问,但内存满了照样被淘汰。
- 问:为什么默认是 noeviction?答:安全优先,宁可写入报错也不静默丢数据;作为纯缓存使用时应显式改成 allkeys-lru/lfu。
- 问:volatile-* 有什么坑?答:若没有 key 设置 TTL,候选集为空,行为退化成 noeviction,写入直接 OOM;混布时要确保缓存类 key 全部带 TTL。
- 问:LRU 和 LFU 怎么选?答:访问有明显热点、希望"热度"长期生效选 LFU;访问模式均匀、实现简单选 LRU。LFU 对突发的新热点反应慢(计数器需要累积),可用
lfu-log-factor调低加速。 - 问:淘汰会阻塞主线程吗?答:会占用主线程时间,尤其是淘汰大 key;可以通过拆分大 key、控制单实例内存(16 GB 以内)来降低单次淘汰耗时。
- 问:主从架构下淘汰怎么发生?答:从库默认不主动淘汰(
replica-ignore-maxmemory yes),由主库淘汰后把 DEL 传播给从库,避免主从数据不一致。 - 问:
maxmemory应该设成物理内存的多少?答:留出 fork COW 与碎片余量,通常 60%~70%;单实例内存建议不超过 16 GB,便于主从同步与故障恢复。 - 问:为什么淘汰后内存没下降?答:淘汰释放的对象内存未必立刻归还操作系统(jemalloc 保留池),
used_memory_rss会滞后;观察mem_fragmentation_ratio判断是否需要activedefrag。 - 问:缓存命中率突然下降,先查什么?答:先看
evicted_keys与maxmemory-policy是否被改过,再看是否有大批 key 集中过期或被人为删除,最后才怀疑访问模式变化。 - 问:调大
maxmemory-samples有副作用吗?答:每次淘汰都要额外采样与比较,CPU 开销随采样数线性上升;命中率提升通常在 5 与 10 之间就趋于平缓,不必盲目调大。
小结
- 先回答"能不能丢",再回答"按什么丢",八种策略自然收敛到一两个候选。
- allkeys-lru 是通用默认,热点明显用 allkeys-lfu,混布用 volatile-* 且必须保证 TTL 覆盖。
- 监控
evicted_keys与命中率,比反复调策略更能定位容量问题。