什么是 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/

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道什么是大 Key,更是想知道你是否能给出明确的阈值判断标准(多大才算大),以及大 Key 对 Redis 的 阻塞、网络、内存 三方面的具体影响。

  2. 排查能力:考察你是否知道生产环境中如何发现大 Key(redis-cli --bigkeysMEMORY USAGESCAN 扫描等),以及不同数据类型的大 Key 判定方式。

  3. 解决方案设计:能否针对不同类型的大 Key 给出对应的拆分、清理、预防方案,而不是笼统地说 "拆小一点"。

核心答案

大 Key 是指单个 Key 的 Value 占用内存过大,或者集合类元素过多的 Key。

类型大 Key 判定参考值示例
StringValue > 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 的操作(如 DELHGETALL)会长时间占用主线程,导致所有其他命令排队等待。比如删除一个 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-toolsrdb-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 预防措施

面试高频追问

  1. 追问一:UNLINKDEL 的区别?

    DEL 是同步删除,主线程直接释放内存,大 Key 会阻塞。UNLINK 是 Redis 4.0 引入的异步删除命令,主线程只做引用计数减 1,真正的内存释放由后台 bio 线程异步完成,不会阻塞主线程。生产环境删除大 Key 应该用 UNLINK

  2. 追问二:大 Key 对持久化有什么影响?

    Redis 做 RDB 持久化时需要 fork 子进程。如果 Redis 中存在大量大 Key,内存占用高,fork 的耗时也会增加(需要复制页表)。同时 AOF 重写时也需要处理大 Key,可能导致 AOF 重写耗时过长。另外,大 Key 过期时的删除操作也会影响 AOF 文件体积。

  3. 追问三: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 --bigkeysMEMORY USAGE、RDB 离线分析等手段。解决方案的核心是 拆分大 Key 为多个小 Key,配合压缩、部分读取、UNLINK 异步删除、开启 lazyfree 配置等手段。预防大于治疗,在设计和开发阶段就要控制 Key 的大小。