线上 Redis 频繁崩溃?这套大 key 治理方案请收好

数据库 Redis
现在稍微大点的项目都用 Redis 集群,假设你用的是分片集群(比如 Codis、Redis Cluster),一个大 key 会被固定分配到某个分片上。想象一下,其他分片内存使用率才50%,就这个分片像吹气球一样涨到90%,整个集群的负载均衡瞬间失效。

兄弟们,凌晨两点,手机突然像地震一样狂震,我迷迷糊糊摸到床头一看,运维群里炸了锅:"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,适合做初步排查。

# 扫描所有数据库,每隔0.1秒扫描一次
redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1
  • 1.
  • 2.

2. 更精准的方案:自己写扫描工具(附Python代码)

如果需要全量扫描,就得用 SCAN 命令代替 KEYS *,因为 KEYS 会阻塞主线程,在生产环境用就是"自杀行为"。下面这段 Python 代码可以扫描指定前缀的大 key,支持设置字符串长度阈值和集合元素数量阈值:

import redis
def scan_big_keys(redis_client, prefix, str_threshold=1024*1024, collection_threshold=10000):
   big_keys = []
   cursor = '0'
   while cursor != 0:
       cursor, keys = redis_client.scan(cursor=cursor, match=prefix + '*')
       for key in keys:
           type_ = redis_client.type(key)
           if type_ == 'string':
               length = redis_client.strlen(key)
               if length > str_threshold:
                   big_keys.append((key, 'string', length))
           elif type_ in ['hash', 'list', 'set', 'zset']:
               count = 0
               if type_ == 'hash':
                   count = redis_client.hlen(key)
               elif type_ == 'list':
                   count = redis_client.llen(key)
               elif type_ == 'set':
                   count = redis_client.scard(key)
               elif type_ == 'zset':
                   count = redis_client.zcard(key)
               if count > collection_threshold:
                   big_keys.append((key, type_, count))
   return big_keys
# 使用示例
redis_client = redis.Redis(host='localhost', port=6379, db=0)
big_keys = scan_big_keys(redis_client, 'user:')
for key, type_, size in big_keys:
   print(f"大key: {key}, 类型: {type_}, 大小: {size}")
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

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。
// 压缩数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
gzipOutputStream.write(userJson.getBytes());
gzipOutputStream.close();
byte[] compressedData = byteArrayOutputStream.toByteArray();
redisTemplate.opsForValue().set("user:123", compressedData);

// 解压缩数据
byte[] data = redisTemplate.opsForValue().get("user:123");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream);
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream));
StringBuilder decompressedJson = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
    decompressedJson.append(line);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 按需拆分:把常用字段(比如用户名、头像)和不常用字段(比如三年前的订单)分开存储。比如用 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万条数据。
// 计算桶编号
long userId = 123;
int bucketCount = 20;
int bucketId = (int) (userId % bucketCount);
String bucketKey = "user:" + userId + ":tags:" + bucketId;
// 添加标签
redisTemplate.opsForSet().add(bucketKey, "tag1", "tag2");
// 遍历所有桶
for (int i = 0; i < bucketCount; i++) {
   String key = "user:" + userId + ":tags:" + i;
   Set<String> tags = redisTemplate.opsForSet().members(key);
   // 处理每个桶的数据
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

进阶操作:用分片集群的路由规则优化

如果用的是 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,导致整个集群不可用。

治理过程:

  1. 第一步:定位罪魁祸首 用前面提到的 Python 扫描工具,发现 room:123:gifts 这个 zset 有67万成员,ZRANGE 操作平均耗时200ms,远超 Redis 单次操作1ms的正常水平。
  2. 第二步:分桶+冷热分离
  • 按送礼时间分桶:最近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点的数据)。
  1. 第三步:优化查询逻辑 原来的业务直接查整个 zset 取Top100,现在改成先查 hot 桶和最近24个 warm 桶,合并后再取Top100。虽然多了几次 ZUNIONSTORE 操作,但每个 zset 的成员数量都控制在1万以内,操作耗时降到了10ms以下。
  2. 第四步:监控与预警 用 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. 开发阶段:设计数据结构时预估元素数量,超过1万的集合类数据强制分桶。
  2. 测试阶段:用压测工具模拟大 key 场景,比如用 redis-benchmark 测试 LRANGE 10万条数据的耗时。
  3. 上线阶段:部署自动扫描脚本,每天凌晨扫描大 key,发现异常及时报警。
  4. 迭代阶段:每次上线新功能,检查是否引入了潜在的大 key(比如新增的集合类存储)。 

希望这篇文章能让你少走弯路,下次再遇到 Redis 崩溃,记得先查大 key——相信我,十有八九是它在搞事情。 

责任编辑:武晓燕 来源: 石杉的架构笔记
相关推荐

2021-07-08 10:04:36

人工智能AI主管

2022-11-03 08:56:43

RediskeyBitmap

2024-12-02 01:16:53

2020-02-10 16:07:42

工具包

2023-05-03 20:53:48

2018-05-18 09:18:00

数据分析报告数据收集

2024-05-23 07:59:42

RedisKey性能

2020-05-26 13:45:46

Python函数字符串

2021-05-13 23:39:19

勒索软件攻击数据泄露

2023-12-13 09:08:26

CPU性能分析Linux

2024-05-29 12:47:27

2023-02-10 18:32:21

项目管理实践

2020-07-02 09:55:32

运维架构技术

2022-01-17 18:21:09

数据库社交引流

2020-09-21 09:00:41

Docker架构容器

2022-06-20 15:19:51

前端监控方案

2020-07-23 14:13:04

运维架构技术

2020-12-25 10:20:53

Linux网络命令
点赞
收藏

51CTO技术栈公众号