☰
Redis 内存还剩 20G 却写入报错:maxmemory 之外的 3 类隐性占用
2026/10/28 18:54:31 网站建设 项目流程

我不会起名字322· 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • Redis 内存还剩 20G 却写入报错:maxmemory 之外的 3 类隐性占用
    • 一、先对齐口径:Redis 的内存不是一个数
    • 二、3 类隐性占用
      • 类别 1:客户端输出缓冲区(最常见的一个)
      • 类别 2:复制相关缓冲(积压区 + 从节点缓冲)
      • 类别 3:内存碎片与分配器不归还
    • 三、命令清单
    • 四、配置模板
    • 五、4 步定位顺序
    • 六、总结

Redis 内存还剩 20G 却写入报错:maxmemory 之外的 3 类隐性占用

凌晨收到告警:一批写入接口开始报OOM command not allowed when used memory > 'maxmemory'。

登上机器一看,INFO memory显示used_memory是 18G,而maxmemory设的是 20G——明明还剩 2G 可用,写入却被拒了。更奇怪的是内存曲线:过去一小时业务数据量几乎没变,但 Redis 进程的实际占用(used_memory_rss)从 20G 一路涨到了 26G。

这个问题的答案是:maxmemory管的只是「数据」那一块内存,而 Redis 进程还背着三块不被 maxmemory 计入的隐性占用。这篇文章把这三类拆开,给出 4 步定位命令,以及一份能直接抄的内存配置模板。在 Redis 的性能优化清单里,内存口径是最容易被忽略的一环。

一、先对齐口径:Redis 的内存不是一个数

INFO memory里有一堆used_memory_*,先弄清谁是谁:

字段含义是否受maxmemory限制
used_memory分配器(jemalloc)视角:数据 + 缓冲 + 部分内部开销✅ 是(超过就触发淘汰/拒写)
used_memory_rss操作系统视角:进程实际占用的物理内存❌ 否
used_memory_peak历史峰值—
mem_fragmentation_ratiorss / used_memory,碎片率—
maxmemory你设的上限,0 = 不限—
mem_clients_normal普通客户端的输入/输出缓冲❌ 否(Redis 7 起单独统计)
mem_clients_slaves从节点客户端缓冲❌ 否
mem_aof_bufferAOF 缓冲❌ 否
mem_replication_backlog复制积压缓冲区❌ 否

关键的两条:

  1. maxmemory限制的是used_memory,不是进程占用的全部内存。官方也明确说过:如果你有 10G 内存,最好把maxmemory设成 8G 左右——要给缓冲和碎片留余量。
  2. used_memory里虽然包含了一部分缓冲,但超限判断发生在「写入数据时」,而很多隐性占用是在写入之后才涨起来的。所以会出现「刚才还能写、现在突然拒写」的假象。

健康状态下的碎片率在1.03 左右;超过1.5就说明碎片比较严重了;小于 1更危险,意味着 Redis 用了 swap(虚拟内存),性能会断崖式下跌。

二、3 类隐性占用

类别 1:客户端输出缓冲区(最常见的一个)

Redis 给每个连接分配输出缓冲区,用来暂存「要发给客户端但对方还没收走」的数据。如果客户端读得慢(或者干脆不读),这个缓冲区会一直堆积。

三种典型来源:

  • 慢客户端:某个订阅者、批处理任务消费太慢;
  • 从节点连接:主节点要给从节点发复制流,从节点处理慢就会堆积(mem_clients_slaves);
  • Pub/Sub 消费者:订阅了频道但不消费,Pub/Sub 的输出缓冲是无上限保护里的重点(默认pubsub32mb 8mb 60)。

Redis 提供了client-output-buffer-limit三档限制:

# 格式:<class> <hard limit> <soft limit> <soft seconds> client-output-buffer-limit normal 0 0 0 # 普通客户端:默认不限制 ⚠ client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60

注意normal默认是0 0 0(不限制)——这意味着一个卡死的普通客户端理论上能把内存吃干。生产上建议给normal也设一个硬上限。

