☰
Redis大Key排查与治理:从性能刺客到彻底根治
2026/10/13 20:51:22 网站建设 项目流程

先交代一下背景。之前帮一个朋友所在的团队排查线上问题,业务侧一直报偶发超时,Redis 的 CPU 和内存看着都还算正常,但每隔一段时间就会出现一次几秒钟的“卡死”。查到最后,祸根是一把存在 Redis 里的排行榜数据——某个活动的实时榜单用了单个 ZSet 来存,活动上线后数据一路狂飙到上千万个成员,单个 key 的体积超过 800MB。就是这么一把大 key,在持久化、主从同步和定期清理的时候轮番折腾,把整个实例的响应延迟从毫秒级拖到数秒级。

这类问题在 Redis 运维里太常见了,社区里给它们起过一个名字叫“性能刺客”,平时不起眼,关键时刻一刀封喉。我自己在多个项目里都踩过类似的坑,也帮别人擦过不少次屁股,所以这一篇就把大 Key 的前世今生、致命危害和根治办法系统性地梳理一遍,既讲清楚原理,也给出可以直接照抄的排查步骤和处理方案。

1. 大 Key 到底是什么:判定标准与常见成因

1.1 什么样的 Key 算“大”

Redis 官方没有给过一个统一的大 Key 阈值,但在实际工程里大家基本默认一套参考标准:String 类型的 value 超过 10KB 就要警惕;集合类型(Hash、List、Set、ZSet)的元素数量超过 5000 个,或者整个 key 的序列化后体积超过 10MB,基本就要纳入大 Key 治理范围。

这个标准不算严苛,但也别教条。我见过有的团队把 1MB 以上的 key 就算大 Key,也见过游戏行业里一个排行榜 ZSet 存了上亿成员依然能扛得住,关键在于 key 对 Redis 实例的稳定运行是否构成了实质性威胁。换句话说,判断大 Key 不能只看体积和数量,还要结合访问频率、操作复杂度、持久化策略一起看。

最容易混淆的是大 Key 和热 Key。大 Key 指的是单个 key 的体积或包含元素数量过大,比如一个 Hash 里塞了 200 万条用户信息;热 Key 指的是某个 key 被高频访问,哪怕它只是个很小的 String。两者可以独立存在,也可以同时出现——一把又大又热的 key 才最要命,既占空间又抢 CPU 还堵塞网络。

1.2 大 Key 是怎么长出来的

大 Key 不是一天形成的,基本都是业务代码里几个典型坏习惯日积月累的结果。

最常见的是把 Redis 当成了“万能存储”。比如用户维度的数据,本应该拆成多个 Hash 分片存储,结果有人图省事,用一个 Hash 把全量用户数据都塞进去,字段越来越多;或者用 List 做消息队列,消费者逻辑有 bug,消息只进不出,队列一路膨胀到几百万条。

另一种典型场景是统计类数据。排行榜、签到记录、浏览历史这种增量型数据,如果没有及时清理过期成员,或者 TTL 压根没设,就会一路增长。像 ZSet 做排行榜,活动越火爆,成员数增长越快,等大促结束再去看,这个 ZSet 可能已经到了几百 MB。

还有一种隐蔽的写法:字符串拼接大 value。比如用 String 存一份全量配置、缓存一个超大 JSON 报文,或者把一批数据经过序列化后强行压进一个 key。这种 key 创建的时候可能不大,但业务迭代过程中不断往 JSON 里加字段,value 体积缓慢膨胀,等到爆炸时已经很难通过代码层面快速救火。

1.3 为什么不能放任不管

有人会觉得,Redis 本来就是把数据放在内存里的,大 Key 顶多多占点内存,能出什么事?这是最大的认知误区。内存占用只是大 Key 最低成本的危害,真正的风险在于它会在不知不觉中拖垮整个实例,而且很多危害是延迟爆发式的,短时间定位不到根因。

2. 大 Key 致命危害的四个机制

