谈谈 Redis 的内存淘汰策略?

#中间件#面试八股

谈谈 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 与命中率,比反复调策略更能定位容量问题。
笔记加载中…