☰
Redis Big Key排查与优化:从定义、发现到预防的完整实战指南
2026/10/7 4:53:08 网站建设 项目流程

今天聊一个Redis面试题里出场率特别高的家伙——Big Key。这个题在“每日面试题分享”系列里已经排到第155篇了,但每次拿到新的对话,还是有不少人把它讲浅了。很多人张口就是“大key就是value很大的key”,然后没了。我当年第一次被问到“那你遇到Big Key怎么处理”,也差点没接住。这里说的Big Key,不只是某个key占的内存大,它背后牵出的是一整套问题:单线程阻塞、网络带宽打满、集群数据倾斜、删除时卡顿、持久化文件膨胀。对准备面试的同学来说,这一题几乎是面试官区分“用过Redis”和“背过Redis”的分水岭。这篇我按自己的实战经验,把Big Key从定义、发现、处理到面试话术,完整捋一遍,争取你看完就能直接拿去用。

1. Big Key到底怎么定义,面试别只说“值很大”

1.1 面试题的第一层:什么算Big Key

Redis官网上其实没有一条“超过XX字节就是Big Key”的硬规定,但业界在实践中形成了一套比较统一的参考标准。我自己平时判断主要看两条:

  • String类型的value超过10KB,就要开始警觉了。
  • Hash、List、Set、ZSet这类集合型key,元素个数超过5000个,或者整体编码后的数据量超过10MB,就要重点排查。

这个标准不是我拍脑袋定的,它和Redis的底层编码、网络模型都有关联。Redis 3.2之后,集合类默认在元素少的时候会用压缩编码存储,比如ziplist、intset,性能和内存都很好。但元素一多,就会升级成hashtable、skiplist,每个操作的时间复杂度虽然仍然是O(1)或O(logn)级别,可一个key里存几十万field,执行HGETALL这种一次取全部的命令,Redis要把所有数据遍历一遍再序列化扔给网络,这个动作是极慢的。

我们在面试的时候,除了说出这些数字,最好再加一句:“判断标准不能只看绝对值,核心是看这个key的读写耗时、内存占用、网络传输大小是否已经影响了Redis的正常服务。”这句话能直接把你的层次拉上去。面试官问的其实不是“10KB哪里来的”,而是你到底懂不懂Redis的单线程工作方式,懂不懂慢查询的本质是什么。

1.2 这些Big Key到底是怎么来的

我排查过不少线上问题,Big Key的来源其实很有规律,基本逃不出下面这几种。

第一类是“数据无脑累积型”。最常见的就是一个Hash里面存用户行为数据、埋点日志,业务方为了查询方便,把所有数据往同一个key里塞,日积月累,field数量从几千涨到几十万。还有一个典型是List,用来做消息队列的,消费者消费速度跟不上生产速度,list越积越长,等发现的时候已经几百万条了。

第二类是“序列化不当型”。有些同学把大对象直接序列化成字符串塞进Redis。比如把一整页搜索结果塞进一个String,或者把一张base64编码的图片塞进去,动辄几百KB甚至几MB。这种key在读写的时候,Redis主线程要处理的时间就长,网络传输也要占很大带宽。

第三类是“设计阶段没有容量评估”。这种在小团队特别多。当时设计缓存结构的时候,没想过这个key以后会存多少数据,或者根本没预估过QPS。上线三个月后,数据量上来了,big key也就出现了。

我强调这个,是因为在面试里,被问到“Big Key为什么产生”,你如果能分这几类讲出来,并且举一个自己实际开发中的例子,比单纯背“因为存的数据太多”要有说服力得多。面试官很吃“你见过、你踩过”这一套。

1.3 为什么Big Key这么危险

