Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Redis核心数据结构
  • Redis持久化机制
  • Redis高可用架构
  • Redis分布式锁
  • Redis缓存设计
  • Redis大Key与热Key
    • 一、核心概念与本质区别
    • 二、大 Key 问题深度解析
      • 2.1 什么是大 Key?
      • 2.2 大 Key 的核心危害(底层原理+业务影响)
      • 2.3 大 Key 的识别方法(实战命令+工具)
      • 方法1:Redis 原生命令(基础排查)
      • (1)扫描大 Key(redis-cli --bigkeys)
      • (2)精准查询 Key 内存(MEMORY USAGE)
      • 方法2:自定义 Scan 脚本(精准筛选,生产推荐)
      • 方法3:第三方工具(可视化+高效)
      • 2.4 大 Key 的解决方案(按优先级排序)
      • 方案1:拆分大 Key(根治方案,首选)
      • (1)String 类型拆分(垂直拆分)
      • (2)集合类型拆分(水平分片)
      • 方案2:数据压缩(临时优化,配合拆分)
      • 方案3:冷热分离(大 Key 降级)
      • 方案4:异步/分批操作(避免阻塞)
      • 方案5:调整 Redis 配置(兜底优化)
    • 三、热 Key 问题深度解析
      • 3.1 什么是热 Key?
      • 3.2 热 Key 的核心危害
      • 3.3 热 Key 的识别方法
      • 方法1:监控指标(生产首选)
      • 方法2:Redis 原生命令(快速排查)
      • 方法3:客户端埋点(精准定位)
      • 3.4 热 Key 的解决方案(按优先级排序)
      • 方案1:本地缓存(Caffeine)+ 多级缓存(首选)
      • 方案2:热 Key 分散(哈希分片)
      • 方案3:读写分离 + 主从复制
      • 方案4:熔断限流(兜底保护)
      • 方案5:预加载 + 永不过期
    • 四、大 Key + 热 Key 叠加问题(最高风险)
      • 解决方案:组合策略
    • 五、最佳实践总结
      • 5.1 预防策略(核心)
      • 5.2 治理策略
      • 5.3 应急策略
    • 六、核心区别与对比表
  • Redis限流
  • Redis IO多路复用
  • Redis过期删除策略
  • Redis Bitmap
  • 《Redis》笔记
Tavio
2023-05-03
目录

Redis大Key与热Key

Redis 中的大 Key 和 热 Key 是高并发/大规模场景下的两大核心性能瓶颈,前者因“数据体积过大”导致内存、网络、操作阻塞问题,后者因“访问频率过高”引发单节点过载、服务不可用风险。两者可能单独出现,也可能叠加(如一个大 Key 同时是热 Key,危害呈指数级放大)。

# 一、核心概念与本质区别

先明确两者的核心差异,避免混淆:

维度 大 Key(Big Key) 热 Key(Hot Key)
核心特征 数据体积/元素数量大(占用内存多) 访问频率极高(QPS 远高于普通 Key)
判定标准 String:>10KB;Hash/ZSet/List:元素数>1000个 单 Key QPS > 1000(或占节点总 QPS 10% 以上)
核心危害 内存倾斜、操作阻塞、传输缓慢 单节点过载、网卡打满、缓存击穿/雪崩
影响范围 主要影响 Redis 存储/操作性能 主要影响 Redis 访问/网络性能
典型场景 存储全量商品列表、用户全量订单、大文本内容 秒杀商品、首页热门数据、高频查询的配置 Key

# 二、大 Key 问题深度解析

# 2.1 什么是大 Key?

大 Key 不是指“Key 名称长”,而是指 Key 对应的 Value 数据体积过大,或集合类型(Hash/ZSet/List/Set)的元素数量过多。行业通用判定阈值(可根据业务调整):

  • String 类型:Value 大小 ≥ 10KB(核心业务建议 ≤ 5KB);
  • 集合类型:
    • Hash/ZSet/Set:元素数量 ≥ 1000 个(核心业务 ≤ 500 个);
    • List:元素数量 ≥ 5000 个(或列表长度 ≥ 1000)。

