什么是 Redis 大 Key 问题,如何解决?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
基础掌握度:面试官不仅仅是想知道什么是大 Key,更是想知道你是否能给出明确的阈值判断标准(多大才算大),以及大 Key 对 Redis 的 阻塞、网络、内存 三方面的具体影响。
-
排查能力:考察你是否知道生产环境中如何发现大 Key(
redis-cli --bigkeys、MEMORY USAGE、SCAN扫描等),以及不同数据类型的大 Key 判定方式。 -
解决方案设计:能否针对不同类型的大 Key 给出对应的拆分、清理、预防方案,而不是笼统地说 "拆小一点"。
核心答案
大 Key 是指单个 Key 的 Value 占用内存过大,或者集合类元素过多的 Key。
| 类型 | 大 Key 判定参考值 | 示例 |
|---|---|---|
| String | Value > 10KB(大于 5MB 严重) | 一个 5MB 的 JSON 缓存 |
| Hash | 元素数量 > 5000,或总内存 > 10MB | 一个 100 万 field 的用户画像 Hash |
| List | 元素数量 > 5000,或总内存 > 10MB | 一个 20 万条的消息队列 List |
| Set | 元素数量 > 5000,或总内存 > 10MB | 一个 50 万个元素的黑名单 Set |
| ZSet | 元素数量 > 5000,或总内存 > 10MB | 一个 10 万条的全服排行榜 ZSet |
注意:以上数值并不是绝对的,是个经验值,具体还需要根据实际情况来判断。
一句话结论:大 Key 的核心危害是 阻塞 Redis 主线程(操作耗时长)和 网络拥塞(传输耗时久)。解决方案是 拆分(减小粒度)+ 异步删除(UNLINK)+ 预防(设置阈值)。
深度解析
一、大 Key 到底有什么危害?
上图展示了大 Key 的三大核心危害:
- 阻塞主线程:Redis 单线程模型下,对大 Key 的操作(如
DEL、HGETALL)会长时间占用主线程,导致所有其他命令排队等待。比如删除一个 100MB 的 Key,释放内存可能需要数百毫秒。 - 网络拥塞:读取大 Key 时需要一次性传输大量数据,可能占满服务器网卡带宽,导致其他客户端的请求超时。
- 内存倾斜:在 Redis Cluster 中,大 Key 集中在某个节点上,导致该节点内存远高于其他节点,可能提前触发淘汰策略或 OOM。
此外,大 Key 过期时的被动删除、持久化时 fork 子进程复制大 Key,都会额外消耗资源。
二、如何发现大 Key?
方法一:redis-cli --bigkeys(最快)
# 扫描整个实例,给出每种数据类型最大的 Key
redis-cli --bigkeys -i 0.1
# 输出示例:
# -------- summary -------
# Biggest string found: user:1001 (5.2 MB)
# Biggest list found: queue:messages (230000 elements)
# Biggest set found: blacklist (850000 elements)
# Biggest hash found: user:profile (120000 fields)
# Biggest zset found: rank:global (95000 members)
关键点:
- 使用
SCAN遍历,对线上影响较小,可以安全使用。 -i 0.1表示每 100 条命令休眠 0.1 秒,降低对 Redis 的影响。- 局限:只能找到每种数据类型最大的一个 Key,无法列出所有大 Key。
方法二:MEMORY USAGE(精确)
# 查看某个 Key 占用的内存字节数
MEMORY USAGE user:1001
# 返回:5452000(约 5.2MB)
# 批量检查
redis-cli SCAN 0 COUNT 100 | while read key; do
echo "$key: $(redis-cli MEMORY USAGE $key) bytes"
done
关键点:
- 精确返回 Key 在内存中占用的字节数,包括 SDS 头部、字典结构等开销。
- 可以结合
SCAN脚本批量扫描,找出超过阈值的所有 Key。 - 注意:每个 Key 都要单独调用,大量 Key 时耗时较长。
方法三:DEBUG OBJECT(辅助)
DEBUG OBJECT user:1001
# 返回:Value at:0x7f... refcount:1 encoding:raw serializedlength:5242880
# ↑ 序列化后大小(字节)
方法四:RDB 分析工具(离线分析)
# 使用 redis-rdb-tools 离线分析 RDB 文件
rdb --command json dump.rdb | python analyze.py
# 或使用 redis-cli 的 --rdb 导出后分析
关键点:
- 从 RDB 快照文件中离线分析,对线上服务零影响。
- 适合定期巡检,生成大 Key 报告。
- 常用工具:
redis-rdb-tools、rdb-tools。
三、大 Key 的解决方案
上图展示了大 Key 的五种解决方案:
- 拆分:把一个大 Key 拆成多个小 Key,是最根本的解决方案。
- 压缩:存入前先压缩(GZIP / Snappy),减少 Value 体积。
- 部分读取:不要
HGETALL/SMEMBERS全量读取,改用HSCAN/HGET分批或按需读取。 - 安全删除:不要用
DEL删除大 Key,用UNLINK(Redis 4.0+)异步删除,或用HSCAN+HDEL分批删除。 - 懒删除配置:开启
lazyfree-lazy-expire yes,让过期删除也在后台线程异步执行。
四、不同类型大 Key 的拆分实战
Hash 大 Key 拆分:
/**
* Hash 大 Key 拆分示例
* 拆前:user:profile:1001(10 万个 field)
* 拆后:user:profile:1001:1 ~ user:profile:1001:100(每个 1000 个 field)
*/
public class HashBigKeySplitter {
// 按 field 名的 hash 值取模,决定放到哪个分片 Key 中
private static final int SHARD_COUNT = 100;
private String getShardKey(String baseKey, String field) {
int shard = Math.abs(field.hashCode() % SHARD_COUNT);
return baseKey + ":" + shard;
}
// 写入
public void hset(String baseKey, String field, String value) {
String shardKey = getShardKey(baseKey, field);
redis.hset(shardKey, field, value);
}
// 读取
public String hget(String baseKey, String field) {
String shardKey = getShardKey(baseKey, field);
return redis.hget(shardKey, field);
}
}
List 大 Key 拆分:
/**
* List 大 Key 拆分示例
* 拆前:queue:messages(100 万条消息)
* 拆后:queue:messages:0 ~ queue:messages:99(每个队列约 1 万条)
*/
public class ListBigKeySplitter {
private static final int SHARD_COUNT = 100;
// 写入:按轮询方式写入不同分片
public void lpush(String baseKey, String value) {
long index = redis.incr(baseKey + ":index");
int shard = (int) (index % SHARD_COUNT);
redis.lpush(baseKey + ":" + shard, value);
}
// 读取:从指定分片读取
public String rpop(String baseKey, int shard) {
return redis.rpop(baseKey + ":" + shard);
}
}
String 大 Key(大 JSON)压缩:
/**
* String 大 Key 压缩示例
* 5MB JSON → GZIP 压缩后约 500KB
*/
public class CompressedCache {
public void set(String key, Object data) throws IOException {
// 1. 序列化为 JSON
String json = objectMapper.writeValueAsString(data);
// 2. GZIP 压缩
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) {
gzip.write(json.getBytes(StandardCharsets.UTF_8));
}
// 3. 存入 Redis(标记为压缩数据)
redis.set(key, bos.toByteArray(), 30, TimeUnit.MINUTES);
}
public <T> T get(String key, Class<T> clazz) throws IOException {
byte[] compressed = redis.get(key);
if (compressed == null) return null;
// 4. GZIP 解压
try (GZIPInputStream gzip = new GZIPInputStream(
new ByteArrayInputStream(compressed))) {
String json = new String(gzip.readAllBytes(), StandardCharsets.UTF_8);
return objectMapper.readValue(json, clazz);
}
}
}
五、安全删除大 Key 的正确姿势
上图展示了大 Key 的四种删除方式:
- 直接
DEL(危险):同步释放内存,如果 Key 很大会阻塞主线程数百毫秒甚至数秒,生产环境严禁使用。 UNLINK(推荐):Redis 4.0+ 提供,主线程只做标记删除立刻返回,后台线程(bio线程)异步释放内存,不阻塞主线程。- 分批删除:用
HSCAN+HDEL(Hash)、SSCAN+SREM(Set)等方式分批删除集合中的元素,最后删除 Key 本身。 - lazyfree 配置:开启 Redis 的懒删除配置,让淘汰、过期、隐式删除等场景也走异步删除,避免被动触发阻塞。
分批删除 Hash 大 Key 的代码示例:
/**
* 安全删除 Hash 大 Key
* 使用 HSCAN + HDEL 分批删除,避免阻塞
*/
public void safeDeleteHashKey(String key) {
String cursor = "0";
ScanParams params = new ScanParams().count(200);
do {
// 1. 分批扫描 field
ScanResult<Map.Entry<String, String>> scanResult =
redis.hscan(key, cursor, params);
cursor = scanResult.getCursor();
// 2. 批量删除这批 field
List<String> fields = scanResult.getResult().stream()
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!fields.isEmpty()) {
redis.hdel(key, fields.toArray(new String[0]));
}
} while (!"0".equals(cursor));
// 3. 删除 Key 本身(此时已经是空 Hash 了)
redis.del(key);
}
六、大 Key 预防措施
面试高频追问
-
追问一:
UNLINK和DEL的区别?DEL是同步删除,主线程直接释放内存,大 Key 会阻塞。UNLINK是 Redis 4.0 引入的异步删除命令,主线程只做引用计数减 1,真正的内存释放由后台bio线程异步完成,不会阻塞主线程。生产环境删除大 Key 应该用UNLINK。 -
追问二:大 Key 对持久化有什么影响?
Redis 做 RDB 持久化时需要
fork子进程。如果 Redis 中存在大量大 Key,内存占用高,fork的耗时也会增加(需要复制页表)。同时 AOF 重写时也需要处理大 Key,可能导致 AOF 重写耗时过长。另外,大 Key 过期时的删除操作也会影响 AOF 文件体积。 -
追问三:Redis Cluster 中大 Key 有什么特殊问题?
大 Key 会集中在某个 slot(即某个节点)上,导致 内存倾斜,该节点内存远高于其他节点。更严重的是,大 Key 涉及的跨 slot 操作可能失败。解决方案是使用
{hash_tag}确保相关 Key 在同一 slot,或者从根本上拆分大 Key。
常见面试变体
- 变体一:"Redis 中一个 Key 能存多大?有限制吗?"
- 变体二:"线上发现大 Key 怎么处理?"
- 变体三:"如何安全删除一个上百 MB 的 Key?"
- 变体四:"如何在线上扫描 Redis 中的大 Key?"
记忆口诀
大 Key 三害:阻塞主线程、网络拥塞、内存倾斜。
发现四招:--bigkeys(快速扫描)、MEMORY USAGE(精确测量)、DEBUG OBJECT(辅助查看)、RDB 离线分析(零影响)。
解决五策:拆分(根本)、压缩(减体积)、部分读(不全量)、UNLINK(安全删)、lazyfree(预防)。
阈值红线:String 超 10KB、集合超 5000 个元素,就该拆了。
总结
Redis 大 Key 问题是指单个 Key 的 Value 过大或集合元素过多,会导致 阻塞主线程、网络拥塞、内存倾斜 等严重问题。发现大 Key 可以用 redis-cli --bigkeys、MEMORY USAGE、RDB 离线分析等手段。解决方案的核心是 拆分大 Key 为多个小 Key,配合压缩、部分读取、UNLINK 异步删除、开启 lazyfree 配置等手段。预防大于治疗,在设计和开发阶段就要控制 Key 的大小。