很多人不理解,就觉得Redis不是号称几万QPS吗,一个key大点又能怎样。实际上Big Key的破坏力是系统性的,我从几个维度拆开讲。

  • 阻塞Redis主线程。Redis是单线程执行命令,一个操作如果耗时几百毫秒,后面排队的几千个请求都得等待。比如对一个几百万元素的List执行LRANGE 0 -1,或者对一个几十万field的Hash执行HGETALL,Redis会花大量时间遍历和序列化,这段时间内所有其他请求都卡着。
  • 网络拥塞和带宽打满。大key一次get返回的数据包可能达到MB级别,在高并发下瞬间把内网带宽打满,影响同集群其他业务。
  • 集群数据倾斜。在Redis Cluster中,key按slot分布,一个大key的slot会占很大内存,其他节点内存可能只用了10%,这个节点已经90%了,很容易触发内存上限,导致整个集群容量没法均匀利用。
  • 删除操作造成长时间停顿。这个最坑。直接DEL一个大key,Redis要释放对应的内存,如果key内部是个大的hashtable,释放成千上万个元素也是耗时的,期间主线程照样卡。4.0之前这个问题没有好的解法,4.0之后有了UNLINK异步删除才缓解。
  • 持久化影响。RDB做快照时,需要把大key完整写入文件,fork子进程后的写时复制,如果大key被修改了,就会复制整页内存,导致内存翻倍。AOF重写也是同理,大key会让持久化文件膨胀得厉害。

面试的时候你不需要把这些全背出来,但你至少要能说出“阻塞、网络、倾斜、删除卡顿”这四个关键词,并且能稍微展开一两个。这一小节的内容,其实就是回答“Big Key有什么危害”这个追问的最佳素材。

2. 一线排查:怎么把Big Key揪出来

2.1 最快的方法:redis-cli --bigkeys,但别被它误导

很多文章一说到发现Big Key,第一反应就是redis-cli --bigkeys。这个命令确实方便,它在Redis内部用的是SCAN命令渐进式遍历所有key,而不是KEYS *,所以不会阻塞线上实例。执行完之后,会给你一份报告,列出扫描过程中发现的最大String、最大List、最大Hash等。

那为什么我说“别被它误导”?因为这个命令是抽样扫描,不是精确统计。它每扫到一定数量的key,就从中挑出当前最大的那个记录,扫描结束后展示的是“采样中的最大”,而不是全局精确最大的key。如果你的大key在扫描采样区间里没有被选中,它就会被漏掉。另外,它只能告诉你每种类型最大的那个key,如果线上存在20个大key,它可能只给你报一个。

所以我的建议是,--bigkeys适合做快速粗筛,发现问题后不要停在这里,要继续用精确手段确认。比如命令里还能看到每个大key的内存估算值,但这个估算对Hash这种复杂结构也不一定准,它看的是内部编码的字节数,跟实际内存占用会有偏差。

2.2 想精确看某个key有多大

拿到疑似目标之后,可以用MEMORY USAGE命令看精确内存占用(Redis 4.0+)。这个命令会计算key和value的整体内存占用,包括Redis对象头、底层数据结构、dictEntry等所有开销。示例:

redis-cli MEMORY USAGE user:profile:1001

返回的是字节数。如果返回(nil),说明这个key不存在。要注意的是,MEMORY USAGE对于很大的聚合结构,本身也会有耗时,但因为它是内存计算,不涉及网络传输,通常比直接访问快很多,在低峰期跑是安全的。

还有一个老命令是DEBUG OBJECT key,它会返回这个key的编码方式、序列化长度、lru时间等信息。比如:

redis-cli DEBUG OBJECT user:photo:88

你会看到类似Value at:0x... refcount:1 encoding:raw serializedlength:1234567 lru:...这样的输出,其中serializedlength是序列化后的长度,可以大致推断value的大小。DEBUG OBJECT在超大key上使用时要小心,它本身有阻塞风险,最好只在低峰期执行。

2.3 真正可用的生产环境扫描方案

如果线上集群有几百上千个key,一个一个MEMORY USAGE显然不现实。我更推荐自己写一个巡检脚本。核心思路就是用SCAN增量遍历key,然后对不同类型的key做不同的“大小估算”:

  • String类型,用STRLEN命令看value长度。
  • Hash类型,用HLEN看field数量。
  • List类型,用LLEN看元素数量。
  • Set类型,用SCARD看成员数量。
  • ZSet类型,用ZCARD看成员数量。

把这些数量指标跑出来之后,按之前说的阈值过滤,超过的再单独用MEMORY USAGE做精确确认。这套方案的好处是不会阻塞Redis,而且能找到所有可疑key,而不是只找“最大”的那一个。