类别 2:复制相关缓冲(积压区 + 从节点缓冲)

只要开了主从,就有两块固定的额外开销:

缓冲配置说明
复制积压缓冲区repl-backlog-size(默认 1mb)主节点上保存最近写命令的环形区,断连重连时做部分同步用。从节点多、写入量大时要调大
从节点输出缓冲client-output-buffer-limit replica每个从节点一份,从节点多就线性增长

一个容易踩的场景:从节点数量 × 写入流量突然放大(比如扩容从节点、或者从节点网络抖动),主节点的复制缓冲会在几分钟内吃掉几个 G,而这些完全不算在业务数据量里。

类别 3:内存碎片与分配器不归还

jemalloc 按固定大小的 bin 分配内存,删掉的 key 留下的空洞不一定能立刻被复用,也不一定马上还给操作系统。表现就是:

  • used_memory降了(数据删了),但used_memory_rss不降 →mem_fragmentation_ratio升高;
  • 频繁写入不同大小的 value(尤其是大 value 频繁更新)会让碎片快速累积。

Redis 4.0+ 有自动碎片整理:

activedefrag yes active-defrag-ignore-bytes 100mb # 碎片超过 100MB 才开始整理 active-defrag-threshold-lower 10 # 碎片率下限 10% active-defrag-threshold-upper 100 # 碎片率上限 100%,越高整理越激进

手动触发一次(jemalloc 归还内存给 OS,4.0+ 需要编译时支持):

MEMORY PURGE

碎片整理会消耗 CPU,建议在低峰开启并根据实际碎片率调阈值,不要照抄。

三、命令清单

命令作用看什么
redis-cli INFO memory | grep -E "used_memory|maxmemory|fragmentation|mem_"内存全景对比used_memory/rss/maxmemory
redis-cli INFO clients客户端概览connected_clients、client_recent_max_input_buffer
redis-cli CLIENT LIST逐个客户端obl(输出缓冲长度)omem(输出缓冲内存)idle
redis-cli CLIENT LIST | awk '{print $NF}' | sort | uniq -c按客户端归类快速找异常来源
redis-cli INFO replication主从状态从节点数量、master_repl_offset差
redis-cli --bigkeys扫大 key找出占空间的 key 类型
redis-cli --memkeys按内存扫(Redis 6.2+,需 LFU/LRU)直接找最吃内存的 key
redis-cli MEMORY USAGE <key>单 key 内存精确核算
redis-cli MEMORY STATS内存细账各部分的占比(含clients.normal、replication.backlog等)
redis-cli MEMORY DOCTOR官方诊断建议会直接提示碎片/缓冲问题
redis-cli INFO stats | grep -E "evicted_keys|keyspace"淘汰情况evicted_keys增长 = 在淘汰
redis-cli CONFIG GET maxmemory-policy淘汰策略确认是不是noeviction
redis-cli INFO persistence持久化状态AOF/RDB 相关缓冲是否异常
redis-cli SLOWLOG GET 10慢查询大 key 操作常常伴随慢查询

四、配置模板

# ===== 内存上限:给缓冲和碎片留 20%~25% 余量 ===== # 机器 32G 内存时,Redis 最多用到 24G,maxmemory 设 20G maxmemory 20gb maxmemory-policy allkeys-lru # 纯缓存用 allkeys-lru;有持久数据用 volatile-lru # 注意:noeviction 会直接拒写,缓存场景不要用它 # ===== 客户端输出缓冲:给 normal 也加上限 ===== client-output-buffer-limit normal 256mb 64mb 60 client-output-buffer-limit replica 512mb 128mb 60 client-output-buffer-limit pubsub 32mb 8mb 60 # ===== 复制缓冲 ===== repl-backlog-size 128mb # 默认 1mb 太小,按「峰值写入速率 × 允许断连时长」估算 repl-backlog-ttl 3600 # 从节点断开后积压区保留时间 # ===== 碎片整理(低峰开启,按实际情况调) ===== activedefrag yes active-defrag-ignore-bytes 100mb active-defrag-threshold-lower 10 active-defrag-threshold-upper 100 # 整理占用的 CPU 上限(25/75 表示最低 25%、最高 75%) active-defrag-cycle-min 25 active-defrag-cycle-max 75

