Redis 大 Key 与多 Key 拆分方案

作者:old wang 发布时间: 2026-05-23 阅读量:4 评论数:0

在 Redis 使用过程中,大 Key 或 Key 数量过多的问题。

常见场景包括:

  1. 单个 String 类型 Key 的 Value 很大;

  2. Hash、Set、ZSet、List 中存储了过多元素;

  3. Redis 集群中存储了上亿个 Key;

  4. 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 可能带来几个问题:

  1. 单次读写耗时变长;

  2. Redis 主线程被阻塞;

  3. 网络传输变大;

  4. 内存分配和释放成本变高;

  5. 删除大 Key 时可能造成卡顿;

  6. Redis Cluster 中容易导致单节点压力不均;

  7. 备份、迁移、同步时成本更高。

因此,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 还可能分布到不同节点上,从而减少单节点压力。

不过这种方案也有代价:

  1. 客户端需要组装数据;

  2. 读写一致性要额外处理;

  3. Key 数量会增加;

  4. 如果需要原子更新整个对象,实现会更复杂。

所以这种方式适合对象较大,但可以接受客户端拆分和组装的场景。

四、每次只访问部分数据

如果大对象不是每次都整存整取,而是只访问其中一部分字段,更适合改成 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

这种方式的好处是:

  1. 不需要每次读写整个对象;

  2. 可以按字段局部更新;

  3. 网络传输更小;

  4. 对业务字段访问更友好。

但也要注意,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} member

ZSet 也可以按 member 分桶,但要注意:

如果业务需要全局排序,ZSet 分桶后就不能直接得到全局有序结果。

List 拆分更要谨慎。

如果业务要求:

LPOP 出来的数据必须严格等于最早 LPUSH 进去的数据

那么简单按 hash 分桶会破坏顺序。

List 更适合按时间或业务维度拆分,例如:

queue:order:20260523
queue:order:20260524
queue:order:20260525

如果要求严格全局顺序,就不建议随意拆 List。

八、场景三:Redis 中 Key 数量过多

第三类情况是:

Redis 集群中存储了上亿个 Key

Key 数量过多也会带来明显的内存开销。

内存消耗主要来自:

  1. Key 字符串本身;

  2. Redis 对象元数据;

  3. 字典结构开销;

  4. Redis Cluster 中 slot 和 Key 映射相关开销;

  5. 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;
}

这种方式的好处是:

  1. 减少 Redis 顶层 Key 数量;

  2. 降低 Key 元数据开销;

  3. 保留原始 Key 作为 field;

  4. 查询仍然可以通过一次 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 拆分建议

实践中可以考虑:

  1. 单个 Bitmap 控制在较小范围内;

  2. 通过 hash 将业务 Key 均匀分配到不同 Bitmap;

  3. 每次请求只访问一个 Bitmap;

  4. 拆分数量要结合总数据量和 Redis 节点规划;

  5. 避免跨多个 Redis Key 查询同一个业务 Key;

  6. 对 BloomFilter 需要评估误判率。

原文中提到:

单个 BloomFilter 控制在 512KB 以下
k 取 13 个

这些值可以作为参考,实际仍需要结合数据量和误判率要求计算。

十七、几类拆分方案对比

场景

问题

拆分思路

单个 String Value 很大

单次读写压力大

拆成多个 Key,或改成 Hash

Hash / Set / ZSet 元素过多

单个集合过大

按 field/member 分桶

List 元素过多

单个队列过大

按时间或业务维度拆分

Redis Key 数量过多

元数据和 Key 本身占用大

多个 String 合并成 Hash

大 Bitmap / BloomFilter

单个 Value 太大

按 hash 拆成多个独立 Bitmap

十八、实践建议

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

而不是:

DEL

UNLINK 会异步释放内存,更适合删除大对象。

结论

Redis 大 Key 和 Key 数量过多都会带来性能和稳定性问题。

常见的大 Key 场景包括:

  1. 单个 String Value 很大;

  2. Hash、Set、ZSet、List 中元素过多;

  3. Redis 集群中 Key 数量过多;

  4. Bitmap 或 BloomFilter 占用空间过大。

对应处理思路是:

  • 大 String:拆成多个 Key,或改成 Hash;

  • 大 Hash / Set / ZSet:按 field 或 member 分桶;

  • 大 List:按时间或业务维度拆分,注意顺序语义;

  • Key 数量过多:将多个相关 Key 合并成 Hash,或做 Hash 分桶;

  • 大 Bitmap / BloomFilter:拆成多个独立 Bitmap,并保证每个业务 Key 只访问一个 Bitmap。

Redis 大 Key 的治理核心不是简单地“拆”,而是根据访问模式选择合适的数据结构和分桶规则,既要降低单个 Key 的压力,也要避免拆分后引入新的复杂度。

评论