Skip to content

Redis 内存满了会怎样:淘汰策略、过期回收与碎片 ​

「内存满了 Redis 会自动淘汰旧数据」——这句话只有在淘汰策略配对的时候才成立。实测把策略设成 volatile-lru 但没有给任何 key 设置过期时间,6 万次写入里有 3.4 万多次直接返回了 OOM command not allowed。

本文在 Redis 8.10.1 上实测了几种典型策略在内存写满时的行为、一次性扫描对 LRU 与 LFU 的影响、过期 key 的实际回收时机,以及大量删除后的内存碎片与主动整理,脚本与原始输出见文末「配套实验」。

一、先说结论 ​

  • 达到 maxmemory 后的行为由 maxmemory-policy 决定,默认是 noeviction:不淘汰任何数据,写入命令直接报错,读命令仍然正常。
  • volatile-* 系列只淘汰设置了过期时间的 key。 没有 key 带 TTL 时,它等价于 noeviction,这是最常见的配置事故。
  • allkeys-lru 和 allkeys-lfu 才是「当缓存用」的正确选择,实测两者都平稳淘汰了约 3.6 万个 key,写入全部成功;遇到一次性的大批写入时,LFU 保住了全部热 key,LRU 只剩 18 个。
  • 过期不等于立即释放内存。 实测 20 万个 key 同时到期,最后一个 key 到期后库里还有 12.3 万个已过期的 key,约 1 秒后才删完。
  • 内存释放给 Redis,不等于还给操作系统。 实测删掉四分之三的数据后 used_memory 从 166MB 降到 46MB,RSS 仍是 196MB;打开主动碎片整理 40 秒后 RSS 降到 107MB。
  • maxmemory 必须显式设置,否则 Redis 会一直申请内存,直到被操作系统的 OOM Killer 杀掉。

二、内存写满时,八种策略的行为 ​

写入请求used_memory ≥ maxmemory ?写入是否还能继续是noeviction直接返回 OOM 错误allkeys-lru / lfu / random从所有 key 中淘汰volatile-lru / lfu / random / ttl只从带 TTL 的 key 中淘汰没有 key 带 TTL → 同样 OOM实测:34,588 次写入失败实测:淘汰约 3.6 万个 key
图 1 · volatile-* 系列只淘汰设置了过期时间的 key;如果没有任何 key 带 TTL,它的行为与 noeviction 一样,写入直接报错
策略淘汰范围选择依据
noeviction(默认)不淘汰写命令返回错误
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剩余存活时间最短

2.1 实测 ​

设置 maxmemory 8mb,然后写入 6 万条 200 字节的数据:

策略写入失败淘汰数量库中剩余之后再写一条
noeviction34,588025,412OOM command not allowed
allkeys-lru035,83024,170OK
allkeys-lfu035,85024,150OK
volatile-lru(无 TTL)34,493025,507OOM command not allowed

第四行是重点:策略写着 volatile-lru,实际行为和 noeviction 一样,因为没有任何 key 可供淘汰。写入报错时,读命令仍然正常返回。

生产上更常见的形式是「缓存 key 大多设置了 TTL,但有一批常驻 key 没设」。实测先写 1.5 万条不带 TTL 的常驻数据,再写 4.5 万条带 TTL 的缓存:写入全部成功,但淘汰的 36,596 个 key 全部来自缓存,常驻数据一条没动,缓存只剩 9,236 条。内存紧张时能淘汰的只有带 TTL 的那部分,缓存命中率会突然塌方。

排查方法:

bash
redis-cli INFO stats  | grep evicted_keys      # 淘汰数量,持续增长说明内存长期吃紧
redis-cli INFO memory | grep maxmemory_policy  # 当前策略
redis-cli DBSIZE                               # 与设置了 TTL 的 key 数量对比

2.2 LRU 还是 LFU ​

  • LRU(最近最少使用):淘汰最久没被访问的。实现上是近似算法,随机采样 maxmemory-samples(默认 5)个 key,淘汰其中最旧的。
  • LFU(访问频率最低):Redis 4.0 引入,用一个带衰减的计数器记录访问频率,淘汰最不常用的。

判断依据是访问模式:偶发的全量扫描(比如一次批量任务遍历了大量冷数据)会把 LRU 的热点挤掉,而 LFU 因为看的是频率,不容易被这种一次性访问带偏。实测 1000 个热 key 各读 30 次,2 秒后一次性写入 6 万个冷 key:allkeys-lru 下热 key 只剩 18 个,allkeys-lfu 下 1000 个全部保留。大多数缓存场景 allkeys-lfu 更稳,但要注意 LFU 的计数器有衰减周期(lfu-decay-time,默认 1 分钟),访问模式变化时的适应速度比 LRU 慢。

三、过期 key 什么时候真正被删除 ​

Redis 的过期删除有两条路径:

  • 惰性删除:访问某个 key 时发现它已过期,立刻删除并返回空。
  • 定期删除:后台每秒若干次,从设置了过期时间的 key 中随机采样一批,删除其中已过期的;如果这批里过期比例很高,就继续多删几轮。

这解释了实测现象。20 万个 key 在 0.52 秒内写完,TTL 都是 3 秒,所以它们在第 3.0—3.52 秒之间陆续到期。在容器内每 0.2 秒采样一次:

text
第 3.00 秒:  keys = 200,000   expired_keys =       0   used_memory = 38.1 MB
第 3.68 秒:  keys = 122,960   expired_keys =  78,362   used_memory = 20.0 MB
第 4.18 秒:  keys =   5,569
第 4.62 秒:  keys =       0   expired_keys = 200,000   used_memory =  1.6 MB

