1. 面试官问出大key时他真正想听什么面到中高级岗位Redis 相关的问题基本躲不掉而大key这个话题这两年出现频率明显变高。原因不难理解早期大家用 Redis 就是当个缓存key 里塞的东西也就几百字节出不了大事现在越来越多的系统把 Redis 用到了排行榜、消息队列、购物车、会话存储、配置中心这些场景单个 key 的体积越滚越大线上事故也就跟着来了。我在实际项目里见过一个存用户行为埋点的 Hash单个 key 里挂了四十多万个 field某次做全量导出直接把实例内存打到 90% 以上接口 P99 从 30ms 飙到 2 秒。所以面试官问这道题表面上是考你知不知道拆实际上是在筛三件事你有没有真的被大key坑过、你清不清楚 Redis 是单线程模型这个前提、你能不能给出一个带数据支撑的拆分方案而不是背概念。先说清楚什么叫大key。这个词没有官方定义业界比较通用的口径分两个维度看体积维度上String 类型超过 10KB 就算偏大Hash/List/Set/ZSet 这类集合类型元素数量超过 5000 个或者总内存占用超过 1MB 就该警惕元素维度上单个集合里某个 field 或 member 的体积超过 10KB同样算大key这种叫元素大key很隐蔽很多人只盯着元素数量看结果一个 field 里存了个富文本或 base64 图片坑得更狠。注意大key的危害不是慢这一个字能概括的。它真正麻烦的地方在于阻塞主线程、拖垮集群数据倾斜、以及在删除和过期时引发连锁反应这三件事的严重程度依次递增。为什么要拆分、怎么拆分得先把危害讲透否则方案就是空中楼阁。Redis 处理命令是单线程的指命令执行路径网络 IO 和部分后台任务是另外的线程这意味着任何一个耗时命令都会让后面排队的请求一起等。一个大key上执行HGETALL、SMEMBERS、LRANGE 0 -1返回几十万元素序列化和网络传输的时间全压在这一次调用上客户端这边可能直接超时。更麻烦的是删除DEL一个大key 的复杂度是 O(N)N 是元素数量删一个几十万成员的 Hash 能让主线程卡住好几百毫秒UNLINK虽然把释放内存的操作挪到后台线程了但主线程仍要做一次键空间的摘除大key场景下依然有感知。还有过期淘汰如果大量大key 设置了相同的过期时间到点集中失效那一下的内存回收压力会非常明显。集群场景下问题还要再翻一层。Redis Cluster 按 key 的哈希槽分配数据一个大key 只落在一个槽、一个节点上这个节点的内存和 CPU 就被它一个人占着其他节点闲得发慌这就是典型的数据倾斜。你做扩容加机器对不起那个大key 还是压在同一台机器上加多少台都没用。我踩过一次某个业务线把全量商品快照塞进一个 Hash集群扩到 12 个节点其中一个节点内存占用是其他节点的三倍多怎么都降不下来最后只能把那个 key 拆了才解决。所以这道面试题的标准答题路径应该是这样的先讲清楚大key 的定义和三种危害再讲怎么发现它--bigkeys、--memkeys、SCAN抽样、慢查询日志、监控平台然后才是核心的拆分方案设计最后补充删除、过期、迁移这些配套处理。只答拆成多个小key然后就没词了是拿不到分的。下面我就按这个顺序把每一步的操作细节、参数选择、踩坑经验都摊开讲尽量让你看完能直接在自己项目里落地。2. 先学会把大key 揪出来再谈怎么拆拆分方案的前提是定位。线上环境不可能让你随便KEYS *那玩意在大库上跑一次就能把实例干趴。我一般用三层手段配合日常巡检用官方工具问题排查用 SCAN 抽样事后复盘看慢查询和监控。2.1 redis-cli --bigkeys 的正确用法和它的盲区redis-cli --bigkeys是最常用的入口它内部就是用SCAN遍历键空间对每种数据类型分别统计最大的那个 key。基本命令是这样redis-cli -h 127.0.0.1 -p 6379 -a your_password --bigkeys它会输出每种类型string、list、hash、set、zset、stream各自的 top1 大key以及元素数量。想看得更细加-i控制扫描间隔避免巡检本身给实例压力redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1-i 0.1表示每扫描 100 个键休眠 0.1 秒。这个小参数很多人不知道但它很关键——不加的话在几百万键的实例上扫一遍CPU 会有一个明显的尖刺。这个工具有两个明显盲区必须在面试里主动说出来能体现你真的用过。第一它统计的是元素数量不是内存占用。一个 5000 个成员但每个成员都很小的 ZSet和一个 100 个成员但每个成员 50KB 的 Hash它会告诉你前者更大。要按内存看得用redis-cli -h 127.0.0.1 -p 6379 --memkeys--memkeys依赖MEMORY USAGE命令能按实际字节数排序更贴近真实危害。它的代价是每次估算都有开销大实例上跑得慢建议低峰期执行。第二它只给 top1且是采样估算。真实生产里大key 往往不止一个而且--bigkeys统计的元素数量是基于SCAN返回的类型和长度信息对某些编码比如 listpack 编码的小集合是精确的但整体仍是逐 key 遍历键特别多的时候耗时会很长。我的做法是把它当体检真正的精确定位靠下面的方法。2.2 用 SCAN 和 MEMORY USAGE 做精确盘点--bigkeys不够用的时候我写个脚本自己扫。核心思路是SCAN游标遍历对每个 key 用MEMORY USAGE拿字节数超过阈值的记下来。Python 版本大概长这样import redis r redis.Redis(host127.0.0.1, port6379, passwordyour_password, decode_responsesTrue) cursor 0 big [] while True: cursor, keys r.scan(cursorcursor, count1000) for k in keys: try: size r.memory_usage(k) # 单位字节 if size and size 1024 * 1024: # 超过 1MB 记录 t r.type(k) big.append((k, t, size)) except Exception as e: print(skip, k, e) if cursor 0: break big.sort(keylambda x: x[2], reverseTrue) for k, t, s in big[:50]: print(f{k}\t{t}\t{s/1024/1024:.2f}MB)几个实战注意点。MEMORY USAGE默认会采样估算加SAMPLES 0是全量精确计算但会更慢对元素特别多的集合我一般用默认采样误差可以接受。SCAN的count参数是提示值不是精确值设 1000 到 5000 之间比较平衡太小了网络往返多太大了单次阻塞时间长。扫描一定要放在从节点或者低峰期做主节点上跑这种遍历再温和也会影响正常请求。如果运维平台支持更省事的做法是看监控里的单key 内存分布或者用redis-rdb-tools离线分析 RDB 文件。后者适合做周期性盘点rdb -c memory /path/to/dump.rdb --bytes 1048576 -f bigkeys.csv这条命令直接从 RDB 快照里筛出超过 1MB 的 key输出 CSV 含 key 名、类型、字节数、元素数量、编码方式。它对线上零影响因为读的是文件而不是实例特别适合每月做一次容量盘点。缺点是数据有滞后取决于你 RDB 的生成频率。2.3 慢查询和延迟监控才是事故现场的第一手证据前面那些是提前发现真出事故的时候最先给你线索的是慢查询日志。配置项是slowlog-log-slower-than单位微秒默认 10000 即 10ms我觉得这个值偏宽松高 QPS 场景建议调到 5000 甚至 2000。查看最近记录redis-cli SLOWLOG GET 20每条记录包含时间戳、执行耗时、命令和参数。大key 的慢查询有个典型特征命令本身很简单比如就是个HGETALL或者DEL但耗时异常高。看到这种就要立刻想到大key。还有一个容易被忽略的命令是LATENCYredis-cli LATENCY LATEST redis-cli LATENCY HISTORY event_name它能记录超过latency-monitor-threshold配置的事件帮你判断阻塞是偶发还是持续性。我一般把阈值设成 100ms只记录真正有影响的尖刺避免噪音。排查手段适用场景对线上影响数据精度--bigkeys日常巡检低可用-i降速按数量非内存--memkeys定位内存大头中按内存估算SCAN MEMORY USAGE精确盘点中建议从节点高RDB 离线分析周期盘点、容量规划无高但有滞后SLOWLOG事故现场取证无只看已发生的LATENCY阻塞事件统计无事件级提示把这几种手段组合成巡检告警取证三层比单靠某一种靠谱得多。巡检抓趋势告警抓异常取证抓原因。定位到具体的 key 之后先别急着动手拆。要先确认这个 key 的写入模式是持续增长的、还是短暂峰值后会自动清理的是单个业务写、还是多方混写这决定了你拆分后的数据路由策略。我见过有人直接按时间拆了结果业务侧读的时候查不到历史数据反而制造了新故障。3. 拆分的底层逻辑路由规则决定一切大key 拆分这件事难的不是把一个 key 变成多个 key这个动作难的是拆完之后怎么让读写方准确地找到数据。面试时如果只答按 ID 取模拆成 N 份那还停留在表面。真正要讲清楚的是路由规则怎么选、扩容时怎么办、拆分的粒度怎么定。3.1 三种主流拆分思路的取舍按数据特征来分拆分大致有三条路按哈希分片、按业务维度切分、按时间窗口滚动。三种各有明确的适用边界。按哈希分片适合元素之间没有明显业务分组、访问又比较均匀的场景比如一个存用户标签的大 Set或者一个超大的布隆过滤器。做法是对元素本身算哈希再取模落到 N 个子 key 里。优点是分布均匀、实现简单缺点是无法按业务维度批量读取比如你想一次性拿出某个用户组的所有标签就得把 N 个子 key 全查一遍再聚合反而更慢。按业务维度切分是我最推荐的方式只要数据本身有天然的划分依据。比如一个存全站用户购物车的大 Hash按 userId 的后两位拆成 100 个 key一个存所有订单的 ZSet按订单所属的商户 ID 分片。这种拆法的好处是路由规则对业务透明、可解释性强出问题好排查而且天然支持按维度扩容。它的前提是业务上必须存在一个稳定的、基数足够大的划分字段。按时间窗口滚动适合日志、流水、埋点这类只关心近期数据、旧数据可以归档的场景。比如按天拆成event:20240115、event:20240116配合过期时间自动清理。这种拆法几乎不用写复杂的聚合逻辑因为业务本来就只查最近几天。缺点是跨天查询需要客户端合并多个 key 的结果。选哪种我一般按这个顺序判断业务上有没有天然的高基数维度有就用业务维度拆。没有就看是否只关心近期数据是就按时间拆。两者都不满足才退到哈希分片。3.2 分片数量怎么算——一个被大多数人忽略的推导过程面试里经常被追问你打算拆成多少个。很多人答拆成 16 个拆成 100 个说不出理由。这个数其实是可以算出来的。设原大key 有 $N$ 个元素总内存 $M$你期望单个子 key 的元素数不超过 $n$、内存不超过 $m$那么分片数 $K \lceil N / n \rceil$同时要满足 $K \ge \lceil M / m \rceil$取两者较大值。举个实际例子。一个用户行为的 Hash60 万个 field总内存约 90MB。如果按元素数算希望每个子 key 不超过 5000 个那 $K 600000 / 5000 120$按内存算希望每个子 key 不超过 1MB那 $M/m 90 / 1 90$。取大的拆成 120 个。但 120 不是个好数字我通常会向上取整到一个方便计算的数比如 1282 的幂或者 100便于按十进制位取模。取 2 的幂的额外好处是可以把取模优化成位运算虽然 Redis 客户端层面这点性能微不足道但路由代码会更好写。除了容量还有两个约束要考虑。一是集群槽位。Redis Cluster 一共 16384 个槽如果子 key 太多比如拆成几万个每个子 key 都要占一个槽会加剧槽分布不均的概率。一般单个业务的大key 拆分成几十到几百个子 key 是比较合理的区间。二是客户端聚合成本。子 key 越多一次全量读取的网络往返就越多如果业务真的需要读全量那就是拆了个寂寞。所以拆分之前一定要先看读模式。3.3 谁来做路由——客户端一致性哈希还是固定映射路由规则放在哪一层是个架构决策面试里讲清楚这个能加分。最朴素的做法是固定映射key base _ (hash(field) % K)K 固定写死或者放配置中心。实现简单问题是扩容麻烦——K 从 128 变成 256几乎全部元素都要迁移等于全量重刷。好一点的做法是一致性哈希扩容时只影响环上相邻的一段。但标准的哈希环实现需要维护虚拟节点客户端复杂度上来了而且对 Redis 这种本来就有槽位机制的系统来说有点重复造轮子。折中方案是预分片一开始就按 1024 份规划但实际只用前 128 个扩容时把新数据往后面没用到的分片挪不需要动已有数据。这个思路在业界很常见本质是提前买好扩容的余地。我的实践建议中小规模子 key 几十个以内用固定映射加配置中心扩容时停机迁移或者双写渐进迁移大规模场景用预分片代价是路由表要维护好不能拍脑袋改。如果你们的 Redis 是 Cluster 部署还有个更省心的选择让 Redis 自己来分散。把原来一个大key 改成biz:user:action:{userId}:field这种结构用 userId 做 hash tag让不同的 userId 落到不同的槽。这种做法本质上不是拆分一个 key而是重新设计 key 的粒度从根源上避免了大key 的产生。我个人更推崇这种思路因为它是治本的。拆分方式典型结构扩容易度读全量代价适用场景按哈希分片base:{0..K-1}中需重分布高查 K 次元素无业务分组按业务维度base:{userId}高新维度即新key中按需查有高基数业务字段按时间滚动base:20240115无自然增长中跨期合并日志、流水、埋点预分片base:{0..1023}高预留位高长期增长、规模大3.4 拆完之后那个聚合读的老问题还在这是拆分方案里最容易被面试官追问的点你把 100 个子 key 拆好了业务要拿全量数据怎么办MGET一次拿 100 个 key那不就是把大key 的痛点换了个地方从单命令慢变成多命令总耗时更长。对待聚合读我一般从两个方向解决。第一从业务上消灭全量读。绝大多数对大key 的全量读回头看都是设计缺陷——要么是业务流程本不需要全量、只是图省事要么是导出、统计这类离线需求混进了在线链路。前者改接口按需分页后者用别的手段解决比如走从库、走离线数仓、走备份文件解析。这个方向听起来像逃避问题但实际收益最大因为拆分只能降低单次操作的成本改变不了总量。第二必须全量时用MGET或pipeline批量拿控制单批大小。MGET 相比 N 次 GET 能省掉大量网络往返但一次塞太多 key 也会造成单命令阻塞我一般按每批 50 到 100 个分批。如果是 Hash 切分客户端拿到子 key 列表后用 pipeline 发HGETALL在客户端内存里聚合。这里要注意内存峰值——100 个子 key 每个 1MB聚合起来就是 100MB 的客户端内存JVM 上很容易触发 GC 抖动。所以聚合逻辑要设上限、要流式处理不能一把梭。4. 删除、过期、迁移拆分方案里最容易翻车的三件事很多人把方案讲完拆就结束了但真正在线上搞死你的往往是拆分之后的配套动作。这一章我按事故发生的顺序讲。4.1 大key 的删除为什么 DEL 和 UNLINK 都得小心DEL大key 是 O(N) 操作N 大起来主线程直接卡死。这个大家都知道所以很多人换UNLINK。UNLINK的原理是把 key 从键空间摘除这步是同步的、很轻然后把实际的内存释放交给后台线程异步做。听起来完美但有两个现实问题。问题一UNLINK的异步释放是有队列和线程数限制的。后台线程默认数量很少跟 IO 线程配置相关如果你在短时间里连续 UNLINK 几十个大key后台释放线程会被塞满内存回收跟不上表现为 used_memory 迟迟不降、碎片率飙升严重时触发 OOM 或者被内存淘汰策略误伤。所以批量清理大key 时一定要控制速率比如每秒删一到两个中间加 sleep。问题二UNLINK只解决释放不解决键空间摘除本身的开销。对于 Hash/Set 这种元素特别多的结构摘除时仍有遍历虽然比 DEL 快很多但不是零成本。元素量到几十万级别依然能观测到延迟尖刺。我的标准做法是渐进式删除用HSCAN/SSCAN/ZSCAN分批处理# 思路每轮扫描若干元素删除一部分直到删空 redis-cli --scan --pattern big:key:* | while read k; do echo handling $k done更具体的 Hash 批量删除可以用 Lua 脚本一次处理一批-- 每次最多删除 200 个 field避免单次阻塞 local key KEYS[1] local limit tonumber(ARGV[1]) or 200 local cursor 0 local deleted 0 repeat local res redis.call(HSCAN, key, cursor, COUNT, limit) cursor res[1] local fields res[2] for i 1, #fields, 2 do redis.call(HDEL, key, fields[i]) deleted deleted 1 if deleted limit then break end end until cursor 0 or deleted limit return deleted然后客户端循环调用这个脚本直到返回 0。注意 Lua 脚本本身在 Redis 里是原子执行的脚本里做太多事情同样会阻塞所以limit别设太大200 到 500 之间比较稳妥循环次数多一点没关系。注意用 Lua 批量删的时候别在脚本里用KEYS或者做全量遍历脚本执行期间整个实例是阻塞的写错一个循环就能把线上打停。如果是 String 类型的大key单个 value 几十 MB 的那种删除其实还好因为 String 的内存释放比较直接UNLINK就够了真正麻烦的是集合类。4.2 过期时间集中失效一个比删除更隐蔽的坑大key 拆成 100 个子 key如果你给它们设了同一个过期时间那到点的时候 100 个 key 同时到期Redis 的主动过期循环要在一轮里处理掉所有这些 key内存回收和键空间清理的压力集中爆发。这个坑很隐蔽因为你在设过期时间的那一刻完全想不到。规避方法简单但一定要做给子 key 的过期时间加随机抖动。基础 TTL 加一个随机偏移比如基础是 24 小时实际设成 24 小时 ± 随机 30 分钟import random def ttl_with_jitter(base_seconds, jitter_ratio0.1): base_seconds: 基础过期时间秒 jitter_ratio: 抖动比例默认 ±10% delta int(base_seconds * jitter_ratio) return base_seconds random.randint(-delta, delta) # 使用 ttl ttl_with_jitter(24 * 3600) # 24 小时 ± 10% r.expire(key, ttl)同样的问题也出现在缓存雪崩场景原理一模一样。所以这条经验其实可以复用任何批量设置的过期时间都该加抖动。顺带说一句淘汰策略。大key 存在的时候如果maxmemory-policy是allkeys-lru或者volatile-lru淘汰一个大key 的操作同样可能造成尖刺。如果你的实例上还有大key 没拆完建议临时把策略调成allkeys-random或者分批处理避免 LRU 在压力下集中淘汰大对象。4.3 数据迁移拆分过程中的双写和校验拆一个大key如果已经有存量数据就涉及到迁移。这里分两种情况。情况一可以接受短暂不一致。那就写个迁移脚本用 SCAN 从旧 key 读按新路由规则写进子 key边写边删。要点是控制每批的数量和节奏别把实例打满。迁移期间旧 key 和新 key 都要能读业务侧做兼容读先读新miss 再读旧。情况二不能接受不一致。那就得双写。改造写入逻辑同时写旧 key 和新子 key读按新规则走但保留旧路径兜底。等新 key 的数据被访问过一轮、确认完整了再下线旧 key 的写入最后清理旧数据。双写的代价是写入量翻倍、要处理两边不一致的兜底逻辑但它是唯一能保证平滑迁移的方式。迁移做完必须校验不能凭感觉。我的校验方法是抽样比对从新 key 里随机抽 1000 个元素去旧 key 里查确认值一致同时统计新旧两边的元素总数允许有微小差异迁移期间的新写入差异超过阈值就停下来查。可以写个简单的对账脚本def verify_migration(old_key, new_keys, sample_size1000): 抽样校验迁移结果 import random # 从旧 key 随机取一些 field all_fields list(r.hkeys(old_key)) samples random.sample(all_fields, min(sample_size, len(all_fields))) mismatch 0 for f in samples: old_val r.hget(old_key, f) # 按路由规则算目标 shard shard hash(f) % len(new_keys) new_val r.hget(new_keys[shard], f) if old_val ! new_val: mismatch 1 return { sampled: len(samples), mismatch: mismatch, rate: mismatch / len(samples) if samples else 0 }这个脚本跑完之后如果mismatch是 0 或者极低比例且能解释原因就可以进入下线旧 key 的流程。还有一点迁移期间一定要盯着监控内存、CPU、慢查询、客户端超时。我见过一次迁移跑到一半因为没控制速率实例内存翻倍直接触发了淘汰把热数据挤出去了业务侧命中率骤降。教训就是迁移脚本里的 sleep 不是浪费时间是保护你自己。操作风险点缓解手段DEL大key主线程长时间阻塞换UNLINK或渐进删除UNLINK批量后台线程队列积压、内存不降控制速率每秒 1-2 个子 key 同时过期过期风暴、内存回收尖刺TTL 加随机抖动存量数据迁移数据不一致、实例过载双写抽样校验限速集群扩容大key 仍在单节点倾斜不缓解先拆分再扩容5. 更根本的解法把大key 消灭在产生之前面试答到这里如果你还能往前推一步讲讲怎么从设计上避免大key分数会明显不一样。因为拆大key 本质上是治已病代价高、风险大而大key 的产生大多源于几个固定的设计坏习惯提前规避成本极低。5.1 从建模阶段就拒绝万能大容器最典型的反模式是为了方便把某个业务实体的所有相关信息塞进一个 Hash。比如用户维度把画像、设置、积分、行为、标签全放user:{id}里。一开始只有几千用户完全没问题等用户量到百万每个 Hash 里有几十个 field单个 key 可能还好但如果某个 field 是列表或者嵌套结构一个 key 就能到几 MB。正确的做法是按访问模式拆域。还是用户这个例子拆成user:profile:{id}、user:setting:{id}、user:points:{id}、user:tag:{id}。拆的依据不是数据相关性而是是否总被一起读写。如果画像和设置从来都是分开读的那放一起就纯属浪费还埋了雷。另一个反模式是把 Redis 当数据库用。见过有项目把全量商品信息序列化成 JSON 塞进一个 key理由是读得快。这种设计的本质是用 Redis 替代了本该由数据库或搜索引擎承担的角色一旦数据量上来必然爆炸。Redis 适合存热点、存小对象、存临时状态不适合当主存储。5.2 编码方式的选择listpack 与 quicklist 的隐性影响这个点比较细节但面试里说出来很加分。Redis 对集合类型有很多种内部编码比如 Hash 在小规模时用 listpack旧版本叫 ziplist元素多了或者单个元素大了会转成 hashtable。编码转换本身是有开销的而且转换之后内存占用会明显上升。相关配置项# Hash 使用 listpack 的最大元素数 hash-max-listpack-entries 128 # Hash 单个元素的最大字节数 hash-max-listpack-value 64 # ZSet 同理 zset-max-listpack-entries 128 zset-max-listpack-value 64很多人不知道这些配置用默认值元素数 128、值 64 字节。如果你确定某个 Hash 会长期保持在 100 个以内的 field可以适当调大阈值让它在 listpack 编码下运行内存能省不少。反过来如果你已经预见到一个 Hash 会涨到几万个 field那就不该让它经历从小 listpack 逐步转成大 hashtable的过程而是一开始就拆。因为转换那一下是有尖刺的而且转换后的内存占用往往超出预期。我的经验是任何预期元素数会突破 1000 的集合类型设计阶段就按分片来考虑别等它涨起来。这个阈值没什么理论依据纯粹是经验——1000 个元素以内的集合内存占用通常在几百 KB 级别风险可控超过之后增长速度往往不是线性的。5.3 写入侧的四条硬性纪律除了建模写入侧的纪律同样重要。我总结了几条在新项目里会直接写进开发规范。第一条任何集合类型的写入必须估算上限。写代码的时候就要问自己这个 List 会不会无限 push这个 ZSet 会不会一直往里面加成员却没有淘汰如果是那要么加长度限制LTRIM、ZREMRANGEBYRANK要么加过期时间。有个很常见的场景是消息队列用的 List生产者快消费者慢List 会一直涨必须加LTRIM保长度。第二条单个 value 的大小要有硬约束。比如规定任何写入 Redis 的 String 不超过 10KB超出的要么压缩、要么拆分、要么干脆不走 Redis。这条约束最好在客户端 SDK 层做拦截靠人自觉是靠不住的。我在一个项目里给 Redis 客户端封装加了个检查超过阈值的写入直接抛异常并打日志上线第一个月就抓出了十几个违规调用点。第三条禁止无过期时间的集合写入。除了明确的持久化配置类数据业务数据一律要设 TTL。这条规则听着粗暴但它能兜住绝大多数忘记清理导致的大key。有了 TTL即使某个 key 涨起来了也有个自然消亡的兜底。第四条监控必须覆盖单key 维度。光是实例级别的内存告警不够要能按 key 前缀统计内存和元素数设置阈值告警。前面讲的 RDB 离线分析就可以做成定时任务每周跑一次输出 top N 大key 清单推给对应业务方。5.4 客户端 SDK 层做统一封装比事后补救划算太多最后讲一个工程化层面的建议。如果团队里多个项目都在用 Redis与其让每个人各自判断要不要拆不如在客户端封装层统一处理。可以做这么几件事自动检测超限写入前检查 key 的预估大小和元素数量超阈值打点告警。统一的分片原语提供shardedHash之类的封装业务侧只管调 API分片逻辑和路由规则收敛在 SDK 里。渐进删除工具把前面讲的 SCAN 分批删除逻辑封装成一个方法避免每个人都手写一遍还写错。可观测性埋点把每次操作的 key 前缀、元素数量、耗时打点到监控系统后面查问题有据可依。这些封装做一次后面所有业务都受益。我现在的项目里分片 Hash 的封装大概一百多行代码包含路由、批量读、渐进删除、TTL 抖动这几个能力接入的业务方基本不用关心拆不拆的问题用起来跟普通 Hash 没区别。这才是比较可持续的做法。我的一个判断标准如果一个 Redis 使用规范只能靠文档和口头约定来落实那它大概率落实不了。能写进 SDK 的规则才是真规则。6. 面试时怎么把这个方案讲出层次感前面把技术细节铺完了最后说点答题策略。这道题区分度很高因为不同水平的回答能拉开明显差距我按常见回答质量分个层你可以对照自己现在的位置。层次回答特征面试官判断及格知道大key 定义能说清阻塞主线程有基础认知中等能讲出拆分思路说出取模或按业务拆有过实践良好能讲定位手段bigkeys、slowlog、拆分粒度推导、聚合读处理真做过优秀加删除方案UNLINK、渐进删除、TTL 抖动、迁移校验、预防机制踩过坑、有体系加分能讲集群倾斜场景、预分片扩容、SDK 层封装、RDB 离线盘点有工程视角具体讲的时候我建议用问题-危害-定位-方案-兜底这条线串下来每一段给一个具体数字或场景别全是概念。比如讲危害的时候说删一个 40 万 field 的 Hash 能让主线程卡住几百毫秒比说会造成阻塞有说服力得多讲拆分粒度的时候把那个 60 万 field、90MB、拆 128 份的推导过程说出来面试官立刻知道你算过。有几个高频追问要提前准备。拆完之后原子性怎么保证答拆分确实牺牲了跨子 key 的原子性如果业务需要跨分片原子操作要么用 Lua 脚本把多个子 key 的操作包起来注意脚本里所有 key 必须在同一个槽否则 Cluster 下会报 CROSSSLOT 错误要么在业务层用分布式锁保证要么重新审视这个业务是否真的需要原子性。跨槽怎么办答用 hash tag把相同分片的部分用{}包起来比如biz:{user123}:tags:0让同一业务实体的子 key 落到同一个槽。拆分后 key 数量暴增有什么影响答键空间变大、内存开销增加每个 key 都有元数据开销大概几十到上百字节、SCAN 变慢、KEYS更不能用了但这些都是可控的跟大key 的阻塞风险比划算。说白了这道题考的不是背方案是考你有没有在真实场景里为一件事负责到底。能说清楚我为什么选这个粒度、我怎么验证迁移没问题、我踩过什么坑比把所有方案罗列一遍强。我面人的时候最怕听到可以用一致性哈希然后追问细节就开始含糊——那说明只是看过文章没动过手。最后分享一个我自己一直在用的排查小习惯线上出现任何莫名变慢且排除了网络和慢 SQL 的情况我第一件事就是去看SLOWLOG里有没有那种命令很短但耗时很长的记录十次里有六七次能直接指向大key。这个习惯帮我省了大量排查时间也让我对大key 的危害有了远超文档描述的直观认识。