内存 90% 告警,凶手是一堆「过期」的 key
凌晨收到内存水位 90% 的告警,我登上去一查,气笑了:几百万个会话 key,TTL 明明只有 2 小时,却堆了整整半天没释放。key 的到期时间早就过了,为什么不删?——如果你也有这个疑问,说明你已经摸到了 Redis 内存管理的第一层真相:到期和删除,是两回事。
Redis 的主流程是单线程,如果每次 key 过期都立刻停下来全量清理,光是扫描就要把主线程卡成幻灯片。所以 Redis 的策略是:删除这件事,能拖就拖,能省则省——然后把内存的压力交给另一套机制兜底。这篇文章就把「过期删除」和「内存淘汰」这对经常被混淆的机制一次讲清。
到期 ≠ 立刻删除:两套拖延策略
Redis 删除过期 key 靠两条腿走路:
| 策略 | 执行时机 | 优点 | 代价 |
|---|---|---|---|
| 定期删除 | 默认每秒 10 次,随机抽一批带 TTL 的 key 检查 | 主动回收,过期 key 不至于无限堆积 | 占用 CPU,抽查模式不保证删干净 |
| 惰性删除 | key 被访问时才检查是否过期 | CPU 零开销,按需付费 | 没人访问的过期 key 会常驻内存 |
定期删除是「抽样 + 循环」:每次随机抽 20 个带 TTL 的 key,过期的删掉;如果这批里过期比例超过四分之一,说明过期 key 太多,再抽一轮。听起来挺勤快,但它永远只抽样、不扫描全库——这就是开头那批「过期不删」的会话 key 幸存的第一个原因。惰性删除补第二刀:只有 key 被访问到,才会顺手发现它过期并删除。两套机制叠加的结果是:既没人抽查到、又没人访问的过期 key,就一直躺在内存里。这是拿「内存短暂浪费」换「CPU 不过载」的刻意权衡。
躺着不怕,怕的是越躺越多。当过期 key 堆积把内存顶到上限,第三套机制才正式登场——内存淘汰。
内存满了杀谁:八种淘汰策略
先纠正一个默认值陷阱:maxmemory 默认为 0,也就是不限内存——Redis 会一路吃到操作系统把它 OOM 杀掉为止。生产环境必须显式设置上限和淘汰策略:
# 给缓存实例设上限,并选好淘汰策略
maxmemory 6gb
maxmemory-policy allkeys-lru八种策略按「淘汰谁、怎么挑」分成三组:
| 策略 | 淘汰范围 | 挑选规则 |
|---|---|---|
| noeviction(默认) | 不淘汰 | 内存满后写命令直接报错,读不受影响 |
| volatile-lru | 设了 TTL 的 key | 淘汰最近最少使用 |
| volatile-lfu | 设了 TTL 的 key | 淘汰最不经常使用 |
| volatile-ttl | 设了 TTL 的 key | 优先淘汰剩余存活时间最短的 |
| volatile-random | 设了 TTL 的 key | 随机淘汰 |
| allkeys-lru | 全部 key | 淘汰最近最少使用 |
| allkeys-lfu | 全部 key | 淘汰最不经常使用 |
| allkeys-random | 全部 key | 随机淘汰 |
两个容易答错的面试题顺便清了:第一,LRU 是「最久没被用过」,LFU 是「用得最少」,前者看时间、后者看频率,访问有明显冷热长尾的数据用 LFU 更准。第二,Redis 的 LRU 不是教科书上那条双向链表,而是随机采样近似实现——每次随机摸几个 key 比一比,省下维护全量链表的内存。
策略怎么选:看实例里住着谁
- 纯缓存实例:allkeys-lru 是标准答案;数据冷热差异大的场景换成 allkeys-lfu。
- 混着不能丢的 key:比如分布式锁、全局配置——用 volatile 系。这类 key 不设 TTL,就永远不会被淘汰,牺牲的只有会过期的缓存。
- 一个真实的坑:曾有团队把分布式锁存进 allkeys-lru 的实例,内存一紧,锁被「杀」了,互斥性直接失效。第 8 篇说过「锁是防线不是保命符」,这里补上后半句:锁要么单独实例,要么落在不会被淘汰的策略里。
删除也分轻重:UNLINK 与 lazy-free
淘汰和过期都讲完了,还有最后一个坑:删除本身也会卡。DEL 一个百万元素的 hash,主线程要亲自把内存一块块释放,大 key 能卡住主线程几百毫秒。解法是 4.0 引入的异步删除:UNLINK 只把 key 从键空间摘除,真正的内存释放交给后台线程。Java 里对应 redisTemplate.unlink(key),用法和 DEL 一致。
顺手把过期和淘汰也一并异步化:
# redis.conf:过期、淘汰、隐式删除都交给后台线程
lazyfree-lazy-expire yes
lazyfree-lazy-eviction yes
lazyfree-lazy-server-del yes再把日常编码的规矩立起来:
- 所有写入显式带 TTL:拒绝裸 key,这既是第 3 篇讲过的容量纪律,也是让 volatile 系策略有活可干的前提。
- maxmemory 必设:上限留出 20~30% 余量给复制缓冲区和碎片,别指望操作系统的 OOM 杀手手下留情。
- 盯住 evicted_keys:这个指标开始增长,说明淘汰策略已经动真格在杀 key,缓存命中率和业务一致性都可能受波及。
总结一下这套内存管理体系:过期 key 靠定期加惰性「慢慢删」,内存满了靠 maxmemory 加淘汰策略「挑着杀」,删除大对象靠 UNLINK 和 lazy-free「异步删」。三层机制各管一段,谁也别替代谁。下一篇聊聊 Redis 的持久化:内存数据怎么落到磁盘?RDB 快照和 AOF 日志怎么选?重启之后的数据又该怎么救?下篇见。
咖啡凉了,记得趁热喝。
评论 (0)