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 杀掉。
二、内存写满时,八种策略的行为
| 策略 | 淘汰范围 | 选择依据 |
|---|---|---|
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 字节的数据:
| 策略 | 写入失败 | 淘汰数量 | 库中剩余 | 之后再写一条 |
|---|---|---|---|---|
noeviction | 34,588 | 0 | 25,412 | OOM command not allowed |
allkeys-lru | 0 | 35,830 | 24,170 | OK |
allkeys-lfu | 0 | 35,850 | 24,150 | OK |
volatile-lru(无 TTL) | 34,493 | 0 | 25,507 | OOM command not allowed |
第四行是重点:策略写着 volatile-lru,实际行为和 noeviction 一样,因为没有任何 key 可供淘汰。写入报错时,读命令仍然正常返回。
生产上更常见的形式是「缓存 key 大多设置了 TTL,但有一批常驻 key 没设」。实测先写 1.5 万条不带 TTL 的常驻数据,再写 4.5 万条带 TTL 的缓存:写入全部成功,但淘汰的 36,596 个 key 全部来自缓存,常驻数据一条没动,缓存只剩 9,236 条。内存紧张时能淘汰的只有带 TTL 的那部分,缓存命中率会突然塌方。
排查方法:
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 秒采样一次:
第 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 秒后删完。
把 TTL 按序号分散到 3—9 秒,删除就从第 3.7 秒持续到第 9.6 秒,每一刻要清理的量小得多。
工程上的影响:
DBSIZE和监控里的 key 数量会偏高,不能直接当作「有效缓存量」。- 大批 key 同时过期会让定期删除集中在一小段时间里,要连续多轮才能清完,对延迟的影响见 Redis 为什么突然变慢。给 TTL 加上随机偏移量(比如
30 分钟 ± 5 分钟)可以把这个压力摊开,也能避免大量 key 同时失效、请求一起回源。 - 内存回收有延迟,容量规划时要留出余量。
四、内存释放了,为什么 RSS 没降
写入 50 万个 64—448 字节的值,再删掉其中四分之三(每 4 个留 1 个):
| 阶段 | used_memory | RSS | allocator_frag_bytes | mem_fragmentation_ratio |
|---|---|---|---|---|
| 写入后 | 166MB | 196MB | 9MB | 1.18 |
| 删掉四分之三后 | 46MB | 196MB | 128MB | 4.27 |
打开 activedefrag 40 秒后 | 46MB | 107MB | 39MB | 2.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。
五、容量与配置的几条实践
- 一定要设
maxmemory。 不设时 Redis 会一直申请内存,最终被系统 OOM Killer 杀掉,主从切换和数据丢失一起发生。 maxmemory留出余量。 复制积压缓冲区、客户端输出缓冲区、AOF 重写和 RDB 持久化时的写时复制都会占用额外内存,通常按物理内存的 60%—70% 设置。- 当缓存用就选
allkeys-lru或allkeys-lfu;当存储用(数据不能丢)就选noeviction,并保证内存充足、有告警。同一个实例不要混用这两种用途。 - 所有缓存 key 都设置 TTL,并加随机偏移。没有 TTL 的常驻数据单独放一个实例。
- 监控这几个指标:
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 故障时业务怎么兜底,见 多级缓存、预热与热点数据。
配套实验
- codesphere-labs/cache/redis-memory-eviction:五种配置的写满行为、扫描污染下的 LRU 与 LFU、过期回收时间序列、碎片与主动碎片整理(验证记录)
参考资料