在 Redis 使用过程中,大 Key 或 Key 数量过多的问题。
常见场景包括:
单个 String 类型 Key 的 Value 很大;
Hash、Set、ZSet、List 中存储了过多元素;
Redis 集群中存储了上亿个 Key;
Bitmap 或 BloomFilter 占用空间过大。
Redis 的命令执行模型对大 Key 比较敏感。如果一次操作的数据量过大,可能会影响 Redis 的响应时间,甚至影响整个实例的稳定性。
所以在业务设计时,需要尽量避免大 Key。能拆分的场景,应该提前设计好拆分方案。
本文整理几种常见的大 Key 和多 Key 拆分思路。
一、什么是 Redis 大 Key
大 Key 并不只指 Key 名称很长,而是指某个 Key 对应的数据体积或元素数量过大。
例如:
String Value 很大
Hash 中有几十万 field
Set 中有几十万 member
List 中有几十万元素
ZSet 中有几十万元素
Bitmap 占用几百 MB这些都可以认为是大 Key。
大 Key 可能带来几个问题:
单次读写耗时变长;
Redis 主线程被阻塞;
网络传输变大;
内存分配和释放成本变高;
删除大 Key 时可能造成卡顿;
Redis Cluster 中容易导致单节点压力不均;
备份、迁移、同步时成本更高。
因此,Redis 中应该尽量避免单个 Key 承载过大的数据。
二、场景一:单个 String Value 很大
第一类情况是:
一个简单 Key 对应的 Value 很大例如:
user:profile:1001 -> 一个很大的 JSON 字符串
config:all -> 一个很大的配置集合
page:data -> 一个很大的页面数据结构这种场景要先判断业务访问方式。
三、每次都需要整存整取
如果这个对象每次都需要完整读取和完整写入,可以考虑把一个大 Value 拆成多个小 Value。
例如原来是:
big:user:data:1001 -> large_value可以拆成:
big:user:data:1001:part:1
big:user:data:1001:part:2
big:user:data:1001:part:3读取时使用批量读取:
List<String> values = redisTemplate.opsForValue().multiGet(keys);这种拆分的作用是:
将一次大 Value 操作拆成多次小 Value 操作,降低单个 Redis Key 的读写压力。
如果是 Redis Cluster,不同 Key 还可能分布到不同节点上,从而减少单节点压力。
不过这种方案也有代价:
客户端需要组装数据;
读写一致性要额外处理;
Key 数量会增加;
如果需要原子更新整个对象,实现会更复杂。
所以这种方式适合对象较大,但可以接受客户端拆分和组装的场景。
四、每次只访问部分数据
如果大对象不是每次都整存整取,而是只访问其中一部分字段,更适合改成 Hash。
例如原来存的是一个大 JSON:
user:1001 -> {"id":1001,"name":"zhangsan","age":18,"country":"china"}可以改成 Hash:
key: user:1001
field: id -> 1001
field: name -> zhangsan
field: age -> 18
field: country -> china读取部分字段:
HGET user:1001 name批量读取部分字段:
HMGET user:1001 name age更新单个字段:
HSET user:1001 age 19这种方式的好处是:
不需要每次读写整个对象;
可以按字段局部更新;
网络传输更小;
对业务字段访问更友好。
但也要注意,Hash 本身也可能变成大 Key。
如果一个 Hash 中 field 数量过多,仍然需要继续拆分。
五、场景二:集合类型中元素过多
第二类情况是:
Hash、Set、ZSet、List 中存储了过多元素例如:
一个 Hash 中有几十万 field
一个 Set 中有上百万 member
一个 ZSet 中有大量排序元素
一个 List 中存储了大量消息这种情况下,可以使用分桶思路。
六、Hash 分桶拆分
以 Hash 为例,原来的访问方式是:
HGET hashKey field
HSET hashKey field value
如果 hashKey 中 field 过多,可以把它拆成多个 Hash。
例如固定分成 10000 个桶。
写入时先计算 field 的 hash 值:
bucket = hash(field) % 10000然后拼出新的 Hash Key:
newHashKey = hashKey + ":" + bucket最终写入:
HSET newHashKey field value读取时使用同样的规则:
bucket = hash(field) % 10000
newHashKey = hashKey + ":" + bucket
HGET newHashKey field示例:
private static final int BUCKET_SIZE = 10000;
public String getBucketKey(String hashKey, String field) {
int bucket = Math.floorMod(field.hashCode(), BUCKET_SIZE);
return hashKey + ":" + bucket;
}
使用 Math.floorMod 是为了避免 hash 值为负数时取模结果异常。
七、Set、ZSet、List 的拆分
Set、ZSet 也可以使用类似的分桶方式。
例如 Set:
原始 Key:
user:tags
拆分后:
user:tags:0
user:tags:1
user:tags:2
...写入时:
bucket = hash(member) % bucketSize
SADD user:tags:{bucket} member读取时如果是按 member 判断是否存在,也可以计算桶后直接访问对应 Key:
SISMEMBER user:tags:{bucket} memberZSet 也可以按 member 分桶,但要注意:
如果业务需要全局排序,ZSet 分桶后就不能直接得到全局有序结果。
List 拆分更要谨慎。
如果业务要求:
LPOP 出来的数据必须严格等于最早 LPUSH 进去的数据那么简单按 hash 分桶会破坏顺序。
List 更适合按时间或业务维度拆分,例如:
queue:order:20260523
queue:order:20260524
queue:order:20260525如果要求严格全局顺序,就不建议随意拆 List。
八、场景三:Redis 中 Key 数量过多
第三类情况是:
Redis 集群中存储了上亿个 KeyKey 数量过多也会带来明显的内存开销。
内存消耗主要来自:
Key 字符串本身;
Redis 对象元数据;
字典结构开销;
Redis Cluster 中 slot 和 Key 映射相关开销;
Key 前缀重复带来的空间浪费。
例如很多 Key 都带有统一前缀:
user.123456789
user.987654321
user.678912345当 Key 数量达到上亿级时,这些元数据和字符串开销会非常明显。
这时可以考虑把多个 String Key 合并成 Hash。
九、强相关 Key 合并成 Hash
如果多个 Key 本身属于同一个对象,可以直接合并成一个 Hash。
例如原来有三个 Key:
user.zhangsan-id = 123
user.zhangsan-age = 18
user.zhangsan-country = china可以改成:
key: user.zhangsan
field: id = 123
field: age = 18
field: country = china
也就是:
HSET user.zhangsan id 123
HSET user.zhangsan age 18
HSET user.zhangsan country china这种方式可以减少 Redis 中的 Key 数量。
同时也让对象结构更清晰。
适合以下场景:
多个 Key 表示同一个对象的不同属性
多个 Key 生命周期一致
多个 Key 访问维度一致
多个 Key 业务上强相关十、无明显相关性的 Key 分桶存储
如果 Key 之间没有明显相关性,但 Key 数量非常多,也可以使用固定桶数量做 Hash 分桶。
例如预估总 Key 数是 2 亿。
如果希望每个 Hash 中存 100 个 field,那么需要:
2 亿 / 100 = 200 万个桶原始 Key:
user.123456789
user.987654321
user.678912345计算桶编号:
bucket = hash(originKey) % 2000000存储时:
HSET bucket:{bucket} originKey value读取时:
HGET bucket:{bucket} originKey示例代码:
private static final int BUCKET_COUNT = 2_000_000;
public String getBucketKey(String originKey) {
int bucket = Math.floorMod(originKey.hashCode(), BUCKET_COUNT);
return "bucket:" + bucket;
}
这种方式的好处是:
减少 Redis 顶层 Key 数量;
降低 Key 元数据开销;
保留原始 Key 作为 field;
查询仍然可以通过一次 HGET 完成。
但要注意:
单个 Hash 中的 field 数量不宜过大。
原文建议一个 Hash 中 field 控制在 100 左右,最好不要超过 512。
这不是绝对规则,但方向是对的:不要让合并后的 Hash 又变成新的大 Key。
十一、Hash 分桶需要注意的问题
1. 负数取模
Java 中 hashCode() 可能是负数。
不要直接写:
int bucket = key.hashCode() % bucketCount;更稳妥的写法是:
int bucket = Math.floorMod(key.hashCode(), bucketCount);这样可以保证桶编号是非负数。
2. 桶数量要提前评估
桶太少,单个 Hash 中 field 太多,容易形成新的大 Key。
桶太多,Redis 顶层 Key 数量又会变多。
需要结合总数据量预估。
例如:
总数据量:2 亿
目标每桶 field 数:100
桶数量:200 万3. 需要考虑扩容
如果后续数据量继续增长,原来的桶数量可能不够。
一旦改变分桶数量,原来的数据路由规则就变了。
所以分桶规则最好提前规划,或者设计版本化 Key。
例如:
bucket:v1:{bucket}
bucket:v2:{bucket}扩容时逐步迁移。
4. 不适合需要批量遍历的场景
Hash 分桶适合点查。
如果业务经常需要扫描所有数据,分桶后遍历会更复杂。
需要使用 HSCAN 或额外维护索引。
十二、场景四:大 Bitmap 或 BloomFilter
Bitmap 和 BloomFilter 常用于大规模数据判断。
例如:
用户是否存在;
用户是否签到;
商品是否命中集合;
黑名单判断;
去重判断。
这种场景下数据量通常很大。
一个 Bitmap 或 BloomFilter 很容易达到几百 MB。
例如某个 BloomFilter 占用 512MB。
这对 Redis 来说就是明显的大 Value。
十三、Bitmap / BloomFilter 拆分思路
可以将一个大 Bitmap 拆成多个小 Bitmap。
例如:
原始 Bitmap:512MB
拆分后:
1024 个 512KB 的 Bitmap拆分后的 Key:
bf:user:0
bf:user:1
bf:user:2
...
bf:user:1023关键点是:
每个业务 Key 只能落到一个 Bitmap 上。
也就是说,先通过 hash 计算它属于哪个 Bitmap:
bucket = hash(userId) % 1024然后只在这个 Bitmap 中进行位操作。
示例:
private static final int BLOOM_BUCKET_COUNT = 1024;
public String getBloomKey(String userId) {
int bucket = Math.floorMod(userId.hashCode(), BLOOM_BUCKET_COUNT);
return "bf:user:" + bucket;
}这样每次请求只需要访问一个 Redis Key。
十四、不要把拆分后的 Bitmap 当成一个整体
有一种错误拆法是:
把一个大 Bitmap 按物理空间拆开,但逻辑上仍然当成一个整体 Bitmap 使用。
这样可能导致一个业务 Key 需要访问多个 Bitmap。
如果这些 Bitmap 分布在 Redis Cluster 的不同节点上,一次判断就可能跨多个节点访问。
这会明显降低查询效率。
更好的方式是:
拆分后的每个 Bitmap 都是独立 Bitmap
通过 hash 将不同业务 Key 分配到不同 Bitmap
一次请求只访问一个 Bitmap这样才能真正降低单个 Key 的大小,同时保持查询效率。
十五、拆分后会不会增加 BloomFilter 误判率
如果拆分均匀,通常不会明显增加误判率。
BloomFilter 的误判率主要和这几个因素有关:
哈希函数个数 k
元素数量 n
Bitmap 大小 m如果拆分后,每个小 BloomFilter 中的元素数量和 Bitmap 大小按比例缩小,那么 n / m 的比例基本不变,误判率也基本不变。
关键是:
第一层分桶要尽量均匀。
如果某些桶中元素特别多,而某些桶很少,就会导致部分 BloomFilter 误判率升高。
十六、Bitmap / BloomFilter 拆分建议
实践中可以考虑:
单个 Bitmap 控制在较小范围内;
通过 hash 将业务 Key 均匀分配到不同 Bitmap;
每次请求只访问一个 Bitmap;
拆分数量要结合总数据量和 Redis 节点规划;
避免跨多个 Redis Key 查询同一个业务 Key;
对 BloomFilter 需要评估误判率。
原文中提到:
单个 BloomFilter 控制在 512KB 以下
k 取 13 个这些值可以作为参考,实际仍需要结合数据量和误判率要求计算。
十七、几类拆分方案对比
十八、实践建议
1. 优先从业务模型上避免大 Key
不要等数据量大了才拆。
设计缓存结构时就应该考虑:
单个 Key 最大会有多少数据
是否会持续增长
是否需要整存整取
是否需要局部更新
是否需要排序或遍历2. 拆分前先明确访问模式
不同访问模式适合不同拆法。
例如:
只按 ID 点查:适合 Hash 分桶;
需要全局排序:不适合简单拆 ZSet;
需要严格队列顺序:不适合按 hash 拆 List;
只访问部分属性:适合 Hash;
每次整对象访问:可以拆多个 String 后组装。
3. 避免拆完后产生新的大 Key
把很多 String 合成 Hash 可以减少 Key 数量。
但如果一个 Hash 中 field 太多,又会变成新的大 Key。
所以需要控制每个桶中的元素数量。
4. 提前考虑扩容和迁移
分桶数量一旦确定,后续修改会影响路由规则。
建议设计时预留空间,或者给 Key 加版本号,方便以后迁移。
5. 删除大 Key 要谨慎
大 Key 删除可能阻塞 Redis。
如果 Redis 版本支持,可以优先使用:
UNLINK而不是:
DELUNLINK 会异步释放内存,更适合删除大对象。
结论
Redis 大 Key 和 Key 数量过多都会带来性能和稳定性问题。
常见的大 Key 场景包括:
单个 String Value 很大;
Hash、Set、ZSet、List 中元素过多;
Redis 集群中 Key 数量过多;
Bitmap 或 BloomFilter 占用空间过大。
对应处理思路是:
大 String:拆成多个 Key,或改成 Hash;
大 Hash / Set / ZSet:按 field 或 member 分桶;
大 List:按时间或业务维度拆分,注意顺序语义;
Key 数量过多:将多个相关 Key 合并成 Hash,或做 Hash 分桶;
大 Bitmap / BloomFilter:拆成多个独立 Bitmap,并保证每个业务 Key 只访问一个 Bitmap。
Redis 大 Key 的治理核心不是简单地“拆”,而是根据访问模式选择合适的数据结构和分桶规则,既要降低单个 Key 的压力,也要避免拆分后引入新的复杂度。