2.1 阻塞主线程:单线程模型的死穴

Redis 是单线程执行命令的,这是它高性能的根基,也是大 Key 问题最致命的放大器。所有操作在 CPU 核心上排队执行,一旦某个命令耗时过长,后面所有的命令都跟着等。

最容易触发阻塞的是集合类型的全量操作。比如对一个包含百万成员的 Hash 执行 HGETALL、对一个超大 Set 执行 SMEMBERS,这些命令的时间复杂度是 O(N),N 越大耗时越长。在一把 800MB 的 ZSet 上执行 ZRANGE 全量查询,耗时可能直接飙升到几百毫秒甚至秒级。这一两秒内,整个 Redis 实例上的所有读写请求全部排队,业务端的表现就是突然超时、连接堆积,接着引发雪崩。

删除大 Key 也一样危险。DEL 命令本身就是 O(N) 操作,删除一把 200 万字段的 Hash,Redis 主线程可能要卡顿几百毫秒到一秒多。很多人线上出事就出在清理数据的那一下,跑完 DEL 之后整个集群的延迟曲线瞬间拉满。

2.2 内存与网络的双重压力

大 Key 对内存的影响不只是“占地方”这么简单。当 value 达到数十 MB 级别时,Redis 会在内存分配器(如 jemalloc)中申请连续的大块内存,频繁的分配和释放会导致内存碎片率升高,used_memory 看起来不高但 RSS 居高不下,内存碎片率冲到 1.5 以上。继续膨胀还可能触发 swap,性能直接断崖式下跌。

网络层面,大 Key 的一次读取可能产生几 MB 甚至几十 MB 的响应数据。如果这个 key 被大量客户端同时访问,瞬间就能打爆出口带宽,造成所有请求的响应时间集体恶化。很多时候问题不在 Redis 本身,而是大 Key 把网络链路堵了,连带着拖累了跑在同一网络环境下的其他服务。

2.3 持久化与主从同步的隐藏风险

大 Key 对 RDB 持久化和主从同步的危害是最容易被忽略的。RDB 快照在生成时会遍历所有 key,大 Key 需要被完整序列化写入文件,这一个 key 的序列化时间可能占据整个快照耗时的一大部分。快照期间如果主线程还在接受写入,两次快照之间的时间差会被拉长,一旦发生宕机,数据丢失的窗口就变大了。

更麻烦的是主从同步。Redis 的主从复制在首次全量同步时要传输 RDB 文件,一把几百 MB 的大 Key 会让 RDB 文件体积变得很大,全量同步的时间会拖得很长。同步期间如果网络抖动,从库反复断开重连,每次都要重新全量同步,结果就是从库长时间无法对外提供服务,主库还要额外承担传输大文件的带宽和 CPU 开销。

AOF 重写也会受牵连。AOF 重写需要遍历内存数据生成新的写入命令,对大集合类型要分批遍历并重写所有元素,大 Key 的存在会明显拉长重写时间,期间产生的磁盘 I/O 和 CPU 开销也会上升。

2.4 数据倾斜:集群模式下的放大效应

在 Cluster 集群模式下,数据会根据 key 的 CRC16 哈希结果分配到不同的 slot,slot 再分布到不同节点。大 Key 的特征是单 key 体积巨大,不管怎么分布,它只能存在某一个节点上。这就导致了数据的严重倾斜:一个节点内存快满了,其他节点还很空闲;一个节点的网络流量和 CPU 负载特别高,其他节点却很闲。

因为 Redis Cluster 是按 key 做数据分片的,大 Key 无法通过增加节点来分摊压力,扩容对大 Key 问题几乎无效。我见过最典型的情况是集群 6 个节点,其中 1 个节点的内存使用率到了 85%,触发内存告警,另外 5 个节点都在 40% 以下。排查下来,罪魁祸首就是一把塞了几百万条数据的 Hash,业务方还以为是集群容量不够,差点白买了一堆机器。