还有一个隐藏技巧:如果开启了慢查询日志,可以通过SLOWLOG GET查看耗时特别高的命令,很多慢命令背后基本都是Big Key在作怪。比如你发现一条HGETALL执行了200毫秒,顺着这个key去查,基本就能揪出来。慢日志是线上排查大key最省力的线索来源,很多同学不知道用。

线上执行这些扫描操作时,务必选在业务低峰期。同时尽量避免直接连主节点,优先连从节点跑,把影响降到最低。这些执行细节,在面试过程中讲出来,会很有“实战感”。

3. 处理Big Key的正确姿势,关键是别把线上搞挂

3.1 集合类大key的拆分思路

发现Big Key后,最主流的处理方式就是拆。拆的思路说白了就是“把一个大key变成多个小key”。

比如业务里用Hash存用户画像,结构是user:profile:{userId},里面有几十万用户的画像数据。你可以按业务维度拆,也可以按用户维度拆。按用户维度拆最简单粗暴:原来是user:profile:batch1,现在改成user:profile:userId:xxxx,每个用户一个小key。访问的时候业务侧多拼一个userId就行了。这种拆分需要注意的是一致性,写的时候也要按同样规则扩散到多个key,不然拆完只有读改了写没改,反而出问题。

Hash还可以按field做二次哈希分片。比如user:hobby:1001里存了用户1001的所有关注关系,有几十万条。你可以把它拆成user:hobby:1001:{0}到user:hobby:1001:{9}这10个key,存之前对field的key做一次哈希,取模之后落到对应的分片上。查询的时候只需要知道这个哈希规则,就能定位到正确的分片key。这个思路在面试里很加分,因为它不是单纯的“拆”,而是有设计在里面。

List类型的大key也类似。一个几十万元素的队列,要么用LTRIM定期裁剪历史数据,要么就按业务类型拆成多个队列,比如queue:order、queue:pay,避免所有消息挤在一个List里。

3.2 压缩、编码、数据结构层面的优化

不是所有场景都需要拆,也可以从“如何存”这个角度做优化。

如果你是Java后端,直接往Redis塞对象的时候,用的序列化方案很关键。JDK原生序列化出来的一坨东西很大,换成Kryo、Protobuf之后体积能小很多。有的团队还会开启value压缩,比如在业务层先用Gzip压缩一下再写入Redis,读的时候再解压。这里有个代价:压缩和解压会消耗CPU,如果读写QPS特别高,压缩性价比可能不高。我做过一个场景,value是一段JSON文本,Gzip压缩后从20KB变成3KB左右,读取的时候解压一次大概几毫秒,对整体QPS影响不大,但内存和带宽都省了一大截。这个取舍要在面试里讲清楚,说明你不是无脑压。

还有一种情况是用错数据结构。比如把大量UUID放进一个Set,其实这种去重集合如果只需要判断存在与否,可以考虑用布隆过滤器,内存占用小很多。再比如,有些人用String存一个标志位,却存了完整对象,这种属于编码意识问题,不是Redis的问题。面试如果被问到“你有没有优化过存储”,这些都可以作为案例。

3.3 删除大key的正确时机和方式

该删的大key还是得删,但“怎么删”是有讲究的,这里坑特别多。

Redis 4.0之后提供了UNLINK命令,它和DEL最大的区别是:DEL是同步删除,删除大key会阻塞主线程;UNLINK是异步删除,主线程先把key从命名空间中摘掉,回收内存在后台线程慢慢做。所以删除大key第一原则就是:不要用DEL,用UNLINK。

redis-cli UNLINK user:profile:batch1

如果Redis版本低于4.0,有一种曲线救国的办法:对需要删除的大key先设置一个极短的过期时间,比如EXPIRE key 1,让Redis自动过期删除。但这里也有隐患,过期删除在4.0之前同样是同步操作,一样会卡。所以最稳的老版本方案是利用LREM、HDEL、SREM这类命令,分批移除元素,等元素少了再DEL。这个方案虽然慢,但至少不会让Redis卡死。

另外,Redis 4.0之后还有一组lazy free配置:

lazyfree-lazy-expire yes lazyfree-lazy-server-del yes lazyfree-lazy-user-del yes

