news 2026/6/10 18:10:41

Redis 内存泄露排查:从 200M 飙升到 8G,罪魁祸首竟然是一个不起眼的 Key

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis 内存泄露排查:从 200M 飙升到 8G,罪魁祸首竟然是一个不起眼的 Key

0. 序幕:周五下午的惊魂时刻

那是周五下午 4 点,离下班还有一个小时,我正准备提交最后一行代码。突然,运维群里的报警机器人疯了。

[CRITICAL] Redis Cluster-02 Memory Usage > 90% (8.1GB / 8.5GB)
[CRITICAL] Redis Cluster-02 OOM Killer invoked

我心里“咯噔”一下。这台 Redis 实例平时主要做用户 Session 和一些简单的配置缓存,常年稳定在200M左右。怎么可能在短短两小时内飙升到8G

这不是简单的流量突增,这是一次典型的内存“泄露”(或者说是业务层面的逻辑泄露)。如果不立刻解决,整个用户登录系统将全面瘫痪。

本文将带你还原这场惊心动魄的排查过程,从现象分析、工具使用到源码定位,揭示那个差点毁了我们周末的“罪魁祸首”。


1. 案发现场:第一轮常规体检

连上 VPN,通过堡垒机登录生产环境。我的第一反应是:是不是内存碎片?

1.1 排除内存碎片

Redis 的内存占用不仅仅是数据大小,还包括内存分配器的开销。如果碎片率过高,操作系统看到的内存占用(RSS)会远大于 Redis 实际存储的数据(used_memory)。

我迅速敲下命令:

redis-cli -h192.168.x.x -p6379INFO memory

返回的关键指标如下:

# Memoryused_memory:8152940210# 约 8.15 GB (Redis 认为它存的数据量)used_memory_rss:8421005120# 约 8.42 GB (操作系统实际分配的量)mem_fragmentation_ratio:1.03

分析

  • mem_fragmentation_ratio只有 1.03,说明内存碎片极少(通常 > 1.5 才需要担心碎片问题)。
  • used_memory实打实地达到了 8.15 GB。

结论:这不是操作系统的问题,是真的有数据把 Redis 撑爆了

1.2 排除客户端缓冲区积压

有时候,如果消费端处理太慢,Redis 的输出缓冲区(Output Buffer)会积压大量数据。

redis-cli -h192.168.x.x -p6379CLIENT LIST

我检查了omem(Output Buffer Memory) 一列,发现所有连接的占用都为 0 或很小。
结论:排除客户端堆积问题。


2. 抽丝剥茧:寻找那个胖子

既然是数据问题,那么只有两种可能:

  1. 海量小 Key:突然写入了千万级的微小 Key。
  2. 超级大 Key:某一个或几个 Key 像黑洞一样在吞噬内存。
2.1 尝试一:dbsize里的猫腻

我先看了一下 Key 的总数:

127.0.0.1:6379>dbsize(integer)15203

疑点出现了!
只有 1.5 万个 Key?
如果 1.5 万个 Key 占用了 8G 内存,平均每个 Key 大约500KB。这在 Redis 里绝对属于“巨型”数据了。平时的 Session Key 只有几 KB。

这强烈暗示:问题不在于 Key 的数量,而在于 Key 的体积。

2.2 尝试二:--bigkeys的扫描

Redis 自带的--bigkeys参数是排查神器,它会扫描整个 keyspace,找到每种数据类型中最大的那个 Key。

redis-cli -h192.168.x.x -p6379--bigkeys

终端飞速滚动,最后输出了摘要:

... [Summary] Sampled 15203 keys in the keyspace! Total key length in bytes is 420123 (avg len 27.63) Biggest string found 'user:session:9921' has 1024 bytes Biggest list found 'system:notification:queue' has 25012 items Biggest hash found 'config:app:settings' has 5 fields ...

困惑

  • 最大的 String 只有 1KB。
  • 最大的 List 只有 2.5 万个元素(即便每个元素 1KB,也才 25MB)。
  • 这就奇怪了,--bigkeys竟然没扫出来?

排查盲区
--bigkeys是基于采样的(虽然它扫描所有 Key,但如果是巨大的 Set 或 Hash,它只统计元素个数,不一定能精准反映内存占用)。或者,这个 Key 可能刚刚过期了?或者是在某些隐藏的 DB 索引里?

不,不对。内存依然是 8G。说明 Key 还在。


3. 深度取证:RDB 离线分析法