3. 实战排查:四招定位大 Key 的准确姿势

3.1 第一招:使用 redis-cli 内置的 bigkeys 扫描

Redis 自带的 redis-cli 提供了一个非常方便的定位命令:

redis-cli --bigkeys

这个命令会以游标方式遍历整个实例的 key,分别统计 String、List、Set、Hash、ZSet 五大类型中体积最大的 key,并输出每个类型中最大 key 的 key 名、体积和元素数量。

实际执行效果类似这样:

-------- summary ------- Sampled 1284 keys in the keyspace! Total key length in bytes is 12453 (avg len 9.70) Biggest string found '"user:profile:88231"' has 1048576 bytes Biggest list found '"queue:order"' has 89123 items Biggest hash found '"hot:search:20250312"' has 522188 fields Biggest set found '"tags:aid_103"' has 37829 members Biggest zset found '"rank:game:42"' has 1200004 members

这个命令胜在简单,不需要额外安装工具,登录服务器就能跑。但它也有明显的局限:返回的只是每个类型里的“最大” key,如果实例里有多个大 Key,你只能看到最大的那一个;另外它需要遍历全量 key,在 key 数量几千万的实例上会比较消耗 CPU,建议在业务低峰期执行。

3.2 第二招:scan + debug object 精准定位

如果你需要把所有疑似大 Key 都捞出来,或者 bigkeys 命令不够细,可以用 scan 配合 debug object 来做更精确的扫描。

先写一个简单的循环脚本,使用 scan 游标遍历所有 key,对每个 key 执行 debug object 获取序列化长度:

redis-cli -a <password> --scan --pattern '*' | while read key; do echo "$key $(redis-cli -a <password> debug object "$key" 2>/dev/null | grep -o 'serializedlength:[0-9]*' | cut -d: -f2)" done

serializedlength 单位是字节,这个值可以粗略反映 key 在内存中占用的空间大小。但要注意两点:serializedlength 不是 key 的真实内存占用,它只是序列化后的长度;debug object 对超大 key 本身不产生阻塞,但逐条执行大量命令会带来网络开销,建议在测试环境或业务低峰期操作。

这种方法最大的价值是可以把输出结果排序,一次性得到所有大 Key 的清单,而不仅仅是最大的一两个。实际操作中我一般会配合 awk 做个简单的大小判断,只输出超过指定阈值的 key,减少噪音。

3.3 第三招:MEMORY USAGE 获取真实内存占用

如果 serializedlength 不能满足精度要求,需要用更接近真实内存占用的指标,可以使用 MEMORY USAGE 命令:

redis-cli -a <password> memory usage user:profile:88231

返回结果如下:

(integer) 1049088

MEMORY USAGE 返回的是 key 及其 value 在内存中实际占用的字节数,比 serializedlength 更接近真实值。返回的值会包含 Redis 内部数据结构的一些额外开销,但用于横向对比哪些 key 大、哪些小,已经足够。

这个命令的代价是,它需要遍历集合类型的全部元素来统计内存,对超大 key 执行时也会有一定的耗时。在定位阶段,一般先用 bigkeys 缩小范围,再对疑似对象执行 MEMORY USAGE 确认,避免盲目大量调用。

3.4 第四招:离线分析与监控告警

上面的方法都要求连到线上实例执行,在核心生产环境要谨慎。更稳妥的方式是从备份文件入手做离线分析。

如果你开启了 RDB 持久化,可以把最新的 dump.rdb 文件下载到本地,然后用开源工具 rdbtools 来解析:

pip install rdbtools rdb -c memory dump.rdb > memory.csv

rdbtools 会输出每个 key 的内存占用、元素数量等信息,生成 CSV 文件后,可以用 pandas 或者简单的 Excel 排序筛选,快速定位所有大 Key。整个过程不影响线上实例,是最安全的一种排查方式。