把过期删除、隐式删除、用户删除都设置成异步,能规避不少删除卡顿问题。不过要留意,异步删除意味着内存释放慢一点,在内存压力大的实例上,配置会出现释放延迟,需要结合监控观察。我遇到过开启lazy free后,内存短期内不降反升的情况,其实是后台线程在慢慢回收,一般几秒内就会恢复正常,不用慌。

4. 面试官的连环追问,你怎么接

4.1 从“怎么治”到“怎么防”

面试到了这一步,通常候选人已经能说出Big Key是啥、怎么发现了,但能不能拿高分,看的其实是预防手段。面试官常问的是“你现在知道它有问题,那你从开始设计的时候怎么避免它出现”。

我一般会从三个层面回答。第一是业务设计阶段,估算单个key的规模,明确“所有String value控制在10KB以内,集合型key元素控制在5000以内”这种硬规矩,超过就必须拆分。第二是代码review阶段,把HGETALL、LRANGE 0 -1这种一次拉全量的命令列成黑名单,开发人员如果敢写,review直接打回。第三是监控阶段,用前面说的巡检脚本定期扫描,并且把大key的发现接入告警,比如发现一个Hash超过5000个field,就推送告警给责任人。

这三个层面答出来,面试官基本就满意了,因为他看到的不再是一个被问题推着走的开发,而是一个有主动治理意识的人。这也是“经验”和“背题”的核心区别。

4.2 一个真实的生产事故复盘

说到预防,我忍不住讲一个我实际遇到的案例。有一年我做活动系统,某个活动页的人数统计用了Redis的Hash,key是stat:activityId,field是userId,value是用户操作次数。活动上线第一天数据量还正常,第二天冲到几万人,这个Hash的field数瞬间涨到十几万。当时最先出问题的是监控,Redis内存占用明显比同集群其他节点高,形成数据倾斜。然后活动页开始变慢,查了下慢日志,全是HGETALL这个命令,一次执行几百毫秒。当时我第一反应不是去改代码,而是先用HLEN确认了这个key的规模,然后带着业务同学做了一个紧急方案:先单独把这个大key“冻结”,新写入的数据落到新的临时key上,接着在低峰期用UNLINK把它删掉,最后再把统计逻辑改成按userId分桶存储,也就是每个用户一个独立field的粒度改成按日期+用户分片。

这个案例里我想强调的是“先止血再优化”的思路。遇到线上出问题,你不能立即跑去改代码,因为改代码要发布,发布要时间,线上还在被打爆。你要先用Redis提供的手段把当前的风险解除,比如把读取切成新的小key、把大key异步删掉,然后再上线新代码。这种处理顺序在面试里讲出来,特别能体现你的应变能力。

4.3 面试回答的框架和话术

最后给你一个面试可以直接套用的回答框架,我管它叫“三步定位法”。

第一步定位定义:先说明Big Key指的是String value过大(常见超过10KB)或集合元素过多(常见超过5000个),并点出核心危害是阻塞单线程、网络带宽、数据倾斜、删除卡顿。

第二步定位发现:说自己会用SCAN或redis-cli --bigkeys做初筛,用MEMORY USAGE做精确确认,再配合慢日志定位具体key。

第三步定位解决:大Key按业务拆分,value过大用压缩和更优序列化,删除用UNLINK异步处理,最后强调从设计、review、监控三方面做预防。

按照这个框架,哪怕遇到没做过实操的候选人,也能给出一个逻辑自洽的回答。如果面试官深挖“你说的10KB和5000是哪来的”,你就说这是业界实践总结出来的经验阈值,不同业务可以有不同标准,判断核心是看key是否已经开始影响Redis性能。这样既不硬背,也有理有据。

写到这里,我想起自己当年面一个中厂岗位,就是栽在“Big Key怎么发现”这个追问上。当时我能说出定义,但说不出排查命令,面试官就很温和地问了一句“那线上出了事你怎么定位”,我支吾半天没答上来。现在回头看,这个问题的价值不只是应付面试,而是它逼着你去理解单线程模型、内存回收、结构编码这些Redis底层机制,理解了这些,你写的代码自然就更靠谱。面试也好,日常开发也好,遇到Big Key别怕,按“发现-拆分-优化-预防”这一套走下来,问题基本都能落地解决。

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

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

立即咨询