
这道题面试官真正在筛什么
「Redis 大 key 多 key 怎么拆?」——美团、阿里、字节都问,得物二面还专门考过。
表面看是个方案题,实际上筛选的是这三件事 :
- 你知道大 key / 多 key 的真正危害是什么吗 ——绝不是一句「占内存」就能概括的;
- 你做过生产事故复盘吗 ——像大 key 删除卡死主线程、多 key 跨 slot 导致 pipeline 失败这类坑,没踩过的人讲不透彻;
- 你了解 Bloom 拆开的陷阱吗 ——这道题只有真正踩过坑的人才能答到点子上。
如果只说「拆成多个 hash + 分桶」,那最多也就30分;如果能清晰地讲出4类典型场景的拆法以及背后的原理,有60分;如果再能补充 Bloom 拆分的常见错误,点出 Redis 集群 slot 映射成本,那就能拿到90分了。
下面我们就按段位层层递进。
L1:30 秒答案过基础线
首先要明确——大 key 和多 key 是两码事 :
|
大 key |
多 key |
| 定义 |
单个 key 的 value 太大(String > 10K,集合 > 1 万元素) |
集群里 key 总数过多(亿级) |
| 真凶 |
单线程阻塞、网络包大、删除耗时 |
内存碎片、slot 映射成本、集群通讯开销 |
| 默认拆法 |
value 拆字段(hash)/ 拆元素(多个 key + 桶 hash) |
多个 key 转一个 hash 结构 |
通用心法 :Redis 是单线程的——单次操作的 value 越大,越容易阻塞主线程。 「能拆则拆」是玩转 Redis 的生存法则 。
L2:2 分钟答案——4 类场景的拆法
按场景分为四类,每类都有特定的拆法。
场景 1:单个简单 key 的 value 很大
比如一个用户信息对象 user:82001 序列化成 JSON 后有 100KB。
两种拆法,取决于你的读取模式 :
// 拆法 A:每次整存整取
// → 拆成多个 key + multiGet 并行读
mget("user:82001:profile", "user:82001:settings", "user:82001:stats");
// 拆法 B:每次只取部分字段
// → 转成 hash,hget / hmget 按需读取
hget("user:82001", "profile");
hmget("user:82001", "name", "age", "city");
关键洞察 :拆法 A 将单点压力分摊到多个 Redis 实例(前提是 key 落到不同 slot),而拆法 B 旨在减少单次网络包的大小。Hash 是大 key 拆分的首选武器 ——它比 String 更省内存(得益于ziplist编码),还支持部分字段操作。
场景 2:value 中存储过多元素(万级以上)
hash / set / zset / list 里塞了几十万个元素——单次 hgetall 或 smembers 就能把主线程卡死。
预分桶 + hash 取模 :
// 原方案:一个大 hash 装 100 万个 field
hget("user_profile", "82001");
hset("user_profile", "82001", value);
// 改造:固定 1000 个桶,按 field hash 取模
int bucket = Math.floorMod(fieldId.hashCode(), 1000); // 用 floorMod,不用 Math.abs
String hashKey = "user_profile:" + bucket;
hget(hashKey, fieldId);
hset(hashKey, fieldId, value);