除了离线分析,线上监控也不能缺。Redis 的 INFO 命令里有几个跟内存相关的指标,比如 used_memory,但这些都是实例级的,看不到单个 key 的情况。想做到大 Key 的持续监控,一般需要引入额外的采集程序,定期用 scan 扫描并上报 key 的规模和体积,或者接入云数据库的 Redis 控制台,利用平台自带的大 Key 分析功能。

4. 根治指南:从临时处理到结构改造

4.1 应急处理:安全地删除或淘汰大 Key

发现大 Key 后,最紧急的一件事不是立刻删掉,而是用安全的方式删。直接执行 DEL 在 key 很大的时候会阻塞主线程,等于自己把枪口对准了自己。

Redis 4.0 及以上版本提供了 UNLINK 命令,这是 DEL 的异步版本:

redis-cli unlink user:profile:88231

UNLINK 会先在主线程中把 key 从 key 空间断开,真正释放内存的动作交给后台线程异步完成,主线程不会被阻塞。这个命令在 4.0 之后基本可以无脑替换 DEL,但对更老版本,只能分批删除集合中的元素来降低阻塞时间。

比如清空大 Hash 可以这样分批操作:

redis-cli hscan user:profile:88231 --count 1000 # 拿到一批 field 后,用 hdel 依次删除 redis-cli hdel user:profile:88231 field1 field2 field3 ...

每批删除几百到几千个元素,中间稍微间隔一下,让主线程喘口气。记住一个原则:宁可多跑几轮,也绝不在大 Key 上直接执行 O(N) 命令。

如果这个大 Key 只是一次性的脏数据,删除后不再依赖,UNLINK 是最干净的方式。如果还要保留数据但暂时不访问,可以设置较短的 TTL 让它自然过期。但这里也有坑:Redis 的过期删除在 4.0 之前的版本是主线程同步删除,大 Key 过期时一样会卡;4.0 之后可以通过配置 lazyfree-lazy-expire yes 开启异步过期,避免过期瞬间阻塞主线程。

4.2 通用方案:拆分与分片

大 Key 的根治手段,核心思想就是“拆分”。把一个把大集合拆成多个小集合,让单 key 的体积降到安全范围。

以 Hash 为例,常见的拆分方案是取模分片。假设原来的 key 叫 user:data,现在把它拆成 N 个分片,分片规则可以是 user:data:{hash(user_id) % N},N 的取值取决于单个分片目标大小。计算逻辑很简单:如果预估总数据量是 500 万字段,希望每个分片不超过 5 万字段,那就拆成 100 个分片左右。

对应代码大体是这样的:

SHARD_COUNT = 100 def get_key(user_id: str) -> str: shard_id = hash(user_id) % SHARD_COUNT return f"user:data:{shard_id}"

读取的时候用 HSCAN 或者 HGETALL 遍历所有分片再合并结果,写入的时候落在对应的分片上。这样单个 key 的体积被严格控制住,删数据也可以按分片粒度操作。

ZSet 类排行榜数据的拆分思路类似,但要注意排序范围的获取会变复杂。如果业务允许接受近似排行,可以对 ZSet 做时间分片,比如按天拆分成 rank:20250312、rank:20250313,查询时先查当天分片,历史数据走离线聚合;如果必须精确全量排行,可以考虑把高频区间(前 100 名)在缓存层维护一个小的 ZSet,定期从大 ZSet 聚合更新。

4.3 给 value 瘦身:压缩与序列化

不是所有大 Key 都需要拆分,如果数据结构本来就必须是完整的单个 value,压缩是另一种有效手段。

之前有团队遇到过一个 20MB 的 String key,里面存的是一个很大的 JSON,读写频率都很高。他们做的优化有两步:先用更紧凑的序列化格式替换 JSON,比如 MessagePack 或者 ProtoBuf;再对个别体积特别大的字段做压缩,比如把用户头像 URL 列表这种重复度高、可压缩性强的数据用 Gzip 或 Snappy 压缩后写入。改造后单 key 体积从 20MB 降到 3MB 左右,网络传输时间明显缩短。

