“每日面试题分享155:Redis中的Big Key问题是什么如何解决”这个话题我在面试里被问过不下五次自己也拿它问过不少候选人。前几天做技术复盘翻到半年前一次线上事故记录恰好就是Big Key引发的接口雪崩。趁这个机会把Big Key从原理到排查再到处理完整梳理一遍。不管你是准备面试还是正在被线上大key折磨这篇文章应该都能帮上忙。Redis是单线程处理命令Big Key的杀伤力恰恰被这个特性放大了。简单说Big Key就是某个key的value太大或者集合类型的元素太多大到单个命令在它身上执行时会长时间阻塞整个Redis。你炒菜的时候锅铲被卡住了后面所有菜都没法下锅就是这个感受。1. 先搞清楚Big Key到底是什么别把判断标准搞错很多刚接触Redis的人会把Big Key简单地理解成“占用内存大的key”。这个说法不算错但不严谨。判断Big Key光看内存占用还不够还要看单个key内部的数据结构复杂度。有些key总内存不大但里面装了十几万个元素一条命令要遍历一遍照样能把Redis拖垮。1.1 一个key算不算Big Key通常看这几个维度业界最常用来量化Big Key的标准有三个第一个是value的长度。对String类型来说单个key的value超过10KB就要开始警惕了超过100KB基本可以断定是Big Key。这个阈值不是官方硬性规定而是大量生产实践总结出来的经验值。第二个是集合类型List、Hash、Set、ZSet里元素的个数。超过5000个元素操作时的耗时就会明显上升超过10万就是非常危险的大key了。第三个是key整体的内存占用单key超过10MB就要赶紧处理。这个指标一般配合MEMORY USAGE命令来确认。注意判断Big Key时“元素个数”往往比“内存大小”更关键。一个Hash里有5万个字段每个字段只有几个字节总内存不大但一次HGETALL就要遍历5万个字段照样卡住主线程。我自己在看代码的时候还会习惯性地看读写频率。同一个key如果是一个低频冷数据就算大一点影响也有限。但如果是一个热点key写入量和读取量都很大那它的大小阈值就要降得更低。Big Key的问题从来不是“它有多大”而是“它在被操作时会造成什么后果”。1.2 什么样的业务场景最容易养出Big Key从我接触过的线上案例来看Big Key的产生基本离不开三种情况第一种把不适合放进Redis的大对象硬塞进去。比如直接把整个订单详情对象序列化成JSON字符串一层嵌套套一层一个value几MB。再比如把图片base64编码存Redis这种操作看着方便实际上是在埋雷。第二种集合类型只进不出越积越多。最典型的就是粉丝列表、商品评论、用户操作日志、消息队列。用List做消息队列时不断LPUSH消费方处理不过来又没做清理列表里堆积几百万条数据这个key就废了。我见过一个用户行为埋点系统把当天所有行为记录直接LPUSH进同一个key跑了一个月之后光这一个key就有将近两千万个元素整个Redis实例的持久化时间直接被它拉长了数倍。第三种定时任务或脚本一次性写入大量数据。比如每天凌晨同步一批数据到Redis直接写进一个key里。平时没人注意等数据量涨到一定程度某一次操作触发了全量读取问题就爆发了。还有一种比较隐蔽的情况是key设置了过期时间但写入频率远高于过期频率结果就是值越来越大一定要警惕这种“边写边过期但总量仍在涨”的场景。判断Big Key从来不是只看数字而是要去理解这个key在业务里是干什么的、怎么被操作的。搞清楚了这两个问题你才能给“多大”下一个合理的结论。2. 为什么Big Key是定时炸弹——危害比你想的更严重面试里问Big Key的危害大多数人能说出一两句“阻塞”“超时”。但如果你能把这几个危害背后的技术机制讲清楚面试官对你的印象会完全不一样。2.1 单线程模型下阻塞是致命的Redis 6.0之前处理命令的主线程是严格单线程的。即便到了6.0开启了多线程I/O真正执行命令的环节仍然是单线程。这意味着任何一条命令执行时间过长后面排队的命令就全部等着。一个几MB的String执行DEL命令删除它需要多长时间很多人以为是毫秒级实际上在内存分配器需要回收连续内存块时耗时可以达到几十毫秒甚至上百毫秒。删除一个100万元素的Hash耗时可能超过一秒。这一秒里整个Redis实例上所有读写请求全部排队包括那些本来只需零点几毫秒就能完成的GET、SET。我复盘过的那次事故就是这样的流程某个排行榜key因为活动流量暴增ZSet里攒了上百万个成员。凌晨的清理脚本执行DEL删除前一天数据删除过程持续了一秒多期间大量请求超时。超时的请求触发客户端重试重试的流量又涌进来直接把服务打挂。这里要特别强调不只是DEL以下这些命令在大key上执行时都特别危险HGETALL一次性取出Hash全部字段数据量大时耗时极长。SMEMBERS返回Set所有成员复杂度O(N)。LRANGE 0 -1取出List全部元素。ZRANGE全量范围同理。KEYS这个我就不说了任何情况下都不要随意在生产环境用。还有SUNION、SINTER这种集合运算遇到大key也是灾难。2.2 客户端超时、网络拥堵与持久化层面的连锁反应就算你不主动执行上面这些命令Big Key在网络传输和持久化环节造成的危害也同样棘手。大key返回给客户端时一个几MB的value要打包成网络数据包发送出去。Redis的带宽是有限的如果同时有几个大key在传输直接打满网卡。客户端拿到数据后还要做反序列化处理耗时也上去了。在网络条件不稳定的场景下大key很容易触发客户端读取超时。持久化方面RDB做快照时主进程要fork出一个子进程这里用的是写时复制机制。如果fork之后有大key被修改了主进程就需要复制整个内存页内存页越多fork的开销越大。极端情况下RDB生成期间主进程内存增长明显触发内存淘汰甚至OOM。AOF重写也是同样的道理大key会让重写后的文件体积剧增重写过程占用的时间和内存都不可控。2.3 集群场景下大key还会制造“热点分片”Redis Cluster使用CRC16对key进行哈希分配到16384个槽位中。一个key经过哈希之后只能落在某一个节点上。大key所在的槽位无论是内存、带宽还是CPU消耗都会远超其他槽位造成明显的节点倾斜。在扩缩容和数据迁移时槽位迁移是按key粒度迁移的。迁一个大key迁移时间被拉长不说迁移期间该key所在节点还要正常处理请求整体稳定性大打折扣。如果恰好这个节点资源吃紧迁移行为本身就可能引发连锁故障。还有一个容易被忽略的点Big Key会拖垮慢查询监控。排查线上问题时如果你在慢查询日志里看到一个执行1秒的HGETALL但日志上只写了命令和耗时你根本不知道是哪条数据出了问题。等你一步步排查定位到这个key的时候影响可能已经扩散了。3. 线上怎么发现Big Key——这几招从快到准排列发现Big Key是解决Big Key的前提。很多团队直到线上出故障了才去排查这是最被动的做法。最好是在日常巡检中就定期扫描把大key消灭在萌芽期。3.1redis-cli --bigkeys最快的扫雷方式但别完全相信它Redis自带的redis-cli --bigkeys命令是最便捷的排查工具用法非常简单redis-cli -h 127.0.0.1 -p 6379 --bigkeys它会以SCAN的方式遍历整个实例统计出每种数据类型里最大的key。输出大概长这样-------- summary ------- Sampled 100000 keys in the keyspace! Total key length in bytes is 3120000 (avg len 31.2) Biggest string found so far user:profile:10086 has 12034 bytes Biggest list found so far queue:order has 305000 items Biggest hash found so far counter:uv:20240601 has 50001 fields Biggest set found so far blacklist:ip has 23000 members Biggest zset found so far rank:hot has 880000 members这个命令上手很快但有三点一定要心里有数。第一它统计的是元素个数而不是真实内存占用。一个Hash有10万个字段但每个字段都很小可能总内存远小于一个1万字符串的key但从“大key风险”角度前者更危险。第二它只针对key本身数据量进行统计不会告诉你哪些key占用了最多内存总量。第三它本身是用SCAN遍历的不会像KEYS那样阻塞但如果Redis实例本身压力很大跑这个命令依然会带来额外开销。建议低峰期执行。3.2MEMORY USAGE和DEBUG OBJECT确认目标精准测量redis-cli --bigkeys能帮你缩小范围但要确认某个key具体的真实内存占用就得用这两个命令了。# 查看一个key的真实内存占用包含key本身、value和内存碎片等 127.0.0.1:6379 MEMORY USAGE user:profile:10086 (integer) 18656 # 查看序列化长度不包含内存碎片主要用来比较大小 127.0.0.1:6379 DEBUG OBJECT user:profile:10086 Value at:0x7f8c0e4180a0 refcount:1 encoding:raw serializedlength:12034 lru:562214 lru_seconds_idle:10MEMORY USAGE返回的是内存分配器实际分配的空间更接近真实占用。DEBUG OBJECT里的serializedlength是序列化之后的长度适合用来横向比较哪些key更大。注意DEBUG OBJECT在Redis 7.0之后的版本里部分字段有调整但serializedlength一直保留着。要批量排查哪些key比较大可以写成脚本用SCAN遍历所有key再对候选key执行MEMORY USAGE来判断是否需要处理redis-cli -h 127.0.0.1 -p 6379 --scan --pattern * | while read key; do size$(redis-cli -h 127.0.0.1 -p 6379 MEMORY USAGE $key) if [ $size -gt 10485760 ]; then echo $key $size fi done这个脚本遍历的是全库key线上如果实例比较大建议加上--count参数控制单次SCAN返回的条数不要一次拉太多。更稳妥的方式是用SCAN的游标循环逻辑在应用里跑。3.3 慢日志、监控告警和可视化工具同样重要除了主动扫描被动排查的手段也不能落下。慢查询日志里如果频繁出现HGETALL、LRANGE 0 -1这类命令就要高度怀疑背后有Big Key。Redis慢日志用SLOWLOG GET查看127.0.0.1:6379 SLOWLOG GET 5如果你的Redis部署在云上阿里云、腾讯云这些云厂商都自带大key分析工具能直接在控制台上看到所有key的大小分布和类型分布比自己写脚本方便很多。开源的可视化工具也有不少比如RedisInsight用可视化界面对key进行分析适合中小团队快速搭一套日常巡检。监控告警的指标建议重点关注这几个used_memory、慢查询数量、实例平均延迟和p99延迟。一旦某个实例的延迟指标出现周期性抖动配合慢日志就能很快定位到是不是大key在作祟。4. Big Key的解决方法——从止血到根治发现Big Key之后处理方案要根据业务场景选择不是所有大key都适合直接删除。这一节按照从“紧急止血”到“长期根治”的顺序来梳理。4.1 紧急情况下可用UNLINK替代DEL但别滥用Redis 4.0引入了UNLINK命令它和DEL的区别在于UNLINK先在主线程中把key从键空间中移除然后异步释放value占用的内存主线程不需要等待内存回收完成阻塞时间被大幅缩短。127.0.0.1:6379 UNLINK user:profile:10086 (integer) 1这是处理大key最直接的止血方案。实测下来删除一个80MB的HashDEL在主线程阻塞了接近1秒而UNLINK几乎无感。但要记住UNLINK只是把阻塞点转移了异步删除期间Redis的内存并没有立刻释放内存占用指标短时间内可能还会维持原样。可用内存吃紧的场景下用完UNLINK之后要盯一段时间内存变化。Redis 6.0之后有两种特殊的“大key”删除是不走UNLINK异步路径的一种是包含大量元素的紧凑编码类型另一种是被标记为“主动过期”的key集合。这些虽然不多见但面试如果聊到这里能提出来会显得很深。4.2 拆分大key推荐的分桶、时间维度与业务维度策略拆分Big Key是根治方案里最常用的手段。拆分的核心思路就一句话把一个大key变成很多个合理大小的key让操作只影响其中一个分片。以Hash为例假设你现在有个hash:user:fans里面存了某个大V的几千万粉丝ID。这种场景下的查询模式通常是“查某个用户是否关注了这个大V”或者“分页查粉丝列表”完全不需要一次性把所有粉丝都取出来。可以按用户ID的哈希值取模拆成多个Hash// Java伪代码按uid哈希分桶 int bucket Math.abs(uid.hashCode()) % 100; String key hash:user:fans: bucket; boolean isFan jedis.hget(key, String.valueOf(uid)) ! null;这样每个Hash里的元素数量就只有原来的百分之一。分桶数怎么定我的经验是先预估这个大key最终可能达到的元素总量再除以5000前面提到的安全元素数量阈值得到最小分桶数然后取2倍左右的富余量。比如预计最终有1000万粉丝分200个桶每个桶最高5万操作复杂度已经完全可接受了。还有一种更通用的拆分思路是按时间维度拆分。比如日志、流水、排行这类数据天然有时间段概念可以按天或按小时拆key。rank:20240601、rank:20240602这样每天的数据独立存储清理和统计都方便。4.3 大value的改造压缩、精简和更换数据结构如果大key不是因为元素多而是因为单条value太大就要从序列化方式入手了。常见的做法有第一精简字段。想清楚业务到底需要哪些字段去掉冗余字段把一次存全量的大JSON改成只存核心字段的小JSON。有些团队习惯把接口返回的完整报文直接缓存进Redis这种偷懒的写法是Big Key的重灾区。第二压缩value。用Snappy、LZ4、Zstd这类高性能压缩算法对value做压缩后再写入。压缩能大幅度降低存储体积和网络传输量代价是读写时多了压缩和解压的CPU开销。对于value动辄几十KB的场景压缩带来的网络收益远高于CPU损耗。我在业务里用Zstd压过一批日志类数据体积能降70%以上。第三换数据结构。比如存用户的最近浏览记录如果只要最近50条用List一直LPUSH然后LTRIM截断而不是无限往里面加。再比如需要“是否存在”判断的场景用Set需要带分数的排行榜用ZSet这些本身就是为特定业务设计的选对结构能避免不少人为制造的大key。4.4 预防方案监控告警、过期策略和代码规范的组合拳处理Big Key更大的价值在于预防。我之前踩过的最大的坑就是直到故障发生了才去做扫描实际上完全可以在事态恶化之前就拦截。第一代码规范层面。写入Redis之前先判断value大小超过阈值就拆分或换方案。这个判断可以写在公共底层封装里所有业务方使用Redis时都先过一层检查。比如我司的缓存框架就强制要求写入前计算value.length超过10KB就打日志超过50KB直接拒绝写入。第二过期策略层面。能设置TTL的key尽量设置尤其是一些临时数据和过程数据别图省事就永久保存。Redis的内存淘汰策略也要提前配置好合理选择淘汰算法如allkeys-lru、volatile-ttl避免内存打满时发生不可控的全量逐出。第三监控告警层面。单key内存、集合元素数量、慢查询数量、主线程耗时等指标都要接监控。我在生产环境接的告警规则是单key内存超过10MB会触发P2告警超过50MB触发P1告警。慢查询超过100ms就同步通知到相关负责人。上线一周之后基本就不会再出现新增的Big Key了。5. 面试怎么答Big Key——从定义到实战一气呵成面试官问这道题的时候表面上考的是“知不知道Big Key”实际上考的是你有没有真正处理过类似问题。下面这套回答思路可以帮你把零散的知识点串联起来。5.1 一句话说清定义再按“危害—发现—处理—预防”展开面试时我的建议是先用一句话给Big Key下定义然后按逻辑顺序往下讲不要只背几个零散的点。定义Big Key是指Redis中某些key的value过大或者集合类key中元素数量过多导致在操作这些key时主线程被长时间阻塞进而影响整个Redis实例的稳定性。之后按这个顺序展开危害单线程阻塞导致请求排队网络带宽被打满持久化期间内存和CPU开销增大集群场景下节点内存倾斜。发现定期用redis-cli --bigkeys扫描用MEMORY USAGE确认结合慢日志和监控识别高风险命令。处理UNLINK异步删除紧急止血分桶、拆分、压缩、精简value根治设置过期策略和监控预防。预防写入前校验、合理设计数据结构、监控告警前置。这套链路走下来回答逻辑清晰信息密度也够比干巴巴背几个命令强太多。5.2 追问一“你们线上真遇到过吗具体怎么排查的”这个问题特别容易被问住。建议提前准备一个自己经历过的案例如果实在没有也要能完整地描述一个典型场景的排查过程。我常用的回答范本是“有次线上接口超时率突然升高排查链路大致要经过三次跳转。第一步先在Redis的慢查询日志里看到一条HGETALL执行了800多毫秒命令本身存在但慢日志里没有key信息。第二步用MONITOR命令短暂监听命令详情确定具体访问的是哪个key。第三步对目标key执行MEMORY USAGE发现这个Hash里有超过100万条字段。最后根据业务场景选择了按UID分桶的方式将一个大Hash拆成200个Hash读写路径只访问其中一个分片超时问题解除同时增加了Big Key监控规则。”这里面有个关键技巧慢查询日志和监控只能帮你定位到命令定位具体key往往需要用MONITOR。但是MONITOR在高峰期使用会放慢Redis整体吞吐所以要短时间使用、快速定位不要长时间开着。5.3 追问二“如果负责的系统里发现大key但业务不允许拆分怎么办”这个追问很实际。有些key结构复杂牵一发动全身短期确实不好动。这时候有几个中间状态可以处理。第一个方案如果只是删除时不阻塞可以直接用UNLINK替换DEL至少先止血。第二个方案如果读请求超根可以在应用层面对该key增加本地缓存避免每次请求都穿透到Redis。第三个方案如果是集合类的key可以在业务空闲时用SSCAN、HSCAN、ZSCAN分批转移数据把大key逐步“瘦身”成一个新key然后用RENAME切换对业务的影响最小。第四个方案如果热点key本身还伴随高并发读可以考虑在Redis前面加一层进程内缓存用多级缓存架构扛住流量而不是让大key频繁被压。这些方案可以组合使用核心思路是不一定要“立刻彻底解决”但要“保证影响可控”。5.4 追问三“拆分成多个key之后原来一次拿完的数据现在要多次读取怎么保证性能”拆分之后确实面临多读的问题但这道题的本质是用可接受的额外读取次数换主线程的稳定性和整体可用性。具体方案取决于业务模式。如果原来的操作是HGETALL整表获取拆包后需要并行读取多个分片再合并返回。这里可以用pipeline批量提交命令配合JUC的CompletableFuture并发聚合一次请求的额外延迟一般能控制在几毫秒以内。如果业务允许轻微延迟本地缓存热点分片也能大幅减少读取次数。如果确实需要实时全量数据且调用频繁那应该先考虑这个场景是否适合用Redis而不是继续硬撑大key。记住一个核心矛盾Redis单线程模型的性能优势是用“单条命令足够快”换来的。你所有的设计都应该围绕“让每条命令在毫秒内完成”这个目标展开拆分本质就是在为这个目标服务。6. 我在实操中踩过的坑和总结出来的经验最后分享几个我在处理Big Key过程中真实踩过的坑这些在文档里很难找到但比文档内容更值钱。第一个坑千万别在流量高峰期跑redis-cli --bigkeys。虽然它内部用的是SCAN不是KEYS但要遍历全库的key对于大实例来说依然会产生不小的CPU开销而且会缩短主从复制的心跳间隔极端情况下可能引起主从切换。建议在凌晨低峰期跑或者用云厂商提供的大key分析工具让平台帮你扫。第二个坑UNLINK删除了大key不代表万事大吉。异步删除后Redis内存释放是有延迟的我曾经删完一个大keyused_memory还在高位维持了将近一分钟。要留意有没有触发内存淘汰策略的边界条件别在内存打满的状态下盲目删。第三个坑不要忽略内存碎片化。大key被删除或拆分后Redis内存分配器产生了大量碎片即使数据量小了used_memory也不一定会降到很低。想彻底回收碎片最好主动执行MEMORY PURGE仅限主从架构或在维护窗口执行重启。第四个坑监控告警阈值不要一上来就设得太小。我刚开始给单key设了1MB告警结果每天几百条告警业务方直接麻木。阈值要比业务正常模型大一些比如10MB或元素数超过5万再告警同时配合“增长速率”的监控比如一个key一周内涨了多少比单纯的大小告警更有价值。第五个坑系统里如果存在大量小key但分布特别分散虽然不算严格意义上的Big Key但批量操作和内存碎片一样会造成延迟。排查时要会分辨别只盯着“大”的看小key过多也要小心。Big Key问题的本质是“一条命令的执行成本和它的收益严重不匹配”。处理思路永远是能拆就拆能删就删能预防就预防。面试里把这个逻辑讲清楚比背再多命令都管用。当然对我来说最核心的一条经验是线上发生问题前解决问题的成本远低于故障之后再补救的成本。把这个意识带到实际业务里比掌握一百个Redis命令都有意义。
阅读完成 · 觉得有帮助?