跳到正文

Redis 内存过期与淘汰 ​

一句话:过期键靠「访问时惰性删 + 后台定期抽样删」清理,内存上限靠淘汰策略兜底;两道清理都不保证及时,所以 maxmemory、淘汰策略和监控必须自己配。

适用版本:Redis 7.x / 8.x。参数默认值以 7.x 的 redis.conf 为准。

为什么需要 ​

Redis 把数据放内存,容量是硬约束。给 key 设了 TTL 不等于到点就释放:如果没人访问,惰性删除不会触发;定期删除又是抽样,不是全量扫描。写入速度长期高于清理速度,内存就会涨到上限。到上限之后必须有明确的动作:拒绝写入,或者按某种策略淘汰。

核心机制 ​

第一道:惰性删除(lazy expire) ​

访问 key 时先检查过期时间,过期就删除并返回空。对 CPU 最友好,但冷数据永远不会被访问,也就永远不会被删。

第二道:定期删除(active expire) ​

主线程默认每秒 10 次(hz=10,约每 100 ms)执行一轮:

  1. 从带 TTL 的 key 中随机抽 20 个;
  2. 删除其中已过期的;
  3. 如果过期比例超过 25%,继续抽;
  4. 达到时间上限就退出,避免阻塞主线程。

active-expire-effort(默认 1,范围 1–10)控制每轮的工作强度:调大后抽样更多、清理更积极,代价是主线程 CPU 升高。官方设计上这一轮最多可占用约 25% 的主线程 CPU;如果监控到 expired_stale_perc 持续偏高、expired_time_cap_reached_count 增长,说明清理跟不上,可以适度调大 effort,但不要把它当成解决内存增长的手段。

主从架构下还有一个窗口:Master 删除过期 key 后向 Slave 发 DEL,如果命令延迟或丢失,Slave 上会短暂保留已过期的 key,形成「幽灵内存」。3.2 之后副本读请求会按过期时间过滤,但内存释放仍依赖主节点。

第三道:内存淘汰(eviction) ​

当 used_memory 达到 maxmemory(默认 0,即不限制)时,按 maxmemory-policy 处理:

策略淘汰范围依据适用
noeviction(默认)不淘汰写命令返回 OOM 错误数据不能丢
allkeys-lru所有 key最近最少使用纯缓存
allkeys-lfu所有 key最不经常使用热点长期稳定
allkeys-random所有 key随机访问均匀
volatile-lru / volatile-lfu仅带 TTL 的 keyLRU / LFU持久数据与缓存混存
volatile-random / volatile-ttl仅带 TTL 的 key随机 / 剩余 TTL 最短特定场景

volatile-* 有一个典型陷阱:如果所有 key 都没设 TTL,就没有可淘汰的对象,行为退化成 noeviction,写命令开始报 OOM。

「近似」LRU / LFU ​

精确 LRU 要维护全量 key 的访问顺序,内存和 CPU 都吃不消。Redis 改为随机采样:maxmemory-samples(默认 5)个 key 中挑最差的一个淘汰。采样越多越接近精确 LRU,CPU 也越高;默认 5 是精度和开销的折中。

  • LRU:redisObject.lru 是 24 位时钟,精度有限。
  • LFU:8 位计数器 + 16 位衰减时间;lfu-log-factor(默认 10)控制计数增长速度,lfu-decay-time(默认 1 分钟)控制衰减速度。
  • 淘汰在主线程执行;删大 key 会阻塞,可开 lazyfree-lazy-eviction yes 交给后台线程。

最小上手 ​

bash
redis-cli

# 看内存水位、策略和两个关键参数
INFO memory
CONFIG GET maxmemory maxmemory-policy maxmemory-samples active-expire-effort

# 纯缓存实例的典型配置
CONFIG SET maxmemory 4gb
CONFIG SET maxmemory-policy allkeys-lru
CONFIG SET maxmemory-samples 10

# 观察是否真的在淘汰
INFO stats | grep -E "evicted_keys|expired_keys"

CONFIG SET 对多数参数即时生效,但生产改 maxmemory 前要确认物理内存和 COW 余量。

生产动作 ​

  • maxmemory 必须显式设置:默认 0 表示不限制,容器里会直接被 OOM Killer 杀。经验上不超过物理内存的 50%,给 fork/COW 留余量。
  • 纯缓存用 allkeys-lru;混合存储用 volatile-lru,并且保证缓存 key 都带 TTL。
  • maxmemory-samples 从 5 起调:调到 10 能改善淘汰准确度,但每次淘汰的 CPU 也上升,先压测再改。
  • active-expire-effort 不要盲目拉满:默认 1;清理压力大时先看 expired_stale_perc,再小幅调到 2–4,观察主线程延迟。
  • 监控四个数:used_memory / maxmemory、evicted_keys、mem_fragmentation_ratio(>1.5 说明碎片明显)、expired_stale_perc。
  • 大 key 删除:用 UNLINK 或开 lazyfree-lazy-*,否则一次 DEL 可能卡住主线程数秒。

边界 ​

  • noeviction 只拒绝写:读请求仍可执行;但内存耗尽时连读所需的内存也可能分配失败。
  • 过期不等于删除:TTL 到点只是「逻辑过期」,物理内存要等惰性/定期删除或淘汰。
  • 淘汰策略会删掉热点:allkeys-lru 在内存不足时可能淘汰正在用的 key,表现为命中率骤降,需要靠容量规划而不是调策略解决。
  • volatile-* 依赖 TTL:没有 TTL 的 key 不会被淘汰,可能一边报 OOM 一边有大量可淘汰数据。
  • active-expire-effort 是 CPU 换及时性:调大只能加快清理,不能突破「写入 > 清理」的长期失衡。

讲清楚 ​

Redis 的过期删除有两层:访问时顺手删,后台每 100 ms 抽 20 个带 TTL 的 key,过期比例高就继续抽。它解决的是「大部分过期 key 能被清掉」,不解决「冷 key 一定被清掉」。所以还需要第三层:达到 maxmemory 后按 LRU/LFU 淘汰,或者直接拒绝写入。

生产上最容易犯的错是只设 TTL、不设 maxmemory,或者用了 volatile-lru 但 key 都没 TTL。真正要盯的是内存水位、淘汰次数和过期堆积比例;调 maxmemory-samples 和 active-expire-effort 只是微调,不能替代容量规划。

把一门技术讲透到既懂也会用