运行时改配置(不用重启):

redis-cli CONFIG SET maxmemory 20gb redis-cli CONFIG SET client-output-buffer-limit"normal 256mb 64mb 60"redis-cli CONFIG SET repl-backlog-size134217728redis-cli CONFIG REWRITE# ⭐ 别忘了落盘,否则重启就回到旧值

五、4 步定位顺序

第 1 步:先分清「是数据涨了」还是「缓冲涨了」。

redis-cli INFO memory|grep-E"^used_memory:|^used_memory_rss:|^maxmemory:|^mem_fragmentation_ratio:"redis-cli MEMORY STATS

判断规则:

  • used_memory接近maxmemory且evicted_keys在涨 →真的是数据问题,去第 2 步;
  • used_memory远小于maxmemory却还在报 OOM → 看MEMORY STATS里的clients.normal、replication.backlog、aof.buffer,缓冲是元凶;
  • mem_fragmentation_ratio> 1.5 → 碎片问题,去第 4 步。

第 2 步:数据侧找大 key。

redis-cli--bigkeys# 每种类型最大的 keyredis-cli--memkeys# Redis 6.2+:按内存占用排序redis-cli MEMORY USAGE user:cache:10086

重点看:是不是有本该设 TTL 却没设的 key、是不是有单个几百 MB 的集合、是不是有某个业务在批量写入新前缀。

第 3 步:缓冲侧找客户端。

redis-cli CLIENT LIST

关注omem(输出缓冲内存)和idle(空闲秒数)。出现「omem几十 MB 且idle很大」的客户端,基本就是它。确认后按连接杀掉:

redis-cli CLIENT KILL ADDR10.0.0.7:54321 redis-cli CLIENT KILL TYPE normal# 谨慎:会杀掉所有普通客户端

如果是从节点导致的,看INFO replication里各从节点的offset差(master_repl_offset - slave_repl_offset),差得越多说明积压越多。

第 4 步:碎片侧处理。

redis-cli INFO memory|grepmem_fragmentation_ratio redis-cli MEMORY PURGE# 尝试把空闲内存还给 OSredis-cli CONFIG SET activedefragyes

碎片降不下去的常见原因是大量中等大小的 value 反复更新(比如 1KB~10KB 的 hash 频繁 hset)。这种情况要么改写入模式(批量合并、控制 value 大小),要么接受碎片率并给足内存余量。

六、总结

  1. maxmemory只管used_memory(数据为主),管不住客户端缓冲、复制积压和碎片——所以「还剩 2G」和「写入被拒」可以同时成立。
  2. 3 类隐性占用:① 客户端输出缓冲(尤其是normal默认不限、pubsub消费者不消费);② 复制相关(积压区 + 每个从节点一份输出缓冲);③ 内存碎片(碎片率 > 1.5 就要处理,< 1 说明用了 swap,更危险)。
  3. 排查顺序不要反:先看INFO memory和MEMORY STATS判断方向(数据 / 缓冲 / 碎片),再分别用--bigkeys、CLIENT LIST、MEMORY PURGE定点处理。一上来就FLUSHDB是最糟的做法。
  4. 配置上留 20%~25% 余量:机器 32G → Redis 上限 24G →maxmemory设 20G。并给client-output-buffer-limit normal补上限制(默认是 0 0 0)。
  5. CONFIG SET之后必须CONFIG REWRITE,否则重启回退,半夜还会再响一次。

本文命令基于 Redis 6/7 的字段名(Redis 7 起INFO memory把客户端/复制/AOF 缓冲拆成了mem_clients_normal、mem_clients_slaves、mem_aof_buffer、mem_replication_backlog等独立字段)。老版本(5.x)没有这些细分字段,需要靠MEMORY STATS或used_memory与业务数据量之差来推断。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询