这篇现在只讲 Redis 的内存侧问题,不再继续混讲持久化。RDBAOF、混合持久化和恢复目标已经拆到下一篇:Redis 持久化:RDB、AOF 与恢复目标(八)

系列导航:

先把“TTL 到了内存没降”拆成四个问题

线上看到 TTL 到了、键也应该失效了,但 used_memory 没有明显下降,通常不是一个点出了问题,而是下面四类现象被混在了一起:

  1. 键已经逻辑过期,但还没有真正从字典里删除。
  2. 键删掉了,但实例正处在 maxmemory 压力和淘汰过程中,整体内存仍在拉扯。
  3. Redis 释放了对象,底层分配器却没有立刻把页归还给操作系统,导致 used_memory_rss 不同步下降。
  4. 少数大 Key 占住了主要空间,让“已经过期的一堆小键”在总量上看起来不明显。

所以判断内存问题时,最好把三个层次分开看:

  • TTL 反映的是逻辑有效期。
  • expired_keys 反映的是过期键是否真的被持续回收。
  • used_memoryused_memory_rssmem_fragmentation_ratio 反映的是对象、分配器和操作系统视角下的内存状态。

TTL 只定义逻辑过期,不承诺立即释放内存

Redis 不会为每个键维护一个“到点立删”的精确计时器。原因很简单:键数量可能极大,如果每个键都做高精度调度,主线程就会把大量时间消耗在清理动作上,而不是处理业务请求。

Redis 回收过期键主要依赖两套机制:

  • 惰性删除:访问到某个键时,先检查它是否过期;过期了就顺手删除。
  • 定期删除:后台按预算抽样扫描带 TTL 的键,清掉其中已过期的一部分。

这套设计意味着两个很常见的线上现象:

  • 冷数据即使已经过期,也可能在内存里停留一段时间,因为没人再访问它。
  • 大批键如果在同一时间窗口内一起到期,expired_keys 会增长,但内存往往是“缓慢回落”,而不是断崖式下跌。

因此,TTL 更像“业务上应视为无效的时间点”,不是“操作系统会在这一秒立刻回收这块内存”的承诺。生产上更稳妥的做法通常是:

  • 给缓存 TTL 加随机抖动,避免同一批热点键一起进入过期窗口。
  • 不把“等 TTL 到点”当成唯一的容量治理手段。
  • INFO stats 中的 expired_keys 变化和业务访问模式一起看,而不是只盯一个 TTL 数字。

maxmemory 和淘汰策略决定“内存满了怎么办”

过期删除解决的是“这个键按时间是否还有效”,淘汰策略解决的是“内存压力已经到来时,Redis 要不要为了新写入驱逐旧键”。这两件事经常被误认为是一回事。

如果实例承担的是纯缓存职责,通常应显式设置 maxmemory,然后再选择符合业务意图的淘汰策略:

  • noeviction:不淘汰,写入超限直接失败。适合不能让 Redis 自己决定丢谁的场景。
  • allkeys-lru:在全部键里近似淘汰最近最少使用的数据。适合大多数纯缓存实例。
  • allkeys-lfu:在全部键里近似淘汰近期最不常用的数据。适合热点差异特别明显的场景。
  • volatile-lruvolatile-lfuvolatile-ttl:只在设置了 TTL 的键中淘汰,适合缓存键和永久状态混放、但又不够理想的过渡阶段。

真正容易出事故的,不是“记不住策略名”,而是一个实例里同时塞了:

  • 不允许丢的在线状态。
  • 可以驱逐的缓存结果。
  • 长时间增长的集合或 Stream。
  • 需要突发返回大结果的业务接口。

这时即使你配置了 allkeys-lru,也只是把“Redis 该不该丢数据”的治理问题推迟到了故障时刻。更可靠的原则通常是:纯缓存实例尽量独立部署;混部场景先拆职责,再谈策略微调。

碎片率高不等于泄漏,但一定要解释得通

很多团队看到 mem_fragmentation_ratio 偏高,就立刻把它理解成内存泄漏。这个结论往往过快了。

碎片率高,通常只说明:Redis 管理的有效对象内存,与进程在操作系统层面实际占住的 RSS 已经出现明显背离。常见原因包括:

  • 大量键在短时间内集中创建和释放,分配模式不均匀。
  • 一批对象已经删除,但底层分配器暂时保留了页,尚未归还给操作系统。
  • 大 Key 频繁变大、变小或重写,导致内存布局更容易碎裂。

排查时可以用下面这组视角:

INFO memory
  used_memory
  used_memory_rss
  mem_fragmentation_ratio

如果 used_memory 已经回落,但 used_memory_rss 长时间居高不下,问题更可能出在分配器层面的页保留,而不是 Redis 仍然持有大量业务对象。此时要先回答“碎片为什么出现”,再决定是否需要重启、迁移流量或进一步做 jemalloc 级别调优。

大 Key 会把所有内存问题一起放大

Redis 里的大 Key 不只是“某个键很占空间”,更危险的是它会同时放大多个维度的成本:

  • 删除成本变高,单次清理更容易拖长主线程。
  • 网络传输变重,客户端和副本都要承受更大的数据包。
  • 后台回收、扫描和采样更容易出现误判,因为少数键就能占掉主体空间。
  • 业务会误以为“键数量不多,为什么内存这么高”,从而忽略真正的大对象分布。

最常见的大 Key 形态通常是:

  • 超大 JSON String
  • 字段数量长期无边界增长的 Hash
  • 不做裁剪的 SetSorted SetStream

生产上更有用的动作,不是等报警后再跑一次全量扫描,而是把“大 Key 采样”变成常规动作,例如用 MEMORY USAGE key 结合业务前缀做周期性抽样。你要知道空间到底被哪些对象占住,才能判断 TTL、淘汰和碎片问题分别占多大权重。

一套更实用的内存排查顺序

如果线上已经出现“内存不降”“实例抖动”“驱逐异常”这类信号,建议按下面顺序判断:

  1. 先看实例职责是不是纯缓存,是否真的配置了 maxmemory
  2. 再看 expired_keys 是否持续增长,确认 TTL 回收链路是否正常工作。
  3. 对照 used_memoryused_memory_rss,判断是对象还在,还是碎片和页保留导致的假象。
  4. 抽样找大 Key,确认是不是少数对象撑住主体空间。
  5. 最后再回到淘汰策略,确认“被驱逐的对象”是不是你本来就愿意丢的那类数据。

把这五步固定下来以后,很多“Redis 为什么不释放内存”的问题都会变得可解释。最怕的是一上来只盯 TTL,最后把过期、淘汰、碎片和大 Key 四类问题混成一个模糊结论。

本篇解决了什么:

  • 解释了为什么 TTL 到了以后,内存不一定会立刻下降
  • 区分了过期删除、淘汰策略、碎片率和大 Key 这四类常见内存问题
  • 给出了一条更适合线上排查的内存治理顺序,而不是只盯单个指标

下一篇:Redis 持久化:RDB、AOF 与恢复目标(八)