某天大促复盘会上监控群里那张Redis CPU使用率的曲线图被反复点名——从晚上八点开始一路爬坡十点直接顶到接近100%同时段接口延迟P999从10ms飙到800ms而真正的数据库请求量其实没有明显增长。逐条翻慢日志清一色是几个超大Key的读写操作单个Hash里存了几百万个field一次HGETALL直接拖垮了网关线程池。这就是典型的Redis大Key隐患平时开发时写入很爽一到高并发或集群扩容场景就开始教做人。这篇内容会比较系统地梳理大Key的准确定义、它的杀伤力到底来自哪里、线上如何精确排查以及根治大Key的几种改造思路。不只是讲原理会带上我实际动过手的命令、脚本和踩坑记录。适合所有在生产环境使用Redis的开发者、运维和SRE参考也适合那些刚接手Redis集群、想提前排雷的同学。1. 先搞清楚什么样才算“大Key”1.1 判断标准不能只看value大小很多人以为大Key就是value很大的字符串比如一个5MB的JSON缓存。这确实算一种但远不是全部。一个Key是否“大”要从它占用的内存、包含的元素数量、以及一次操作的时间复杂度三个维度去看。以我实际运用的标准为例判断维度触发阈值典型对象value大小单个Key超过10KBString类型的大文本、序列化对象集合元素数量Hash/Set/ZSet超过5000个元素榜单、关注列表、评论列表集合内单元素大小单个元素超过1KBHash里存了大量长文本、Base64图片访问耗时单次操作超过100ms涉及大集合的排序、聚合类命令为什么把阈值定在10KB和5000个元素不是拍脑袋。Redis是单线程模型所有命令在主线程队列里依次执行一个耗时的命令会堵住后面所有命令。10KB左右的value走一次网络拷贝还能接受再大的话在千兆网络下serialize和transmit的耗时就会明显拉长而5000个元素的集合用SORT、ZRANGEBYSCORE这类命令处理时计算量和内存拷贝量已经可能让主线程卡顿几十毫秒。1.2 大Key的四大杀伤路径大Key真正的危害不只是“占内存”这么简单。它通过四条路径影响整个系统第一是阻塞主线程。Redis处理请求是单线程的一个大Key的操作比如HGETALL一个百万field的Hash或者LPOP一个几十万元素的List会让主线程持续工作几百毫秒甚至秒级。这个窗口内所有其他客户端请求都排队等待表现出来就是延迟瞬间拉高P99曲线出现尖刺。第二是引发网络拥塞。一个大Key响应体可能有几MB高并发下多个客户端同时读出口带宽会被打满。更隐蔽的是Redis集群的节点间同步、数据迁移也要走网络大Key会成为带宽钉子户拖累整个集群的正常数据流动。第三是造成内存碎片和倾斜。在Redis Cluster里数据按hash slot分布到不同节点。大Key经过CRC16计算后只会落到某个固定的slot这意味着某个节点可能比其他节点多出几个GB的内存负载。节点内存不均衡会导致集群容量利用率下降某个热点节点先满其他节点还空着。第四是阻塞持久化和主从同步。RDB快照和AOF重写需要fork子进程并复制内存页表大Key会让这个复制过程更长占用的临时内存更多。主从复制时如果大Key在快照传输中途被修改还可能引发全量同步进一步放大风险。1.3 大Key一般从哪里来排查经验告诉我大Key极少是故意写入的绝大多数是业务逻辑演进中的“漏网之鱼”。常见来源有这么几类缓存未设置TTL的累积数据。比如缓存一个用户的购物车一开始几十个商品没问题但如果把历史浏览记录也塞进同一个Key并且永不失效日积月累就是几十万条记录。滥用集合类型存储无限增长的数据。典型的是把日志、埋点数据直接LPUSH到同一个List Key以为Redis内存够大就无所谓结果这个List一路增长到GB级别。没有拆分的数据汇总。一个Hash Key下用field存放不同维度的统计数据或者一个ZSet存放全量用户排行榜数据量达到线上规模后单Key必然超重。这些都是设计阶段埋下的雷到了要扩容、要迁移、要优化的时候才集中引爆。所以排查和根治必须两手抓缺一不可。2. 排查大Key的实战方法2.1 先看慢日志最直接的线索排查大Key的第一步永远不是全量扫描而是翻慢日志。Redis的慢日志记录了执行时间超过阈值的命令它是最真实的事故残留证据。# 设置慢日志阈值为10毫秒并查看最近200条慢日志 CONFIG SET slowlog-log-slower-than 10000 SLOWLOG GET 200慢日志里能看到具体的命令、Key名称、执行耗时和时间戳。如果发现大命令集中在某个Key上定位就完成了一大半。注意一点慢日志只覆盖超过阈值的命令如果一条HGETALL都执行了500ms那它必然出现在慢日志里。但如果你的大Key只是占空间大、而平时没有被大命令访问慢日志可能完全看不到它。所以慢日志要作为第一排查手段但不能是唯一手段。2.2 使用官方工具做初步体检Redis自带的redis-cli有一个--bigkeys参数可以扫描整个实例并找出最大的Key。用法非常简单# 采样扫描全库找出最大的key redis-cli -h 127.0.0.1 -p 6379 --bigkeys这个命令会遍历整个Key空间按类型分别统计最大的String、Hash、Set、ZSet、List。不需要额外安装工具五分钟就能出报告适合日常巡检。但是要清楚它的局限。--bigkeys的扫描是基于SCAN的抽样不会读取每个Key的完整内容对于集合类型它统计的是元素个数而不是真实内存占用。更坑的是SCAN本身是增量迭代的如果某个大Key在扫描中途被删掉报告里可能完全体现不出它。所以它适合“摸个底”不适合作为唯一的依据。2.3 用SCAN配合MEMORY USAGE精确定位想要精确定位每个Key的真实内存占用需要手工组合SCAN和MEMORY USAGE两个命令。SCAN用来无阻塞地遍历KeyMEMORY USAGE用来查看指定Key的完整内存开销。# 查看某个key的精确内存占用包含key本身、value、编码结构等 MEMORY USAGE user:profile:10001MEMORY USAGE返回的是字节数可以通过RESTORE命令辅助深度分析但它需要传入完整的序列化数据所以实际使用中我们主要是做估算。下面是我日常巡检用的Python脚本遍历所有Key并输出内存占用排名前二十的清单import redis r redis.Redis(host127.0.0.1, port6379, db0, decode_responsesTrue) result [] cursor 0 while True: cursor, keys r.scan(cursorcursor, count1000) for key in keys: try: size r.memory_usage(key) result.append((key, size)) except Exception: pass if cursor 0: break result.sort(keylambda x: x[1], reverseTrue) for key, size in result[:20]: # 打印前20名单位换算成MB print(f{key} : {size / 1024 / 1024:.2f} MB)这个脚本在Key总数百万级别的实例上实测大概十几秒跑完不会阻塞业务。它的原理是每次SCAN只返回少量Key游标自动推进对主线程的影响非常小。2.4 从监控侧围堵延迟、CPU与网络除了主动扫描监控侧的数据也能提供大量线索。延迟类监控要关注Redis实例的latency指标如果出现周期性尖峰且尖峰时间与某个大Key的访问峰值吻合基本可以锁定目标CPU方面如果某节点CPU明显高于其他节点很可能是大Key正好落在该节点的hash slot上对比节点间的内存使用率也能及时发现倾斜。网络监控同样关键。案例中遇到过有一种情况某个大Key每秒被读取几百次出口带宽被打满。排查时翻监控才发现网络面板里出口流量曲线几乎是跟着这个Key的QPS走的而且其他正常请求被连带拖慢因为TCP通道被大包占满了。所以一份完整的排查报告应当把慢日志、bigkeys扫描、MEMORY USAGE精确统计、监控指标四类数据叠加起来交叉验证。只靠单一工具很容易漏判。3. 根治大Key的改造策略3.1 拆分从数据模型上解决发现大Key之后第一选择永远是改造数据结构而不是扩容机器。拆分方案要结合业务场景来定不是简单地切成几个小Key。最常用的是Hash分片。如果一个Hash Key里field太多按照field的某个维度比如用户ID、时间范围、业务类型把一个大Hash拆成多个小Hash。例如统计一个公众号文章每天的数据原来是一个hash存所有天数的数据field是日期字符串value是统计结果时间久了field数量会非常多。改造后按月份拆# 旧结构 STAT:ARTICLE:10001 - field: 20240101, 20240102, 20240103... # 新结构 STAT:ARTICLE:10001:202401 - field: 01, 02, 03... STAT:ARTICLE:10001:202402 - field: 01, 02, 03...每个分片内的field数量控制在几千以内单Key的读写压力直线下降。需要注意拆分后业务代码要处理多个Key的查询和聚合通常用pipeline批量获取或者把分片数量固定比如固定128个槽位避免无限制地增加Key数量。List分片适用于消息队列、日志列表等场景。比如一个存用户操作日志的List按天存成不同Key每天一个再设置TTL七天后自动过期这样既控制了大Key膨胀也顺带解决了数据清理问题。ZSet分片更多用在排行榜场景。如果榜单数据量极大可以按分数区间拆比如0-100分、100-200分各存一个ZSet查询时分别取再合并。或者按用户ID的哈希值固定分到N个桶中每个桶一个ZSet。3.2 压缩与编码优化有些大Key没法拆或者拆了收益不大这时候可以考虑压缩value的体积。Redis对不同类型有内部编码优化比如String类型在长度小于44字节时使用embstr编码Hash、List、Set、ZSet在元素数少或元素体积小时会用压缩链表ziplist编码。同一份数据用ziplist存储比hashtable存储内存占用能减少50%以上。现实中更常见的优化手段是业务层压缩。缓存对象在写入Redis之前先做一次序列化和压缩import json, zlib def set_cache_with_compress(redis_client, key, obj): # 先序列化成JSON字符串再gzip压缩最后写入 serialized json.dumps(obj, ensure_asciiFalse) compressed zlib.compress(serialized.encode(utf-8), level6) redis_client.set(key, compressed) def get_cache_with_decompress(redis_client, key): compressed redis_client.get(key) if compressed is None: return None return json.loads(zlib.decompress(compressed).decode(utf-8))压缩级别选6是性价比比较高的压缩时间和压缩率取得了平衡。对于重复性高的JSON数据压缩率能到70%以上但对随机性强的数据比如已经加密的结果压缩率有限要提前压测确认收益。对于value体积本身就很大的String类型还有一个思路是改造为Hash类型并拆分field。例如缓存一个用户的完整画像原来是整个JSON对象存为String现在改成Hash每个field存一个信息维度。这样更新单个维度时不需要读取整个大对象同时也能利用Hash本身的内部编码节省内存。3.3 删除大Key的正确姿势某些情况下不需要保留大Key直接删掉就行。但直接DEL一个几百MB的Key会造成主线程阻塞因为DEL需要释放内存并处理对应的数据结构。这时候应该用UNLINK命令让Redis在后台异步回收内存# 异步删除不阻塞主线程 UNLINK user:history:10001UNLINK在Redis 4.0之后可用原理是把Key从键空间摘除后内存释放操作放入后台线程执行。如果使用的是Redis 3.x版本可以用CONFIG SET lazyfree-lazy-user-del 参数控制DEL的异步行为但最稳妥的做法是升级Redis版本。还有一个坑需要强调删除大Key时即使用了UNLINK如果这个Key正在被热点访问删除动作本身会导致缓存击穿大量请求瞬间打到数据库。先摘流量、再删除、再重建缓存顺序不能乱。3.4 治理机制把大Key问题前置根治大Key的最后一块拼图是预防。可以在三个层面建立防线。在写入层面封装Redis客户端工具类在set操作前检查value长度超过预设阈值直接报警或者拒绝写入。MAX_VALUE_SIZE 10 * 1024 # 10KB def safe_set(redis_client, key, value): value_len len(value) if isinstance(value, (str, bytes)) else 0 if value_len MAX_VALUE_SIZE: raise ValueError(fkey {key} value size {value_len} exceeds limit) return redis_client.set(key, value)在巡检层面把前面提到的Python扫描脚本做成定时任务每周扫描一次全实例Key内存排名生成报告推送到运维群。阈值可以按实例规格调整小规格实例1MB就算大Key大规格实例可以放宽到10MB。在容量层面对每个大Key设置合理的TTL避免永久Key无限累积。同时让业务方在需求评审时明确单个Key的数据规模上限把隐患消灭在设计阶段。4. 典型故障排查实录4.1 案例一Hash大Key拖垮集群迁移某次集群做在线扩容新增了两个节点迁移过程中有个分片的数据一直迁不完报错的日志里频繁出现“Cluster state changed, reloading...”的警告。查看慢日志发现有一个Hash Key的HSCAN操作耗时2秒多。顺着Key名称查到业务这是一个商品维度的销售统计Hashfield是日期字符串value是当日销售额。运营系统每五分钟写入一次跑了两年多field数量到了惊人的五十多万个。当时不是立刻删除而是做了两步先用UNLINK把旧Key异步删掉这是数据冗余丢失可以重建再在代码里改成按月拆分Hash。迁移完成后节点间的数据量变得均衡HSCAN耗时从2秒降到几毫秒。复盘时有个细节值得注意迁移前我们用了bigkeys扫描报告里压根没看到这个Hash。原因就是它字段多但每个value只有不到100字节在内置扫描的“最大集合”维度里排不上Top但真实内存占用和操作代价都是巨大的。MEMORY USAGE全量扫描才把它揪出来。4.2 案例二String大Value拖慢全站接口另一回是某应用接口在高峰期响应变慢但慢日志里看不到任何针对Redis的慢命令。后来发现是网关的所有出站流量都压在了一条链路上网络监控显示这个Redis实例的出网流量已经跑到网卡上限。翻流量构成查到一个device:status这个Keyvalue是设备全量状态列表序列化后的JSON有3MB多。客户端每5秒轮询一次高峰期几十台游戏机同时轮询一下子就把带宽吃满了。处置思路不再让客户端拉全量数据改为服务端计算增量推送给设备。Redis里的大Value改成多字段Hash每个设备一个field一次性读取变成按需拉取。同时把推送间隔从5秒改为15秒。接口P99从800ms回落到30ms。这里踩过一个坑最初尝试单纯压缩value3MB压到500KB带宽消耗降了但没有根治问题。因为压缩和解压也要消耗CPU而且在高并发下频繁压缩反而让CPU先成为瓶颈。最终确定压缩只作为辅助手段分拆量和增量推送才是正解。4.3 案例三ZSet大Key导致内存倾斜一个在线答题积分排行榜ZSet的score是用户积分member是用户ID。活跃用户超过千万时这个ZSet单Key占用超过2GB并且固定落在某一个节点的某些slot上。观察监控发现这台节点的内存使用率比集群其他节点高出40%频繁接近内存上限。改造方案其实不复杂按积分段拆分成多个ZSet。0-1000分一个1000-5000分一个5000分以上一个。查询Top排行时先确定目标分数段落在哪个分片再只读对应分片查询跨分段时可并查多个分片然后内存合并。实际落地后每个ZSet的元素数量控制在几万级别单Key内存占用降到几十MB节点间内存也基本均衡了。这个案例有一个教训排行榜这类数据天然高频访问big key的TopN查询如果不定时清理长期不活跃用户ZSet会持续膨胀。我们在改造时加了逻辑每两周把积分小于阈值且长时间未活跃的用户从ZSet中移除只保留活跃榜单。业务上排行榜只需要前天开始的积分数据历史数据归档到离线存储。4.4 常见问题速查表场景典型症状定位方法根治手段String大ValueCPU拉高、带宽打满、响应延迟尖刺MEMORY USAGE精确统计拆分/压缩/增量推送Hash field过多节点内存倾斜、迁移超时全量SCANMEMORY USAGE按维度分片HashList无限增长Redis内存持续上涨、大Key扫描Top--bigkeys初步扫描按天/按业务拆KeyTTLZSet元素过多排行榜接口P99升高、节点内存不均衡慢日志大Key扫描按分数段分片、清理非活跃数据删除大Key阻塞删除瞬间大量命令超时慢日志查看DEL耗时改用UNLINK异步删除这个表看着简单实际排查的时候一定要结合业务上下文。我见过一个团队把所有大Key全部删掉的抢救方案结果导致缓存全空、数据库被压垮比大Key问题更严重。任何根的治方案都要做足够的业务评估小步改造、逐步灰度。5. 写在最后一点日常运维的小建议排查和治理大Key这件事越早介入越省事。开发阶段最好就约定好单Key大小规范不同数据类型的滥用要能在code review阶段被拦下来如果已经线上跑出大Key优先拆数据模型压缩是辅助删Key是保底。最后分享一个我在日常巡检时常用的小技巧在扫描脚本里加上“单Key内存环比增长率”这个指标两次扫描之间内存翻倍增长的Key要重点关注它下一周就可能发展成大Key。把这个指标写进报警规则后我的值班手机消停了不少。与隐患赛跑的本质上不是处理故障的能力而是提前发现问题的那份敏感。