在线上执行DEBUG OBJECT或者遍历所有 Key 是极其危险的,会阻塞主线程。为了不影响业务(虽然已经快挂了),我决定采用离线分析

3.1 导出 RDB

利用 Redis 的 BGSAVE 功能(注意:如果内存快满了,BGSAVE 的fork操作可能会导致 OOM,需确认sysctl vm.overcommit_memory设置为 1)。

redis-cli bgsave

等待 dump.rdb 生成后,我将其下载到本地分析服务器。

3.2 使用rdb-tools验尸

这是一个 Python 编写的强大工具,能将 RDB 解析成 CSV 或 JSON。

pipinstallrdbtools# 解析 RDB,按内存大小排序,取前 3 名rdb -c memory dump.rdb --bytes128-l3

几分钟的漫长等待后,屏幕上跳出了结果。看到结果的那一刻,我差点一口老血喷出来。

database,type,key,size_in_bytes,encoding,num_elements,len_largest_element 0,list,global:error:log:2023-10-27,8102394812,quicklist,45020120,512 0,string,user:session:102,4096,raw,1024,1024 0,hash,app:config,2048,ziplist,10,200

凶手找到了!

  • Key 名称global:error:log:2023-10-27
  • 类型List
  • 大小8.1 GB
  • 元素个数4500 万个

一个用来记录“全局错误日志”的 List,竟然在一天之内膨胀到了 8G!


4. 案情还原:一行代码的蝴蝶效应

拿到 Key 名字后,我迅速在代码库中全局搜索。

终于,在一个角落的GlobalExceptionHandler.java(全局异常处理类)里发现了这段逻辑:

// 伪代码示例publicvoidhandleException(Exceptione){StringlogKey="global:error:log:"+LocalDate.now().toString();StringerrorMsg=e.getMessage()+StackTraceUtils.toString(e);// 记录错误日志到 Redis,方便后台查看redisTemplate.opsForList().rightPush(logKey,errorMsg);}

致命缺陷分析

  1. 无限追加:使用RPUSH写入 Redis List,原本是想做一个简单的日志队列。
  2. 无 TTL没有设置过期时间!
  3. 无长度限制没有执行LTRIM这意味着 List 可以无限增长。
  4. 触发条件:平时系统稳定,错误很少,这个 Key 也就几 MB。但今天下午,某个第三方 API 服务挂了,导致系统内部抛出了海量的ConnectionTimeoutException
  5. 死循环:每次异常都触发handleException,写入 Redis;如果 Redis 慢了或者满了,可能引发新的异常,再次写入…

这就是 200M 飙升到 8G 的真相:一次外部服务的抖动,配合一段没有边界限制的代码,制造了一个吞噬内存的黑洞。


5. 紧急救援:如何安全删除 8G 的大 Key?

找到了凶手,现在的任务是“排雷”。

绝对不能做的事
直接执行DEL global:error:log:2023-10-27

为什么?
Redis 是单线程的。删除一个 8G 的大 Key,涉及 4500 万次内存释放操作。这会导致 Redis 主线程阻塞几十秒甚至几分钟。在这期间,所有应用请求都会超时,导致真正的服务雪崩。

5.1 正确方案:UNLINK (Lazy Free)

如果你的 Redis 版本 >= 4.0,请务必使用UNLINK命令。

UNLINK global:error:log:2023-10-27

原理
UNLINK只是把这个 Key 从元数据(Keyspace)中摘除,告诉 Redis “这个 Key 不可见了”。真正的内存释放操作会交给后台线程(Bio Thread)异步执行,不会阻塞主线程。

5.2 兼容方案(Redis 4.0 以下)

如果还在用老版本,只能用脚本一点点地删(Scan + LTRIM/LPOP)。

# Python 渐进式删除脚本示例defsafe_delete_list(redis_conn,key,batch_size=10000):whileredis_conn.llen(key)>0:# 每次只修剪 10000 个元素redis_conn.ltrim(key,batch_size,-1)time.sleep(0.1)# 甚至可以睡一会,让出 CPUredis_conn.delete(key)

执行完UNLINK后,INFO memory监控图表瞬间出现了一个漂亮的断崖式下跌。内存回到了 200M。


6. 总结与反思:如何避免下一次悲剧?

虽然事故解决了,但这次教训必须深刻吸取。作为架构师,我们需要从规范层面堵住漏洞。

