资讯动态

Redis 内存满了会怎样?6.2 实测:OOM command not allowed、8 种淘汰策略各删了谁、删了 key 内存为什么不降

发布时间:2026/10/17 6:54:51 来源:尧图企业网站定制
这一篇讲什么「Redis 内存满了」网上的答案大多是一张淘汰策略表,外加一句「设置 maxmemory 和淘汰策略即可」。真碰上的时候,想知道的往往是:满了以后应用那边报什么错?是所有命令都不能用,还是只有写不了?8 种淘汰策略,到底删掉的是哪些 key?热数据会不会被删?不设maxmemory会怎样?为什么删了一半 key,内存监控上一点没降?key 早就过期了,为什么内存还占着?没往 Redis 里多写东西,数据怎么就被淘汰了?这一篇每个问题都在真机上把内存写满,贴出命令输出、错误原文和INFO里的数字。实测环境:CentOS 7.9,Redis 6.2.4源码编译(gcc 7.3.1,内存分配器jemalloc-5.1.0)。实验用单独的实例(端口 6380),不开持久化(save 、appendonly no),maxmemory按每个实验调。Java 端:Spring Boot 4.1.1 Lettuce 7.5.2。写入的数据都是SET k:序号 100 个 x,用redis-cli --pipe批量灌。1. 默认策略下写满:直接拒写1.1 报什么错maxmemory 50mb、maxmemory-policy noeviction,灌 100 万条:errors: 730087, replies: 1000000只写进去 269,913 条,其余 730,087 条每一条的回复都是:OOM command not allowed when used memory maxmemory.INFO stats里evicted_keys:0—— noeviction 不删任何东西,满了就拒。noeviction 就是默认值。redis.conf原文里这两行都是注释掉的:# maxmemory bytes # maxmemory-policy noeviction我这台机器是照一份常见的安装说明装的,配置里只加了maxmemory 1024000000,没配策略,CONFIG GET maxmemory-policy读出来就是noeviction。只配上限、不配策略,满了的表现就是这一节:拒写。1.2 满了以后哪些命令还能用为了让结果干净,把上限调到当前用量以下(CONFIG SET maxmemory 40mb,此时used_memory是 48M),再逐条试:命令结果GET k:5正常返回SET k:6 y(把 100 字节的值改短成 1 字节)(error) OOM command not allowed when used memory maxmemory.SET brandnew 1OOMINCR/APPEND/GETSET/SETEXOOMHSET/LPUSH/SADDOOMEXPIRE k:9 100/PERSIST k:9(integer) 1,正常DEL k:10/UNLINK k:11(integer) 1,正常FLUSHALLOK注意第二行:把值改短也被拒。Redis 不判断这条命令会不会让内存变大,它看的是两件事:执行前used_memory是否已经超过maxmemory,以及这条命令有没有denyoom标志。用COMMAND INFO可以直接查:127.0.0.1:6380 command info set 1) 1) set 2) (integer) -3 3) 1) write 2) denyoom ... 127.0.0.1:6380 command info expire 1) 1) expire 2) (integer) 3 3) 1) write 2) fast ...被拒的set、getset、append、incr、setex都带denyoom;能执行的expire、persist、del、unlink、get、flushall都没有。所以满了以后,删除和设过期时间还能做—— 这是能自救的那条路。我第一次是写满后马上逐条试的,结果有的SET成功、有的失败:内存正好卡在 50M 的边上,一条改短值、一条DEL腾出一点点空间,后面的写入又能进去。线上也一样:贴着上限的时候,同样的命令时好时坏,看起来像随机报错。1.3 Java 端看到什么Spring Boot 4.1.1(Lettuce)写一个已经超限的实例:org.springframework.data.redis.RedisSystemException: Error in execution io.lettuce.core.RedisCommandExecutionException: OOM command not allowed when used memory maxmemory.set和increment都是这个;同一时刻get正常返回、delete正常执行。异常最外层只写着Error in execution,要往下看 cause才知道是内存满了。2. 8 种淘汰策略,各删掉了谁2.1 实验设计每种策略都做同样的事:FLUSHALL,写 2 万个不带过期时间的perm:*,和 2 万个带过期时间的vol:*(EX 100000i,序号越小剩余 TTL 越短);等 2 秒,然后把两组的前 2000 个各读 5 遍,当作热数据;CONFIG SET maxmemory 当前 used_memory,让内存刚好满;再写 2 万个新 keynew:*,数每组还剩多少。2.2 结果policy perm perm热 vol vol热 new 被拒 evicted noeviction 20000 2000 20000 2000 0 20000 0 allkeys-lru 10494 2000 10403 2000 20000 0 19103 volatile-lru 20000 2000 2085 1776 20000 0 17915 allkeys-lfu 13172 2000 13110 2000 14226 0 19492 volatile-lfu 20000 2000 2085 1754 20000 0 17915 allkeys-random 12435 1211 12445 1207 15721 0 19399 volatile-random 20000 2000 2081 194 20000 0 17919 volatile-ttl 20000 2000 2081 0 20000 0 17919(perm热/vol热 前 2000 个热 key 还剩几个;被拒 写new:*时报 OOM 的条数。)LRU / LFU 在 Redis 里是采样近似的(默认maxmemory-samples 5),所以我跑了两次。两次的定性结论完全一样,数字差几十到几百,比如 allkeys-lru 第二次是10480 / 2000 / 10415 / 2000 / 20000。2.3 怎么读这张表noeviction:新数据一条都没写进去。allkeys-lru:新数据全进去了,两组热 key 一个没丢,删掉的是两组里的冷数据。最「符合直觉」的一个。volatile-*(lru / lfu / random / ttl):perm:*一个都没动,只在带过期时间的 key 里挑。volatile-ttl:热的那 2000 个vol恰好是 TTL 最短的,结果全部被删(剩 0)。它只看谁快过期,不看谁常用。allkeys-lfu:热 key 都保住了,但刚写进来的新 key 也被删了,2 万条只剩 14,226 —— 新 key 访问次数少,在 LFU 眼里就是冷数据。random:热 key 也只剩六成左右(1,211 / 2,000)。2.4 坑:设了 volatile 策略,数据却都没有 TTL很多人把策略设成volatile-lru,但业务里大部分 key 是不设过期时间的。只写 2 万个不带 TTL的 key,写满后再写 2 万个新的:策略结果volatile-lru20000 条全部OOM command not allowed …,evicted_keys:0volatile-ttl同上volatile 策略找不到可淘汰的 key 时,表现和 noeviction 一模一样—— 设了淘汰策略,却照样拒写。3. 不设 maxmemory:被系统杀掉,Redis 日志里什么都没有maxmemory默认是 0,即不限制。这时一直写下去,Redis 会一直向系统要内存,直到系统不给。这台机器上还跑着别的服务,为了不把整机写爆,我把实验实例放进一个cgroup,限制它最多用 200M 内存(memory.limit_in_bytes 200M,memory.swappiness 0),相当于「这台机器只给 Redis 200M」。确认CONFIG GET maxmemory是0,然后灌 200 万条(约 337M):大约 2 秒后,Redis 进程没了。客户端--pipe什么都没输出,退出码 1;再连:Could not connect to Redis at 127.0.0.1:6381: Connection refused。Redis 自己的日志一行相关记录都没有,最后一行还是启动时的Ready to accept connections。原因在内核日志里,dmesg:redis-server invoked oom-killer: gfp_mask0xd0, order0, oom_score_adj0 Task in /redislab killed as a result of limit of /redislab Memory cgroup out of memory: Kill process 17417 (jemalloc_bg_thd) score 981 or sacrifice child Killed process 17412 (redis-server), UID 0, total-vm:409364kB, anon-rss:204732kB, file-rss:1896kB, shmem-rss:0kB同样的步骤跑了两次,现象一致。所以「Redis 突然没了、日志里什么都没有」,第一件事是看dmesg里有没有Killed process。这里因为用的是 cgroup,被杀的只会是组里的 Redis;如果是整台机器内存耗尽,内核会按oom_score挑进程杀,被杀的不一定是 Redis —— 也可能是同机的数据库或应用。4. 删了一半 key,内存为什么不降4.1 现象maxmemory 0,写 100 万个 key,然后隔一个删一个(删 50 万):时点used_memoryRSSmem_fragmentation_ratio写入后168.97M175.05M1.07删 50 万后88.90M182.97M2.00等 15 秒88.90M178.97M2.01MEMORY PURGE后88.90M178.97M2.01FLUSHALL后851.91K17.68M21.25used_memory是 Redis 自己记的「数据占了多少」,删完立刻降了一半;RSS 是操作系统看到的进程占用,几乎没动。监控上看的通常是后者,所以「删了 key 内存没降」。两者的比值就是mem_fragmentation_ratio,从 1.07 变成了 2.00 —— 删掉的 key 散落在各处,空出来的是一个个小洞,分配器没法把整页还给系统。MEMORY PURGE(让 jemalloc 归还空闲页)在这里没有任何效果。FLUSHALL全清以后 RSS 才回到十几 M。空库时mem_fragmentation_ratio是 15~21:分母used_memory只有 800K,这时候这个比值没有参考意义,别被它吓到。4.2 activedefrag:要过门槛才会动Redis 有主动碎片整理activedefrag(默认关闭)。在上面那个状态下CONFIG SET activedefrag yes,等了 60 秒:active_defrag_hits:0 active_defrag_misses:0一次都没整理。查参数:active-defrag-ignore-bytes 104857600 active-defrag-threshold-lower 10redis.conf里对它的注释是Minimum amount of fragmentation waste to start active defrag,默认 100MB:碎片少于 100MB 不启动整理。此时碎片(allocator_frag_bytes)约 84MB,没到门槛。数据量翻倍再来一遍(200 万个,删 100 万,碎片约 168MB),默认参数:时点used_memoryRSSmem_fragmentation_ratio删 100 万后等 15 秒177.01M347.47M1.96MEMORY PURGE后177.01M347.47M1.96activedefrag yes后 30 秒177.01M270.18M1.5360 / 120 秒177.01M268.6M1.52这次动了:active_defrag_hits:2069542,RSS 从 347M 降到 269M。之后停住不再降,这时allocator_frag_bytes是 85,529,848(约 85MB),刚好又低于 100MB 的门槛。结论:删 key 不降 RSS 是正常的;想让它降,开activedefrag,但要知道它有门槛,碎片不到active-defrag-ignore-bytes(默认 100MB)时开了也不动。5. key 过期了,内存却还占着5.1 全部过期:几秒就清完100 万个 key 全部EX 5,写完不再访问:t 0s dbsize 1000000 expired_keys:0 used_memory_human:199.86M t 4s dbsize 955032 expired_keys:46055 used_memory_human:191.24M t 6s dbsize 384537 expired_keys:616790 used_memory_human:86.74M t 8s dbsize 0 expired_keys:1000000 used_memory_human:852.12K5.2 少量过期混在大量未过期里:清得极慢更常见的情况:库里大部分 key 的过期时间很长,只有一小部分到期了。100 万个EX 86400 10 万个EX 5,写完不再访问:t 0s dbsize 1100000 expired_keys:0 used_memory_human:250.25M t 30s dbsize 1099377 expired_keys:623 used_memory_human:234.13M t 60s dbsize 1098649 expired_keys:1351 t120s dbsize 1097079 expired_keys:2921 t180s dbsize 1095639 expired_keys:4361 used_memory_human:233.45M10 万个早就过期的 key,3 分钟只清掉了 4,361 个,dbsize里仍然算着它们,内存也还占着。此时INFO stats里:expired_stale_perc:7.92redis.conf里对后台清理的说明是:默认力度下,它尽量让残留的过期 key 不超过一成(try to avoid having more than ten percent of expired keys still in memory)。这里 10 万 / 110 万不到一成(expired_stale_perc7.92%),所以它清得很省力。这时候直接访问某个过期 key,会被当场判为不存在(EXISTS short:1→0)—— 所以业务上看不出问题,只有内存和dbsize不对。5.3 两个处理办法(都实测过)办法一:用 SCAN 扫一遍。SCAN 遍历到过期 key 时会顺手删掉:$ redis-cli -p 6380 --scan --pattern short:* | wc -l 0 ← 耗时 2,228ms扫完以后dbsize回到 1,000,000,expired_keys:100000,used_memory从 233M 降到 216M。办法二:调高active-expire-effort(默认 1,最大 10;设 11 会报argument must be between 1 and 10 inclusive)。改成 10,同样的场景再来一遍:t 10s dbsize 1077284 expired_keys:22716 t 20s dbsize 1033504 expired_keys:66496 t 30s dbsize 1000000 expired_keys:100000 expired_stale_perc:0.00约 25 秒清完;默认值时 3 分钟只清了 4,361 个。代价配置文件里写了:At its maximum value the system will use more CPU, longer cycles (and technically may introduce more latency)。6. 没多写数据,key 却被淘汰了:慢消费者最后一个场景解释「内存突然飙升」:客户端的输出缓冲区也算在used_memory里。maxmemory 50mb、allkeys-lru,先放 20 万个 key(used 34.80M)。起一个客户端SUBSCRIBE ch之后一直不读(模拟消费者卡住),然后往频道里发消息,每批 60 条、每条 100KB:dbsize used_memory evicted_keys 订阅端 omem 放好数据 200000 34.80M 0 0 第 1 批后 200000 37.76M 0 2,867,872 第 2 批后 200000 43.33M 0 8,919,104 第 3 批后 200000 49.19M 0 15,064,544 第 4 批后 167885 49.91M 32115 21,209,984 第 5 批后 131220 49.93M 68780 27,371,808 第 6 批后 94470 49.92M 105530 33,517,248一个 key 都没多写,数据被淘汰了 10.5 万个,超过一半。原因只是那个订阅端不读消息,Redis 替它攒着,攒的这部分算进了used_memory(INFO memory里记在mem_clients_normal)。pubsub 的输出缓冲默认上限是client-output-buffer-limit pubsub 33554432 8388608 60(硬上限 32MB)。实验里 omem 到了 33,517,248,还没碰到硬上限,所以这个订阅端没被断开,日志里也没有任何记录。排查时用CLIENT LIST看每个连接的omem(输出缓冲占的字节)和oll(积压的回复条数),谁在攒一眼就能看到:id598 ... sub1 ... oll328 omem33517248 ... cmdsubscribe7. 速查现象原因看哪里 / 怎么办OOM command not allowed when used memory maxmemory到上限了,且策略是 noeviction(默认)设淘汰策略;紧急时DEL/UNLINK/EXPIRE还能执行设了volatile-lru还是报 OOM数据没有 TTL,volatile 策略没东西可删改allkeys-lru,或给数据加 TTL热点 key 被删了volatile-ttl/random不看冷热换allkeys-lru/allkeys-lfu新写进去的 key 很快没了allkeys-lfu下新 key 访问次数少了解 LFU 行为,或用 LRURedis 突然没了,日志里什么都没有没设maxmemory,被系统 OOM 杀了dmesg找Killed process;设maxmemory删了大量 key,监控内存不降碎片,RSS 不还给系统看mem_fragmentation_ratio;activedefrag yes(碎片要过 100MB)dbsize远大于实际有效的 key过期 key 没被后台清掉INFO stats看expired_stale_perc;SCAN 扫一遍;调active-expire-effort没多写数据,却有大量evicted_keys客户端输出缓冲涨了(慢消费者等)CLIENT LIST看omem/oll下一篇:Redis 宕机会丢多少数据 ——kill -9和断电下,RDB、AOF 各丢多少,以及「明明有 dump.rdb,重启后数据却没了」的几种原因。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