最后一个 key 在第 3.52 秒到期,而第 3.68 秒时库里还有 12.3 万个 key。它们在逻辑上已经过期(查询返回空、不计入命中),但物理上还没被删除,内存也还没释放。同一时刻 expired_stale_perc(采样中已过期 key 的比例)升到 75.6%,定期删除会据此连续多轮清理,约 1 秒后删完。

20 万10 万00s2s4s6s8s10s同时到期的窗口12.3 万个已过期仍在库里TTL 全部为 3 秒TTL 分散在 3—9 秒Redis 8.10.1,容器内每 0.2 秒采样 INFO keyspace
图 2 · 同时到期时,最后一个 key 到期后库里仍有十几万个已过期的 key,约 1 秒后才删完;TTL 分散到 3—9 秒后,删除被摊到约 6 秒里

把 TTL 按序号分散到 3—9 秒,删除就从第 3.7 秒持续到第 9.6 秒,每一刻要清理的量小得多。

工程上的影响:

  • DBSIZE 和监控里的 key 数量会偏高,不能直接当作「有效缓存量」。
  • 大批 key 同时过期会让定期删除集中在一小段时间里,要连续多轮才能清完,对延迟的影响见 Redis 为什么突然变慢。给 TTL 加上随机偏移量(比如 30 分钟 ± 5 分钟)可以把这个压力摊开,也能避免大量 key 同时失效、请求一起回源。
  • 内存回收有延迟,容量规划时要留出余量。

四、内存释放了,为什么 RSS 没降 ​

写入 50 万个 64—448 字节的值,再删掉其中四分之三(每 4 个留 1 个):

阶段used_memoryRSSallocator_frag_bytesmem_fragmentation_ratio
写入后166MB196MB9MB1.18
删掉四分之三后46MB196MB128MB4.27
打开 activedefrag 40 秒后46MB107MB39MB2.34

Redis 使用 jemalloc 分配内存,释放的对象散落在各个内存页里,页内还有存活对象时整页都还不回去,这就是内存碎片。删除数据后 Redis 眼里的内存降了 120MB,操作系统看到的进程占用一点没变。

  • 看碎片要看绝对字节,不能只看比值。 上面 20 万个 key 过期完之后,used_memory 只有 1.58MB、RSS 58.2MB,mem_fragmentation_ratio 高达 37.28;可 allocator_frag_bytes 只有 19.4MB,其余是进程本身和分配器的固定开销。数据集很小时比值会被放大,不说明有问题。
  • 碎片率略大于 1 是正常的,1.0—1.5 之间通常不用管;大量删除或过期、大小不一的对象频繁写删之后,碎片字节会明显增加。
  • 主动碎片整理:activedefrag yes,配合 active-defrag-ignore-bytes(默认 100MB,实验里调成 10MB)、active-defrag-threshold-lower 等阈值,让 Redis 搬移对象、合并空洞。实测碎片字节在 2 秒内从 128MB 降到约 39MB,RSS 第 7 秒才开始下降(jemalloc 归还内存有延迟),40 秒内 Redis 多用了约 4 秒 CPU;碎片率仍高于阈值,整理一直在低强度运行。
  • 碎片率小于 1 说明部分内存被操作系统换出到磁盘(swap),这对 Redis 是灾难性的,应当关闭 swap 或增加内存。

监控里应该同时看 used_memory、used_memory_rss 和 maxmemory——Redis 是按 used_memory 判断是否触发淘汰的,而机器是否 OOM 取决于 RSS。

五、容量与配置的几条实践 ​

  1. 一定要设 maxmemory。 不设时 Redis 会一直申请内存,最终被系统 OOM Killer 杀掉,主从切换和数据丢失一起发生。
  2. maxmemory 留出余量。 复制积压缓冲区、客户端输出缓冲区、AOF 重写和 RDB 持久化时的写时复制都会占用额外内存,通常按物理内存的 60%—70% 设置。
  3. 当缓存用就选 allkeys-lru 或 allkeys-lfu;当存储用(数据不能丢)就选 noeviction,并保证内存充足、有告警。同一个实例不要混用这两种用途。
  4. 所有缓存 key 都设置 TTL,并加随机偏移。没有 TTL 的常驻数据单独放一个实例。
  5. 监控这几个指标:evicted_keys、expired_keys、used_memory / maxmemory 比例、mem_fragmentation_ratio、命中率(keyspace_hits / (hits + misses))。

六、常见误区 ​

  • 「内存满了 Redis 会自动淘汰」:默认策略是 noeviction,写入会直接报错。
  • 「设了 volatile-lru 就安全了」:没有 key 带 TTL 时它等于 noeviction;只有部分 key 带 TTL 时,淘汰压力全部落在这部分上。
  • 「key 过期了内存就释放了」:过期是逻辑上的,物理删除靠惰性和定期采样。
  • 「used_memory 降下来内存就还给系统了」:RSS 可能仍然很高,这是碎片;判断时看 allocator_frag_bytes,不要只看碎片率。
  • 「maxmemory 设成物理内存大小最划算」:持久化和缓冲区还要占内存,会被 OOM Killer 杀掉。

小结 ​

Redis 的内存管理有三层:maxmemory 加淘汰策略决定写满时的行为,过期机制决定失效数据什么时候真正消失,内存分配器决定释放的内存能不能还给操作系统。配置时把这三层分开检查——策略与 TTL 是否匹配、TTL 是否加了随机、maxmemory 是否留了余量——比出问题后再调参有效得多。

数据结构选择对内存的影响,见 Redis 数据结构与编码;Redis 故障时业务怎么兜底,见 多级缓存、预热与热点数据。


配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权