6.1 开发规范三原则
  1. 所有 Key 必须有过期时间
    这是 Redis 使用的第一铁律。特别是对于日志、缓存类数据,必须设置 TTL(Time To Live)。
redisTemplate.expire(logKey,1,TimeUnit.DAYS);
  1. 容器类数据结构必须有容量限制
    使用 List, Set, Hash, ZSet 时,必须考虑“最大包含多少元素”。
redisTemplate.opsForList().rightPush(logKey,msg);// 每次 push 后,保留最近 1000 条redisTemplate.opsForList().trim(logKey,-1000,-1);
  1. 禁止在 Redis 存放巨型对象
    如果需要存储日志,请使用 ELK (Elasticsearch, Logstash, Kibana) 或文件系统。Redis 是内存数据库,寸土寸金,不是垃圾桶。
6.2 运维监控兜底
  1. **设置maxmemory-policy**
    生产环境必须设置内存淘汰策略。例如volatile-lruallkeys-lru。如果设置了策略,当内存满时,Redis 会自动踢掉旧数据,而不是直接 OOM 宕机(虽然这可能会丢日志,但保住了服务)。
  2. 大 Key 预警
    利用云厂商的 Redis 分析服务,或者自建脚本定期扫描(在低峰期),一旦发现超过 50MB 的 Key,立即报警。
  3. 流量监控
    监控 Redis 的output_bufferinput_buffer,以及网络流量。异常的流量增长往往是大 Key 的前兆。

7. 结语

技术债总是要还的,通常还是在周五下午。

那个不起眼的List,因为缺乏边界控制,在特定条件下变成了吞噬系统的怪兽。这次排查经历告诉我们:敬畏每一行代码,特别是那些看似无害的日志逻辑。

希望这篇文章能放入你的“避坑指南”里。如果你也遇到过类似的 Redis 奇葩问题,欢迎在评论区留言分享!


互动环节 (Hook)

👀你遇到过最离谱的 Redis 故障是什么?

  1. 开发把 100MB 的 HTML 存进了 String?
  2. 循环里忘了关 Redis 连接导致连接数耗尽?
  3. 或者是和我一样,被日志撑爆了内存?

在评论区晒出你的“炸库”经历,点赞最高的,我下期专门写一篇关于 Redis 性能优化的硬核代码解析!别忘了关注我,获取更多后端实战干货!👇

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 11:01:03

收藏!35岁程序员转行大模型领域:从入门到落地的全路径规划

对于35岁的程序员而言,转行进入风口正盛的大模型领域,既是突破职业瓶颈的契机,也需要科学的规划与坚定的执行。不同于应届生的从零起步,资深程序员可凭借现有技术积淀快速切入,以下是经过实践验证的转行准备路径&#…

作者头像 李华
网站建设 2026/6/10 17:23:38

旧景如故,新景盎然,南湖公园续写九江温柔时光

历经五个月的闭园改造,位于九江市的南湖公园现已重新对公众开放。这座始建于上世纪五十年代的公园,是城市里具有历史记忆的地标之一。如今的南湖公园,通过应用海绵城市技术与延续场所记忆,将生态功能、人文氛围与市民的休闲需求紧…

作者头像 李华
网站建设 2026/6/7 1:18:12

IMDSI02数字输入模块

IMDSI02 数字输入模块是一种用于工业自动化系统的现场信号采集单元,主要负责接收来自开关量设备的状态信号,并将其可靠地传送到控制系统中,为逻辑判断和联锁控制提供基础数据。 主要特点: 支持多路数字量输入,适用于多…

作者头像 李华
网站建设 2026/6/10 1:06:13

秦朝48郡分布SHP矢量数据

作为中国历史上第一个统一的中央集权王朝,秦朝推行的郡县制奠定了后世行政区划的基础。 秦郡的数量与分布历来是历史地理研究的核心议题,其中“四十八郡”说基于谭其骧先生《中国历史地图集》等权威研究,成为学界与爱好者广泛参考的主流观点…

作者头像 李华
网站建设 2026/6/10 14:49:56

React Server Components (RSC) 协议中的高危漏洞:CVE-2025-55182 技术剖析

我撰写这篇博客是因为我发现,对于那些确实了解React但无法理解此问题的初学者,目前还没有任何博客能真正解释清楚。本文纯粹用于教育目的。 此问题的根源在于Next.js中React Server Functions处理客户端和服务器之间数据块的方式存在安全缺陷。以下是逐步…

作者头像 李华