
先交代一下背景。之前帮一个朋友所在的团队排查线上问题业务侧一直报偶发超时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 万字段的 HashRedis 主线程可能要卡顿几百毫秒到一秒多。很多人线上出事就出在清理数据的那一下跑完 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 哈希结果分配到不同的 slotslot 再分布到不同节点。大 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) doneserializedlength 单位是字节这个值可以粗略反映 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) 1049088MEMORY 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.csvrdbtools 会输出每个 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:88231UNLINK 会先在主线程中把 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 fuser: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 持续膨胀。这种情况根本不应该用 ListRedis 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 点上线了活动活动规则是把用户的点赞数实时写入一个 ZSetkey 名类似 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并临时改为按小时生成多个小 ZSetkey 类似 rank:activity:hot:2025031212高峰期 p99 延迟立刻回落到 50ms 以下。第二步优化数据结构把实时排行逻辑改成两级方案热数据在小 ZSet 中维护超过 1 万成员就把尾部数据异步迁移到历史冷数据存储前端只查询排名前 100直接用固定 key 缓存。第三步建立长期规范Redis 操作封装层增加大 Key 检测超过阈值立即告警所有集合类型写入必须设置容量上限超限自动拒绝写入并引导走冷数据链路。这个案例里最关键的动作其实是第一步的 UNLINK不要小看这个命令它把一次可能持续数秒的阻塞降到了毫秒级为后续改造争取了时间。而真正的根治是第二步的数据结构设计从源头杜绝了单 key 无限膨胀的可能性。我个人在持续跟进这些案例后的体会是大 Key 治理本质上不是一次性的运维救火而是一套从代码规范到监控告警再到架构设计的系统工程。任何一个环节缺失问题迟早都会换个马甲重新出现。如果你现在手上正好有 Redis 实例在跑不妨先跑一次 bigkeys 看看输出结果说不定第一个大 Key 就躺在那里等你处理。