⚠️ 注意:即使单 Key 内存不大(如 5KB),但如果是百万级数量的此类 Key,也会累计成“大内存占用”,但不属于本文讨论的“大 Key”(属于“内存膨胀”问题)。

# 2.2 大 Key 的核心危害(底层原理+业务影响)

Redis 是单线程事件循环模型,大 Key 操作会阻塞主线程,引发一系列连锁反应:

危害类型 底层原理 业务影响示例
内存分布不均(集群倾斜) 大 Key 集中在某一节点,导致集群节点内存使用率差异>50%,触发内存淘汰/宕机 节点 A 内存 90%,节点 B 内存 30%
操作阻塞(DEL/EXPIRE) 单线程执行 DEL 大 Key 时,需遍历释放所有内存,阻塞毫秒级→秒级,期间无法处理其他请求 服务响应超时、接口 500 报错
网络传输缓慢 大 Key 序列化/反序列化、网络传输耗时久,占用带宽 客户端读取超时、Redis 网卡瓶颈
主从复制卡顿 大 Key 同步时占用大量网络/CPU,导致主从延迟>秒级,数据一致性风险 从库数据滞后,故障切换时丢数据
RDB/AOF 性能下降 Fork 子进程时,大 Key 拷贝耗时久;AOF 写入时,大 Key 操作日志体积大 备份耗时翻倍、AOF 文件膨胀
扩容/迁移失败 集群扩容时,大 Key 迁移耗时超阈值,触发迁移超时/失败 集群扩容中断、服务不可用

# 2.3 大 Key 的识别方法(实战命令+工具)

# 方法1:Redis 原生命令(基础排查)

# (1)扫描大 Key(redis-cli --bigkeys)
# 扫描所有 Key,统计各类型大 Key,输出汇总(耗时久,生产建议低峰期执行)
redis-cli -h {host} -p {port} -a {password} --bigkeys

# 输出示例(重点看“Biggest”部分):
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec per 100 SCAN commands to reduce server load.
# [00.00%] Biggest string key: "goods:detail:1001" (size: 25600 bytes)
# [00.00%] Biggest hash key: "user:order:10086" (fields: 5000)
# [00.00%] Biggest zset key: "rank:hot:goods" (elements: 8000)
1
2
3
4
5
6
7
8
9

⚠️ 注意:--bigkeys 仅统计“元素数最多”的集合 Key 和“体积最大”的 String Key,无法自定义阈值(比如只查>10KB 的 String),且会遍历全量 Key,高并发时慎用(可加 -i 0.1 降低扫描压力)。

# (2)精准查询 Key 内存(MEMORY USAGE)
# 查询单个 Key 的内存占用(单位:字节,包含元数据)
redis-cli MEMORY USAGE "goods:detail:1001"

# 输出示例:25689(≈25KB,判定为大 Key)
1
2
3
4

# 方法2:自定义 Scan 脚本(精准筛选,生产推荐)

通过 SCAN 遍历所有 Key,结合 MEMORY USAGE/HLEN/ZCARD 等命令筛选符合阈值的大 Key,避免全量遍历阻塞:

import redis

# 连接 Redis
r = redis.Redis(host="127.0.0.1", port=6379, password="xxx", decode_responses=True)

# 大 Key 阈值
STRING_THRESHOLD = 10 * 1024  # 10KB
HASH_THRESHOLD = 1000          # Hash 元素数阈值