桶数量怎么选 :
- 一个 hash 内 field 数量控制在 100-500 比较合适;
- 总元素 100 万 → 桶数 2000-5000;
- 太少 :单桶仍然过大;太多 :Redis 内部从 ziplist 退化成 hashtable,省内存的优势就没了。
注意两点 :
- hash 取模要处理负数 ——Java
hashCode() 可能返回负数。别用 Math.abs() ——Math.abs(Integer.MIN_VALUE) 仍是负数,正确写法是 Math.floorMod(hash, bucketNum);
- 保持顺序的场景要特殊处理 ——比如
lpop 需要拿到最早 push 的元素,简单分桶会破坏这个顺序,这时就要按时间维度来拆 key。
场景 3:集群存了上亿 key(key 本身过多)
上亿的 key 在 Redis 的早期版本(如 3.x)会带来两个问题:
- 每个 key 自带 Category 前缀 ——一亿个 key 光前缀就要吃掉几百 MB 内存;
- 集群模式下 slot2key 映射的指针占用 ——达到亿级时,几个 GB 的开销毫不夸张(好在 4.0 后有所优化)。
解法是把多个相关的 key 合并到一个 hash 结构里:
情况 A:key 本身有强相关性
原来:
user.zhangsan-id = 123
user.zhangsan-age = 18
user.zhangsan-country = china
合并后:
hash key = user.zhangsan
field id = 123
field age = 18
field country = china
3 个 String key 合并成 1 个 hash key——省下 2 个 key 的元数据和 slot 映射成本。
情况 B:key 之间没相关性
原来 2 亿独立 key:
user.123456789
user.987654321
user.678912345
预分桶:
桶数 = 2 亿 / 100 = 200 万 个桶
按 hash(userId) % 2000000 分到对应桶
hset(bucket_key, userId, value)
200 万个 hash key 替代 2 亿个 String key——key 数量直接降了两个数量级。
场景 4:大 Bitmap / Bloom 过滤器拆分
Bloom 过滤器经常做到 512MB——单 key 半个 G 是绝对的巨型 value,所有大 key 的坏处它全占 :
- 内存碎片;
- 主从同步缓慢;
- 网络传输延迟高;
- 删除时甚至能卡住主线程好几秒。
拆开它 = 分成多个小 Bloom ——比如 512MB 拆成 1024 个 512KB。但拆法有正反之分,一步走错,效果适得其反。
L3:5 分钟答案——Bloom 拆开的陷阱与集群成本
陷阱 1:拆 Bloom 不是「把大徐隆切成 N 个小块凑在一起用」
90% 的人第一反应会犯错 ——把大 Bloom 切片,每个 key 依然按原 Bloom 的多个哈希函数落在不同的切片上:

这种拆法的问题 :
- 一个 key 的查询要访问多个 Bitmap → 带来多次网络往返;
- 这些 Bitmap 可能散落在不同的 Redis 节点上 → 导致跨节点查询;
- 拆完反而更慢了。
正确姿势 :让每个小 Bloom 独立完整。先通过 hash key 决定它落到哪个 Bloom,然后在这个小 Bloom 内部再执行多哈希函数:

每次查询只需访问一个 Bloom(一个 Redis key)——网络往返次数从 N 次降为 1 次。
陷阱 2:Bloom 拆开后,误判率会变吗?
直觉上,Bloom 变小了,误判率应该升高——但实际情况并非如此。Bloom 的误判率约等于:
$(1 - e^{-\frac{kn}{m}})^k$
公式取决于哈希函数个数 k、元素个数 n 和 Bitmap 大小 m。只要拆分时元素是均匀分配的(n / m 的比值保持不变),误判率就不会变。
实操参数推荐:
- k 取 13 是经验上的最优解;
- 单个 Bloom 控制在 512KB 以下 ,大了就拆;
- 客户端利用
setBits / getBits(>= 2.3.4)进行批量操作,能有效减少网络往返。
陷阱 3:别再忽视 Redis 集群的 slot 映射成本了
很多人会忽略一件事——Redis 集群的 16384 个 slot 与 key 的数量息息相关 :
- 每个 key 都需计算 CRC16 → 对 16384 取模 → 映射到一个 slot;
- 集群中的每个节点都要维护
slot → keys 的反向索引;
- key 越多,这个反向索引就越大,集群通讯(如 reshard、failover)时的开销也越重。
对亿级 key 进行拆分的本质,就是为了「降低 slot 映射的开销」 ——不仅仅是省内存,更是省下宝贵的集群协议带宽。
这也就是为什么大厂的 Redis 开发规约里,总能看到「单实例 key 数量不超过 1000 万 」这样一条硬性规定——超过了这个量级,集群的任何运维操作(resharding、节点上下线)都可能变得异常缓慢。
大 key 删除的隐藏陷阱
大 key 还有一个独立的坑——直接 DEL 绝对会阻塞主线程 :
- 4.0 之前:
DEL 命令是同步执行的,一个 100MB 的 key 就能让主线程卡住几百毫秒;
- 4.0 之后:我们应该用
UNLINK 进行异步删除,主线程仅仅解除引用,后台线程会慢慢地回收内存。
生产环境里,删除大 key 要永远使用 UNLINK 而不是 DEL ——这是个反直觉但又极其重要的细节。
直接掉分的几种答法
按扣分的严重程度从高到低排列:
- 「直接 DEL 掉就行」 ——大 key 删除会阻塞主线程,这是基础知识,封顶30分;
- 「Bloom 拆开就拆成 N 份小的,原来怎么用现在还怎么用」 ——这是最典型的错误拆法,反而会越拆越慢,也是这道题最容易踩的坑;
- 「桶数越多越好」 ——错。桶太多了会让 hash 从 ziplist 退化成 hashtable,省内存的优势荡然无存;
- 「Redis 单线程不影响多 key 操作」 ——错。多 key 操作(如 mget / pipeline)在集群模式下是按 slot 分组的,一旦跨 slot 就会直接报错;
- 「大 key 是值大,多 key 是 key 多,没什么本质区别」 ——错。大 key 的核心问题是单次操作阻塞,多 key 的核心问题是元数据与 slot 映射的成本,这是两类完全不同的问题;
- 从来不提 UNLINK / lazy free ——4.0 之后的核心特性都被你忘了。
高频追问怎么接
Q1:怎么发现线上有大 key?
四种方式:
redis-cli --bigkeys —— 扫描全库找大 key(基于采样,不影响主线程);
MEMORY USAGE key —— 精确定位单个 key 的内存占用;
SCAN + 业务侧统计 —— 避开阻塞主线程的 KEYS * 命令;
- 慢查询日志 —— 大 key 操作往往都伴随着慢查询。
实际上,国内不少大厂内部都有自研的大 key 巡检工具(比如蚂蚁的 Aresd、字节的 RedisProxy 都具备),会定期自动执行全库扫描。
Q2:拆开大 key 后业务代码改动很大,怎么办?
封装一层 SDK 或 DAO 层 ——让业务代码仍然以为自己在「读取一个用户」,至于内部怎么拆 key,全都由 SDK 负责。
public class UserCache {
public User get(long userId) {
int bucket = (int)(Math.abs(userId) % BUCKET_NUM);
Map<String, String> data = redis.hgetAll("user:" + bucket + ":" + userId);
return User.from(data);
}
}
业务代码里依旧是 cache.get(82001),桶的分片逻辑对上层业务完全透明。
Q3:集群模式下,像 mget 这样的多 key 操作该怎么处理?
Redis Cluster 的 mget 要求所有 key 必须在同一个 slot ——跨 slot 会直接抛出 CROSSSLOT 错误。
两种应对策略:
- 使用 hash tag :通过
{tag} 来强制 key 的 slot 归属——比如 user:{82001}:profile 和 user:{82001}:settings 就会被分配到同一个 slot;
- 客户端侧按 slot 分组 :客户端事先将 mget 的众多 key 按 slot 分组,然后分组执行 mget,最后将结果合并。
像 Redisson、Lettuce 这些常见的 Java 客户端,都已经内置了第二种策略。
Q4:业务上对「大 key」的边界是怎么定义的?
可以参考阿里的开发规约:
| 类型 |
大 key 边界 |
| String |
value > 10KB |
| Hash |
field 数 > 5000 |
| List |
元素数 > 5000 |
| Set |
元素数 > 5000 |
| ZSet |
元素数 > 5000 |
在实际生产中,这些都应被视为「警告线」,一旦某个 key 接近这个阈值,就应该考虑进行拆分了。
Q5:为什么不干脆用 hash tag 把所有 key 都强制分配到同一个 slot?
这是新手经常犯的一个错误——slot 是负载均衡的根基。把所有的 key 都圈在一个 slot 里,就等于把所有压力都打到了同一个节点,这样一来,你的 Redis 集群实际上就退化成了单机。
hash tag 只应在「必须用 mget 操作一组相关联 key」的场景下使用,切忌滥用。
一句话收口
「Redis 大 key 多 key 怎么拆」这道题,真正在考的从来就不是分桶,而是你对 Redis 单线程模型 + 集群协议成本 + 生产事故的深度理解 :
- 30 分 :知道分桶 + hash 取模;
- 60 分 :能清晰讲出 4 类场景(value 大 / 元素多 / key 多 / Bloom)各自不同的拆法;
- 90 分 :能额外补充 Bloom 拆开的陷阱、论证 UNLINK 为什么应取代 DEL、解释集群的 slot 映射成本。
写代码时,记住一个简单的心法——始终把 Redis 当成一个「很容易卡住」的单线程服务——单次操作 value 不能过大、key 的总数不能过多、删除大 key 必须走异步、跨 slot 操作必须分组。
Redis 并不是没有瓶颈,只是这些瓶颈太容易被那些「能跑就行」的早期代码给隐藏了。拆 key 不是什么高深的优化,它本质上是给主线程留出一块不被 100MB 大小的删除操作卡死的余地。
想在面试求职中应对好这类分布式组件问题,不仅需要理解Redis的运行原理,更要有扎实的后端架构功底。这是系统考察候选人实战经验深度的经典题型。