压缩要付出的代价是 CPU。Redis 主线程要做压缩和解压,所以压缩率越高的算法 CPU 开销通常也越大。建议在访问频率不是特别极端的场景使用,并且优先选择 Snappy、Zstandard 这类追求速度的算法,不要为了极致压缩率选 Gzip 而牺牲性能。

4.4 结构性替换:用更适合的数据结构

有些大 Key 问题的根源不是数据量,而是数据结构选得不对。

比如用 List 做消息队列,消费者速度跟不上生产速度,导致 List 持续膨胀。这种情况根本不应该用 List,Redis 5.0 推出的 Stream 数据结构更适合:它支持消费者组、消息确认、死信队列,还能通过 MAXLEN 参数限制消息长度,老消息会自动淘汰,防止队列无限增长。

又比如,很多人喜欢把一个大 JSON 塞进 String,但业务实际上只关心其中的两三个字段。这种情况用多个小 String 缓存,固然要做一次逻辑改造,但收益是每个 key 都很小,更新单个字段时不需要重写整个大 value。

4.5 关于 TTL:别把所有 Key 都设成永不过期

很多大 Key 之所以失控,根源就是没有设置 TTL。Redis 的 key 一旦创建,如果没有 TTL,就会永远占着内存,只增不减。

建议在写入业务数据时严格遵守缓存规范:可重放的数据设置合理的过期时间;不可丢失的数据,要评估是否需要放进 Redis,而不是把 Redis 当成持久化数据库。数据量的增长速度要在上线前做评估,预留好冗余,不能等到内存被吃满才去补救。

5. 从灭火到防火:大 Key 的长效治理机制

5.1 上线前的准入规范

大 Key 问题的成本,越早发现越低。最好的治理时机是代码评审和上线前检查阶段。

我所在团队的做法是,在 Redis 操作封装层加入大 Key 检测逻辑。所有写操作在写入前会估算 value 大小,超过阈值直接告警并阻断;所有集合类型的写入,会周期性检查 key 的元素数量变化,如果增速异常,自动通知负责的开发者。封装层检测跟业务代码解耦,不需要每个业务方自己实现,成本最低、效果最好。

对于无法在代码层拦截的历史包袱,可以做一个周期性巡检任务,每天凌晨低峰期跑一次全量扫描,输出大 Key 清单,推动相关团队整改。巡检结果归档到文档,每次大促前再专项复查一遍,确保存量问题不会死灰复燃。

5.2 监控告警的落地姿势

监控告警不能只盯实例维度的 CPU 和内存,单个 key 维度的数据才更有价值。

对自建 Redis,可以写一个采集脚本,每隔 5 分钟执行一次 scan 加 debug object 扫描,把超过阈值的 key 信息写入监控系统,再做告警;对云数据库实例,一般控制台都自带大 Key 分析工具,可以直接订阅分析结果。

告警阈值不要设置得太激进,否则会被频繁的误报淹没。我的经验是先记录一周的基线数据,再根据基线上下浮动设置告警线,避免第一天就告警不断、第二天就没人看告警。

5.3 常见误区:为什么拆完了还会复发

我见过不少团队,处理大 Key 就是“发现一个删一个”,没过多久问题又回来了。核心原因通常有三类。

第一类是只清理了结果没治理源头。比如排行榜大 Key 是因为活动数据源源不断写入,如果业务逻辑里没有控制成员数量的机制,那么旧的大 Key 删掉了,新的数据几天之内又会形成一个新的大 Key。

第二类是拆分了,但拆分规则设计得不合理。比如分片数是固定的,随着数据增长,单片体积又涨到危险线了。这种问题要在设计之初就把分片数量定得足够大,并且预留二轮拆分的路由方案,让后续扩容不用重写读写逻辑。

