主题
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)执行一轮:
- 从带 TTL 的 key 中随机抽 20 个;
- 删除其中已过期的;
- 如果过期比例超过 25%,继续抽;
- 达到时间上限就退出,避免阻塞主线程。
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 的 key | LRU / 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只是微调,不能替代容量规划。