兄弟们,凌晨两点,手机突然像地震一样狂震,我迷迷糊糊摸到床头一看,运维群里炸了锅:"Redis节点又挂了!内存使用率飙到99%,CPU直接打满!" 顶着黑眼圈爬起来连服务器,刚登录就看到熟悉的报错:OOM killer 又把 Redis 进程干掉了。
那一刻我真想把写代码时随手往 Redis 里塞大集合的同事拎过来——咱就是说,存数据能不能别跟往麻袋里装砖头似的,可劲儿造啊!
一、先搞明白:啥是 Redis 大 key?它凭啥能搞崩服务器?
很多新手可能还不清楚,所谓"大 key"其实分两种情况:一种是单个 key 的值特别大(比如一个字符串类型的值超过1MB),另一种是集合类数据结构(像 hash、list、set、zset)里的元素数量超多(比如一个 zset 存了10万+成员)。别小看这些大块头,它们就像藏在 Redis 里的定时炸弹,主要靠这三招搞破坏:
1. 内存分布不均匀,分片集群秒变"单腿跳"
现在稍微大点的项目都用 Redis 集群,假设你用的是分片集群(比如 Codis、Redis Cluster),一个大 key 会被固定分配到某个分片上。想象一下,其他分片内存使用率才50%,就这个分片像吹气球一样涨到90%,整个集群的负载均衡瞬间失效。更要命的是,当你要删除这个大 key 时,分片节点会经历一段漫长的"卡顿期",因为删除操作需要释放大量连续内存,堪比在市中心拆除一栋摩天大楼,周围的交通都得跟着堵。
2. 网络IO成瓶颈,批量操作直接"卡脖子"
举个真实的例子:之前有个兄弟在项目里用 list 存用户的历史操作记录,一个 key 存了50万条数据。某天运营要导出用户数据,直接用 LRANGE key 0 -1 捞数据,结果 Redis 所在服务器的网卡流量直接飙到峰值,应用服务器这边等了10秒都没拿到响应。为啥?因为 Redis 是单线程模型,处理这种大集合操作时,会把所有元素序列化后通过网络传输,就像用一根水管同时给100户人家供水,水压自然上不去。
3. 内存碎片疯狂增长,好好的内存变成"碎纸片"
Redis 采用jemalloc分配内存,当大 key 被频繁删除和写入时,会产生大量无法利用的小碎片。比如你先存了一个10MB的大字符串,然后删除,再存一堆1KB的小字符串,jemalloc 没办法把这些小碎片合并成大的连续内存,导致实际内存使用率比 INFO memory 里看到的 used_memory 高很多。曾经见过一个线上节点,used_memory 显示8GB,但物理内存已经用了12GB,就是被碎片坑的。
二、检测大 key:别等崩溃了才后悔,提前扫描是王道
1. 最简单的命令:redis-cli --bigkeys
这个命令是 Redis 自带的大 key 扫描工具,原理是对每个数据库的不同数据类型做抽样检查。比如检查 string 类型时,会随机选一些 key 用 STRLEN 查看长度;检查集合类型时,用 HLEN、LLEN、SCARD、ZCOUNT 统计元素数量。注意要加 -i 0.1 参数,这表示每次扫描间隔0.1秒,避免阻塞主线程。不过它有个缺点:只能告诉你每个数据类型的最大 key 是谁,没办法扫描所有大 key,适合做初步排查。
2. 更精准的方案:自己写扫描工具(附Python代码)
如果需要全量扫描,就得用 SCAN 命令代替 KEYS *,因为 KEYS 会阻塞主线程,在生产环境用就是"自杀行为"。下面这段 Python 代码可以扫描指定前缀的大 key,支持设置字符串长度阈值和集合元素数量阈值:
3. 可视化工具辅助:让大 key 一目了然
如果觉得命令行太麻烦,可以用 RedisInsight(官方可视化工具)或者开源的 RedisDesktopManager,这些工具都有大 key 扫描功能,能生成直观的图表。比如 RedisInsight 的"Memory Analysis"模块,能按数据类型展示内存占用分布,点击某个类型就能看到具体的大 key 列表,适合团队协作时给非技术同学演示。
三、治理大 key:分场景出招,不同类型有不同解法
(一)字符串类型大 key:能压缩就压缩,能拆分就拆分
案例:用户详情存成大 JSON
某电商项目把用户详情(包括收货地址、订单历史、会员信息)存成一个大 JSON,单个 key 大小超过2MB。解决方案分两步:
- 数据压缩:先用 gzip 压缩 JSON 字符串,压缩后大小能降到500KB左右。Redis 提供了 COMPRESS 和 DECOMPRESS 命令(需要开启 redis-module-recompress 模块),不过更推荐在应用层处理,比如 Java 里用 GZIPOutputStream 和 GZIPInputStream。
- 按需拆分:把常用字段(比如用户名、头像)和不常用字段(比如三年前的订单)分开存储。比如用 user:123:base 存基础信息,user:123:order:2023 存2023年的订单,查询时用 MGET 批量获取,虽然多了几个 key,但每次获取的数据量小了,网络传输速度快了很多。
避坑指南:别用 APPEND 命令往大字符串里追加数据
曾经有个项目用 APPEND 记录用户操作日志,每天往一个 key 里追加几MB数据,一个月后这个 key 变成了50MB。APPEND 操作在字符串底层实现是动态扩展数组,当数组需要扩容时,会申请一块更大的内存,把旧数据复制过去,再追加新数据。50MB的字符串每次扩容都要复制大量数据,CPU使用率直接飙升,后来改成按天拆分key,问题立刻解决。
(二)集合类型大 key:分桶存储,别把鸡蛋放一个篮子里
案例:千万级用户的标签集合
某社交APP用 set 存储每个用户的兴趣标签,个别活跃用户的标签数量超过20万。直接遍历这个 set 时,Redis 主线程被阻塞了好几秒。解决方案是"分桶+哈希取模":
- 确定桶的数量:根据最大元素数量决定,比如每个桶最多存1万条数据,20万条就分20个桶。
- 计算桶编号:用 CRC32 算法对用户ID取模,保证同一个用户的标签分布在同一个桶里(如果需要保证顺序,用 hash_mod 时要考虑一致性)。
- 修改数据结构:把 set user:123:tags 改成 set user:123:tags:0 到 set user:123:tags:19,每个桶最多1万条数据。
进阶操作:用分片集群的路由规则优化
如果用的是 Redis Cluster,大 key 会被分配到固定分片上,分桶后可以让不同的桶分布在不同分片,比如每个桶的 key 加上分片标识(user:123:tags:0:shard1),不过这种方法需要和集群架构深度结合,建议在架构设计阶段就考虑大 key 问题。
(三)业务层面优化:从源头减少大 key 的产生
- 分页处理:比如用户的消息列表,别把所有历史消息都存到一个 list 里,改成按页存储,用 list:user:123:page:1、list:user:123:page:2,每次只取当前页的数据。
- 时效性控制:给大 key 设置合理的过期时间,比如临时缓存的大集合,用完就自动删除,别让它一直占着内存。
- 数据归档:像电商的历史订单,超过半年的可以归档到数据库或文件存储,Redis 里只存最近三个月的常用数据。
四、实战案例:从崩溃到稳定,我们是怎么搞定大 key 的
背景:某直播平台的礼物排行榜
直播间的礼物排行榜用 zset 存储,每个直播间一个 key,里面存了所有送礼用户的分数,个别热门直播间的 zset 成员超过50万。每天晚上高峰期,存储排行榜的 Redis 节点频繁触发 OOM,导致整个集群不可用。
治理过程:
- 第一步:定位罪魁祸首 用前面提到的 Python 扫描工具,发现 room:123:gifts 这个 zset 有67万成员,ZRANGE 操作平均耗时200ms,远超 Redis 单次操作1ms的正常水平。
- 第二步:分桶+冷热分离
- 按送礼时间分桶:最近1小时的实时数据存在 room:123:gifts:hot,1-24小时的数据存在 room:123:gifts:warm,超过24小时的归档到数据库。
- 每个桶限制成员数量:hot桶最多存1万条(只保留最新的1万条实时数据),warm桶按小时分桶(room:123:gifts:warm:2025041010 表示2025年4月10日10点的数据)。
- 第三步:优化查询逻辑 原来的业务直接查整个 zset 取Top100,现在改成先查 hot 桶和最近24个 warm 桶,合并后再取Top100。虽然多了几次 ZUNIONSTORE 操作,但每个 zset 的成员数量都控制在1万以内,操作耗时降到了10ms以下。
- 第四步:监控与预警 用 Prometheus + Grafana 监控每个 zset 的成员数量,设置预警:当单个 zset 成员超过8000时触发报警,同时监控内存碎片率(mem_fragmentation_ratio),当超过1.5时自动触发大 key 扫描。
治理效果:
- 内存使用率从95%降到60%,OOM 再也没出现过。
- CPU 负载从平均80%降到20%,因为处理小集合的速度快了很多。
- 业务查询延迟从200ms降到15ms,用户刷新礼物榜再也不卡顿了。
五、避坑指南:这些大 key 相关的坑,千万别踩!
1. 别迷信"大 key 一定是坏事"
有些场景下,合理的大 key 反而更高效。比如存储一个1MB的图片二进制数据,虽然是大 key,但比拆分成多个小 key 更节省内存(每个 key 本身有元数据开销,Redis 中每个 key 大约占1KB内存)。所以治理大 key 要结合业务场景,不能一刀切。
2. 批量操作时注意"管道"的使用
用 pipeline 批量处理小 key 没问题,但处理大集合时别滥用管道。比如用管道执行100次 HGETALL 一个有10万字段的 hash,会导致客户端内存飙升,因为所有结果会一次性返回。正确的做法是分批次处理,每次处理1000个字段,或者用 HSCAN 渐进式扫描。
3. 集群迁移时的大 key 陷阱
当需要给 Redis 集群扩容时,大 key 的迁移会导致源节点和目标节点之间产生大量网络流量。比如一个10MB的大 key 迁移,需要先在源节点序列化,通过网络传输,再在目标节点反序列化,这个过程可能会阻塞两个节点的主线程。建议在低峰期迁移,并且对大 key 单独处理(比如先删除,迁移后再重新生成)。
4. 监控要关注这几个关键指标
- used_memory:超过物理内存80%就该警惕了。
- mem_fragmentation_ratio:大于1.5说明内存碎片太多,需要清理或重启(仅单节点有效,集群节点重启要谨慎)。
- blocked_clients:如果这个值经常大于0,说明有慢操作阻塞主线程,很可能是处理大 key 导致的。
六、总结:防患于未然,比事后救火更重要
回顾这次治理经历,最大的感悟是:大 key 问题就像房间里的大象,刚开始觉得"存几个大集合没关系",等到出问题时已经积重难返。
最好的办法是在项目初期就建立规范:
- 开发阶段:设计数据结构时预估元素数量,超过1万的集合类数据强制分桶。
- 测试阶段:用压测工具模拟大 key 场景,比如用 redis-benchmark 测试 LRANGE 10万条数据的耗时。
- 上线阶段:部署自动扫描脚本,每天凌晨扫描大 key,发现异常及时报警。
- 迭代阶段:每次上线新功能,检查是否引入了潜在的大 key(比如新增的集合类存储)。
希望这篇文章能让你少走弯路,下次再遇到 Redis 崩溃,记得先查大 key——相信我,十有八九是它在搞事情。