第三类是代码层没有形成规范的约束。某个业务方的开发同学不知情,又写出了往单个 key 里塞全量数据的代码,导致新的大 Key 在另一个位置冒出来。这种问题只能靠准入规范和评审机制来解决,治标不治本的操作永远堵不住后续的增量风险。

5.4 我用过的几个实战避坑点

这里集中分享几个实操中容易踩的细节坑。

第一,UNLINK 不是万能的。UNLINK 只对内存回收做了异步化,如果这个 key 同时存在大量未完成的客户端阻塞操作(比如 BLPOP),或者遇到集群网络分区,UNLINK 的收益会打折扣。最稳妥的方式是在业务低峰期操作,操作前后都要观察实例延迟曲线。

第二,bigkeys 命令不要在生产高峰期跑。它要遍历全部 key,在百万级 key 的实例上会对 CPU 造成明显压力。我习惯的做法是提前写到定时任务里,跑完把输出追加到日志文件,白天直接看结果。

第三,对大 Key 做拆分时,要注意“短连接风暴”问题。如果原来一个 key 承载的流量在拆分后分散到几百个分片,客户端的连接数、命令数都会相应上涨,有时候 Redis 的单实例 CPU 反而会升高。拆分前要做压测,确认新的分片方案不会引发新的性能瓶颈。

第四,配置了 maxmemory-policy allkeys-lru 不代表万事大吉。在内存压力下,Redis 会按 LRU 淘汰 key,但如果是大 Key,淘汰时释放内存的动作同样可能造成阻塞(老版本)。建议在 4.0 以上版本中开启 lazyfree-lazy-eviction yes,把淘汰流程也异步化。

6. 一次真实的大 Key 事故复盘

想用一个简化版案例来做收尾,因为纯讲理论和命令总是不够直观。

某社交应用的一个排行榜功能,运营团队在周五晚上 8 点上线了活动,活动规则是把用户的点赞数实时写入一个 ZSet,key 名类似 rank:activity:hot。开发时没有做任何容量评估,ZSet 里只进不出。周六中午,线上监控显示 Redis 主节点 CPU 使用率从 20% 飙到 80%,延迟曲线出现周期性尖刺,部分接口超时率超过 15%。

排查过程:先看慢查询日志,发现有大量 ZRANGE 操作耗时在 800ms 以上;再用 bigkeys 扫描,定位到 rank:activity:hot 有接近 900 万成员,体积约 600MB;进一步用 MEMORY USAGE 确认单个 key 占用内存约 650MB,而整个实例 used_memory 大概 4GB,这个 key 一家就占了 16%。

处理方案分为三步。第一步紧急止血,在低峰期用 UNLINK 删除这个 key,并临时改为按小时生成多个小 ZSet,key 类似 rank:activity:hot:2025031212,高峰期 p99 延迟立刻回落到 50ms 以下。第二步优化数据结构,把实时排行逻辑改成两级方案:热数据在小 ZSet 中维护,超过 1 万成员就把尾部数据异步迁移到历史冷数据存储;前端只查询排名前 100,直接用固定 key 缓存。第三步建立长期规范,Redis 操作封装层增加大 Key 检测,超过阈值立即告警;所有集合类型写入必须设置容量上限,超限自动拒绝写入并引导走冷数据链路。

这个案例里最关键的动作其实是第一步的 UNLINK,不要小看这个命令,它把一次可能持续数秒的阻塞降到了毫秒级,为后续改造争取了时间。而真正的根治是第二步的数据结构设计,从源头杜绝了单 key 无限膨胀的可能性。

我个人在持续跟进这些案例后的体会是,大 Key 治理本质上不是一次性的运维救火,而是一套从代码规范到监控告警再到架构设计的系统工程。任何一个环节缺失,问题迟早都会换个马甲重新出现。如果你现在手上正好有 Redis 实例在跑,不妨先跑一次 bigkeys 看看输出结果,说不定第一个大 Key 就躺在那里等你处理。

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

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

立即咨询