# 扫描 Key(游标遍历,避免阻塞)
cursor = 0
big_keys = []
while True:
    cursor, keys = r.scan(cursor, count=1000)  # 每次扫描1000个Key
    for key in keys:
        # 获取 Key 类型
        key_type = r.type(key)
        if key_type == "string":
            # String 类型:判断内存大小
            mem = r.memory_usage(key)
            if mem >= STRING_THRESHOLD:
                big_keys.append({"key": key, "type": "string", "size": mem})
        elif key_type == "hash":
            # Hash 类型:判断元素数
            hlen = r.hlen(key)
            if hlen >= HASH_THRESHOLD:
                big_keys.append({"key": key, "type": "hash", "fields": hlen})
        # 可扩展 ZSet/List/Set 类型的判断
    if cursor == 0:
        break

# 输出大 Key 列表
print("发现大 Key 数量:", len(big_keys))
for k in big_keys:
    print(k)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 方法3:第三方工具(可视化+高效)

  • RedisInsight(Redis 官方工具):可视化查看 Key 内存、元素数,支持按大小/类型筛选大 Key;
  • RedisShake(阿里开源):离线解析 RDB 文件,快速定位大 Key(无需在线扫描,无性能影响);
  • Prometheus + Grafana:通过 redis_key_size 指标(需部署 Redis Exporter),监控单 Key 内存变化,设置大 Key 告警阈值。

# 2.4 大 Key 的解决方案(按优先级排序)

核心思路:拆分为主,压缩为辅,冷热分离兜底,操作异步化。

# 方案1:拆分大 Key(根治方案,首选)

根据数据类型不同,拆分策略不同:

# (1)String 类型拆分(垂直拆分)

将大文本/大对象拆分为多个小 String Key,比如:

  • 原 Key:goods:detail:1001(存储商品所有详情,20KB);
  • 拆分后:
    • goods:name:1001(商品名称,1KB);
    • goods:price:1001(商品价格,0.5KB);
    • goods:desc:1001(商品描述,15KB → 再拆分为 goods:desc:1001:1/goods:desc:1001:2)。

代码示例(Java):

// 拆分存储
public void saveGoodsDetail(Goods goods) {
    // 基础信息拆分
    redisTemplate.opsForValue().set("goods:name:" + goods.getId(), goods.getName());
    redisTemplate.opsForValue().set("goods:price:" + goods.getId(), goods.getPrice().toString());
    // 长描述拆分(按10KB拆分)
    String desc = goods.getDesc();
    int chunkSize = 10 * 1024; // 10KB/段
    int chunks = (desc.length() + chunkSize - 1) / chunkSize;
    for (int i = 0; i < chunks; i++) {
        int start = i * chunkSize;
        int end = Math.min((i + 1) * chunkSize, desc.length());
        String chunk = desc.substring(start, end);
        redisTemplate.opsForValue().set("goods:desc:" + goods.getId() + ":" + i, chunk);
    }
}

// 合并读取
public Goods getGoodsDetail(Long id) {
    Goods goods = new Goods();
    goods.setId(id);
    goods.setName((String) redisTemplate.opsForValue().get("goods:name:" + id));
    goods.setPrice(new BigDecimal((String) redisTemplate.opsForValue().get("goods:price:" + id)));
    // 合并描述
    StringBuilder desc = new StringBuilder();
    int i = 0;
    while (true) {
        String chunk = (String) redisTemplate.opsForValue().get("goods:desc:" + id + ":" + i);
        if (chunk == null) break;
        desc.append(chunk);
        i++;
    }
    goods.setDesc(desc.toString());
    return goods;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# (2)集合类型拆分(水平分片)

以 Hash 为例(存储用户10000条订单):

  • 原 Key:user:order:10086(Hash,10000个字段);
  • 拆分后:按订单 ID 哈希分片,拆为10个小 Hash:
    • user:order:10086:0(订单 ID % 10 = 0);
    • user:order:10086:1(订单 ID % 10 = 1);
    • ...
    • user:order:10086:9(订单 ID % 10 = 9)。

代码示例(Java):

// 分片存储订单
public void saveUserOrder(Long userId, Order order) {
    // 按订单ID哈希分片(10个分片)
    int shard = (int) (order.getId() % 10);
    String shardKey = "user:order:" + userId + ":" + shard;
    // 存储到对应分片Hash
    redisTemplate.opsForHash().put(shardKey, order.getId().toString(), order);
}

// 分片查询订单
public Order getUserOrder(Long userId, Long orderId) {
    int shard = (int) (orderId % 10);
    String shardKey = "user:order:" + userId + ":" + shard;
    return (Order) redisTemplate.opsForHash().get(shardKey, orderId.toString());
}

// 批量查询用户订单(遍历所有分片)
public List<Order> listUserOrders(Long userId) {
    List<Order> orders = new ArrayList<>();
    for (int shard = 0; shard < 10; shard++) {
        String shardKey = "user:order:" + userId + ":" + shard;
        // 用hscan分批遍历,避免hgetall阻塞
        Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(
            shardKey, ScanOptions.scanOptions().count(100).build()
        );
        while (cursor.hasNext()) {
            Map.Entry<Object, Object> entry = cursor.next();
            orders.add((Order) entry.getValue());
        }
    }
    return orders;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 方案2:数据压缩(临时优化,配合拆分)

对无法拆分的 String 大 Key(如序列化后的对象),通过压缩降低体积:

  • 压缩算法:Snappy(高性能)、GZIP(高压缩比,CPU 消耗高);
  • 适用场景:冷/温大 Key(访问频率低,可接受压缩/解压缩耗时)。

代码示例(Java + Snappy):

import org.xerial.snappy.Snappy;

// 压缩存储
public void saveBigString(String key, String value) throws IOException {
    byte[] rawBytes = value.getBytes(StandardCharsets.UTF_8);
    byte[] compressedBytes = Snappy.compress(rawBytes);
    redisTemplate.opsForValue().set(key, compressedBytes);
}

// 解压缩读取
public String getBigString(String key) throws IOException {
    byte[] compressedBytes = (byte[]) redisTemplate.opsForValue().get(key);
    if (compressedBytes == null) return null;
    byte[] rawBytes = Snappy.uncompress(compressedBytes);
    return new String(rawBytes, StandardCharsets.UTF_8);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 方案3:冷热分离(大 Key 降级)

将大 Key 中的冷数据迁移到低成本存储(如 HBase、Elasticsearch、MySQL),Redis 仅保留热数据:

  • 示例:用户订单大 Key 中,仅缓存近3个月的热订单(Redis),3个月前的冷订单存储到 HBase;
  • 读取逻辑:先查 Redis 热数据,未命中则查 HBase 冷数据。

# 方案4:异步/分批操作(避免阻塞)

  • 删除大 Key:用 UNLINK 代替 DEL(Redis 4.0+ 支持),UNLINK 异步释放内存,不阻塞主线程:
  # 异步删除大 Key(推荐)
  UNLINK "user:order:10086"
  # 对比:DEL 会阻塞主线程
  DEL "user:order:10086"
1
2
3
4
  • 遍历大集合:用 HSCAN/ZSCAN/LSCAN 代替 HGETALL/ZRANGE/LRANGE 0 -1,分批遍历(每次100条),避免一次性读取所有元素阻塞线程。

# 方案5:调整 Redis 配置(兜底优化)

  • 关闭大 Key 的过期时间:若大 Key 必须存在,尽量不设置 EXPIRE(过期删除会阻塞),改为业务层异步清理;
  • 增大 client-output-buffer-limit:避免大 Key 传输时触发客户端输出缓冲区限制,断开连接(仅临时调整)。

# 三、热 Key 问题深度解析

# 3.1 什么是热 Key?

热 Key 是指单 Key 被高频次访问,导致该 Key 所在的 Redis 节点成为“热点节点”,占用节点绝大部分 CPU、内存、网络资源。行业通用判定标准:

  • 单 Key QPS ≥ 1000(核心业务建议 ≤ 500);
  • 单 Key 访问量占所在节点总访问量的 10% 以上;
  • 热点持续时间 ≥ 10 秒(瞬时热点可忽略)。

⚠️ 注意:热 Key 不一定是大 Key,但如果热 Key 同时是大 Key(如 10KB 的 String 热 Key),会同时引发“访问高频+传输缓慢”,危害翻倍。

# 3.2 热 Key 的核心危害

热 Key 会直接压垮单个 Redis 节点,进而引发整个集群的性能问题:

危害类型 底层原理 业务影响示例
单节点 CPU 满载 高频命令(GET/SET)占用节点 CPU 100%,无法处理其他请求 节点响应超时,服务雪崩
网卡带宽打满 热 Key 高频传输,占用节点网卡 90% 以上带宽,其他 Key 传输被阻塞 网络延迟飙升,客户端连接超时
集群负载不均 热 Key 固定在某一节点(Redis 集群按 Key 哈希分片),节点负载差异>80% 节点宕机,集群故障切换
缓存击穿/雪崩 热 Key 过期/失效时,大量请求穿透到数据库,压垮 DB 数据库宕机,服务不可用
客户端连接数耗尽 高频访问导致客户端到热点节点的连接数达到上限,新连接被拒绝 接口报错“Could not get resource”

# 3.3 热 Key 的识别方法

# 方法1:监控指标(生产首选)

通过 Prometheus + Grafana + Redis Exporter 监控以下指标:

  • redis_key_space_hits:单 Key 命中次数(需开启 redis-cli config set keyspace_events KEA);
  • redis_command_stats:统计 GET/SET 等命令的执行次数,定位高频命令对应的 Key;
  • redis_server_net_input_bytes/redis_server_net_output_bytes:单节点网络流量,定位热点节点。

# 方法2:Redis 原生命令(快速排查)

# 查看命令执行统计(按执行次数排序)
redis-cli INFO commandstats | grep -E "cmdstat_|calls" | sort -k2 -nr

# 输出示例(GET 命令执行100万次,远高于其他命令):
# cmdstat_get:calls=1000000,usec=500000,usec_per_call=0.50
# cmdstat_set:calls=100000,usec=100000,usec_per_call=1.00
1
2
3
4
5
6

# 方法3:客户端埋点(精准定位)

在业务代码中统计每个 Key 的访问次数,超过阈值则标记为热 Key:

@Service
public class HotKeyMonitorService {
    // 访问次数计数器(本地缓存,定时清理)
    private final Map<String, AtomicInteger> keyCounter = new ConcurrentHashMap<>();
    // 热 Key 阈值(QPS ≥ 1000)
    private static final int HOT_KEY_THRESHOLD = 1000;
    // 统计周期(10秒)
    private static final long STAT_PERIOD = 10 * 1000;

    // 初始化定时任务:每10秒检测热 Key
    @PostConstruct
    public void initMonitor() {
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleAtFixedRate(this::detectHotKey, 0, STAT_PERIOD, TimeUnit.MILLISECONDS);
    }

    // 记录 Key 访问
    public void recordKeyAccess(String key) {
        keyCounter.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet();
    }

    // 检测热 Key
    private void detectHotKey() {
        List<String> hotKeys = new ArrayList<>();
        long qpsThreshold = HOT_KEY_THRESHOLD;
        for (Map.Entry<String, AtomicInteger> entry : keyCounter.entrySet()) {
            int count = entry.getValue().get();
            long qps = count / (STAT_PERIOD / 1000);
            if (qps >= qpsThreshold) {
                hotKeys.add(entry.getKey() + " (QPS: " + qps + ")");
            }
            // 重置计数器
            entry.getValue().set(0);
        }
        if (!hotKeys.isEmpty()) {
            log.warn("检测到热 Key:{}", String.join(", ", hotKeys));
        }
    }
}

// 业务层集成埋点
@Service
public class GoodsService {
    @Autowired
    private HotKeyMonitorService hotKeyMonitorService;

    public Goods getGoodsById(Long id) {
        String key = "goods:id:" + id;
        // 记录 Key 访问
        hotKeyMonitorService.recordKeyAccess(key);
        // 后续查询逻辑...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 3.4 热 Key 的解决方案(按优先级排序)

核心思路:分散访问压力,降低热点节点负载,避免单点依赖。

# 方案1:本地缓存(Caffeine)+ 多级缓存(首选)

在应用层(JVM)增加本地缓存(如 Caffeine),缓存热 Key 数据,减少对 Redis 的直接访问:

  • 逻辑:请求先查本地缓存 → 未命中查 Redis → 未命中查 DB → 更新 Redis + 本地缓存;
  • 优势:本地缓存访问耗时<1ms,可承接 90% 以上的热 Key 请求;
  • 注意:本地缓存需设置短过期时间(2~5分钟),并通过消息队列同步更新(避免数据不一致)。

代码示例(Java + Caffeine):

// 配置本地缓存
@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<String, Object> localHotKeyCache() {
        return Caffeine.newBuilder()
                .maximumSize(1000) // 缓存1000个热 Key
                .expireAfterWrite(3, TimeUnit.MINUTES) // 3分钟过期
                .recordStats() // 开启统计
                .build();
    }
}

// 业务层集成本地缓存
@Service
public class GoodsService {
    @Autowired
    private Cache<String, Object> localHotKeyCache;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private GoodsMapper goodsMapper;

    public Goods getGoodsById(Long id) {
        String key = "goods:id:" + id;
        // 1. 查本地缓存
        Goods goods = (Goods) localHotKeyCache.getIfPresent(key);
        if (goods != null) {
            return goods;
        }
        // 2. 查 Redis
        goods = (Goods) redisTemplate.opsForValue().get(key);
        if (goods != null) {
            // 更新本地缓存
            localHotKeyCache.put(key, goods);
            return goods;
        }
        // 3. 查 DB
        goods = goodsMapper.selectById(id);
        if (goods != null) {
            // 更新 Redis + 本地缓存
            redisTemplate.opsForValue().set(key, goods, 30, TimeUnit.MINUTES);
            localHotKeyCache.put(key, goods);
        }
        return goods;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 方案2:热 Key 分散(哈希分片)

将单个热 Key 拆分为多个“子热 Key”,分散到不同 Redis 节点,降低单节点压力:

  • 示例:热 Key goods:hot:1001 → 拆分为 goods:hot:1001:0 ~ goods:hot:1001:9;
  • 写入:将数据同步写入所有子 Key;
  • 读取:客户端随机访问一个子 Key(如 Random.nextInt(10))。

代码示例(Java):

// 写入:同步到所有子 Key
public void saveHotGoods(Goods goods) {
    String baseKey = "goods:hot:" + goods.getId();
    // 写入10个子 Key
    for (int i = 0; i < 10; i++) {
        String subKey = baseKey + ":" + i;
        redisTemplate.opsForValue().set(subKey, goods, 30, TimeUnit.MINUTES);
    }
}

// 读取:随机访问一个子 Key
public Goods getHotGoods(Long id) {
    String baseKey = "goods:hot:" + id;
    // 随机选一个子 Key
    int random = new Random().nextInt(10);
    String subKey = baseKey + ":" + random;
    return (Goods) redisTemplate.opsForValue().get(subKey);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 方案3:读写分离 + 主从复制

将热 Key 的读请求分流到从节点,主节点仅处理写请求:

  • 配置:Redis 主从架构(1主N从),客户端读请求随机分发到从节点;
  • 优势:分散读压力,主节点专注写操作;
  • 注意:主从延迟需控制在 100ms 内,避免读旧数据。

# 方案4:熔断限流(兜底保护)

通过 Sentinel/Hystrix 对热 Key 访问限流,避免压垮 Redis/DB:

// Sentinel 限流配置(热 Key 访问 QPS 上限 5000)
@SentinelResource(
    value = "hotKey:goods",
    blockHandler = "hotKeyBlockHandler"
)
public Goods getHotGoods(Long id) {
    // 热 Key 查询逻辑...
}

// 限流兜底方法
public Goods hotKeyBlockHandler(Long id, BlockException e) {
    log.warn("热 Key 访问限流,id:{}", id);
    return new Goods().setName("系统繁忙,请稍后再试");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 方案5:预加载 + 永不过期

对热 Key 提前加载到 Redis,且不设置过期时间(逻辑永不过期):

  • 预加载:系统启动/低峰期主动加载热 Key 到 Redis;
  • 永不过期:后台异步线程定期更新热 Key 数据,避免过期;
  • 优势:避免热 Key 过期引发的缓存击穿。

# 四、大 Key + 热 Key 叠加问题(最高风险)

如果一个 Key 既是大 Key 又是热 Key(如 20KB 的 String 热 Key,QPS 2000),会同时引发:

  • 大 Key 问题:传输缓慢、内存倾斜;
  • 热 Key 问题:单节点 CPU/网卡满载。

# 解决方案:组合策略

  1. 先拆分大 Key(拆为多个小 Key);
  2. 对拆分后的小 Key 做本地缓存 + 哈希分散;
  3. 限流兜底 + 主从读写分离;
  4. 冷热分离:仅缓存热数据,冷数据迁移到其他存储。

# 五、最佳实践总结

# 5.1 预防策略(核心)

  • Key 设计规范:
    • 单个 String Key ≤ 5KB,集合 Key 元素数 ≤ 500;
    • 避免用 Hash/List 存储全量数据,按业务维度拆分;
  • 监控告警:
    • 大 Key:设置内存阈值告警(如 String ≥ 10KB 告警);
    • 热 Key:设置 QPS 阈值告警(如单 Key QPS ≥ 1000 告警);
  • 压测验证:上线前对大 Key/热 Key 做压测,验证拆分/分流效果。

# 5.2 治理策略

  • 大 Key:优先拆分,其次压缩,最后冷热分离;
  • 热 Key:优先本地缓存,其次哈希分散,最后限流兜底;
  • 操作大 Key:用 UNLINK/SCAN 代替 DEL/HGETALL,避免阻塞;
  • 热 Key 数据:尽量简化(只缓存核心字段),降低传输成本。

# 5.3 应急策略

  • 大 Key 阻塞:临时下线大 Key 所在节点,用 UNLINK 异步删除;
  • 热 Key 过载:临时开启本地缓存兜底,降低 Redis 访问频率;
  • 集群倾斜:手动迁移大 Key/热 Key 到负载较低的节点。

# 六、核心区别与对比表

对比维度 大 Key 热 Key
核心问题 数据体积大,操作/传输成本高 访问频率高,单节点负载高
识别难点 需扫描全量 Key,耗时久 需统计访问次数,需埋点/监控
根治方案 拆分 Key(垂直/水平) 分散访问(本地缓存/哈希分片)
临时方案 压缩、异步删除 限流、读写分离
监控指标 单 Key 内存、集合元素数 单 Key QPS、节点 CPU/网卡使用率
典型错误 用 HGETALL 遍历大 Hash 所有请求直接访问同一个热 Key

通过以上方案,可有效解决 Redis 大 Key 和热 Key 问题,核心是提前预防、精准识别、分层治理,避免单点问题扩散为集群级故障。

编辑 (opens new window)
#Redis大Key与热Key
上次更新: 2026/01/21, 19:29:14
Redis缓存设计
Redis限流

← Redis缓存设计 Redis限流→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式