Redis 大 Key 与 热点 Key 问题(解决方案大全 + 可落地实践)
大 Key 和热点 Key 是 Redis 线上事故的两大元凶:
- 大 Key:单个 key 的 value 太大 / 元素太多,导致阻塞、网络抖动、复制变慢、RDB/AOF 变慢
- 热点 Key :某个 key QPS 超高,导致单点打满(CPU/带宽/网卡/单线程)
本文给你一套发现 → 定位 → 解决 → 预防的完整打法(含代码模板与运维命令)。
目录
- [1. 大 Key / 热点 Key 的定义与危害](#1. 大 Key / 热点 Key 的定义与危害)
- [2. 如何发现:线上快速定位方法](#2. 如何发现:线上快速定位方法)
- [3. 大 Key 解决方案(拆分、压缩、异步删除)](#3. 大 Key 解决方案(拆分、压缩、异步删除))
- [4. 热点 Key 解决方案(多级缓存、分片热点、逻辑过期)](#4. 热点 Key 解决方案(多级缓存、分片热点、逻辑过期))
- [5. 读写模型与一致性注意事项](#5. 读写模型与一致性注意事项)
- [6. 预防清单(上线前就该做的事)](#6. 预防清单(上线前就该做的事))
- [7. Java/Spring Boot 代码模板(可直接抄)](#7. Java/Spring Boot 代码模板(可直接抄))
- [8. 面试标准回答(1~2 分钟)](#8. 面试标准回答(1~2 分钟))
1. 大 Key / 热点 Key 的定义与危害
1.1 什么是大 Key
常见判定(经验值,不是硬标准):
- String:value > 1MB(或更小就明显影响延迟)
- Hash/Set/ZSet/List:元素数量 > 1w(视业务与元素大小)
- 单次 HGETALL/SMEMBERS/ZRANGE 结果太大(网络与序列化成本爆炸)
1.2 大 Key 的危害
- 阻塞 Redis 单线程:某些操作时间复杂度与元素数量有关(O(N))
- 网络抖动:一次返回几十 MB,网卡被打满,其他请求排队
- 复制与持久化变慢:RDB/AOF rewrite、主从同步更慢
- 删除卡死:DEL 大 key 是 O(N),可能阻塞几百毫秒甚至秒级
1.3 什么是热点 Key
热点 key = 单个 key 的访问 QPS 极高(例如全站配置、爆款商品、秒杀库存)。
1.4 热点 Key 的危害
- Redis 是单线程处理命令(主线程),热点会导致:
- CPU 飙高
- 请求排队、延迟上升
- 甚至触发超时/雪崩
- Cluster 下热点 key 也只在一个槽 → 仍然是单点瓶颈
2. 如何发现:线上快速定位方法
2.1 发现大 Key
方法 A:redis-cli bigkeys(简单但有开销)
bash
redis-cli --bigkeys
注意:
- 会扫描全库,线上大库慎用
- 适合离峰、或在从库跑
方法 B:MEMORY USAGE(精准但需要逐个 key)
bash
MEMORY USAGE mykey
方法 C:SCAN + 采样统计(推荐做自动化)
- 用 SCAN 抽样 key
- 对可疑 key 做
TYPE+STRLEN/HLEN/LLEN/SCARD/ZCARD+MEMORY USAGE
2.2 发现热点 Key
方法 A:MONITOR(强烈不建议线上长开)
bash
MONITOR
方法 B:命令统计(推荐)
bash
INFO commandstats
看:
- 某些命令调用次数是否异常(GET/HGETALL 等)
方法 C:客户端埋点 + TopN key(最靠谱)
- 在业务侧记录 key 的 QPS TopN(采样即可)
- 或使用网关/代理层统计
3. 大 Key 解决方案(拆分、压缩、异步删除)
3.1 方案一:拆分 Key(核心解法)
3.1.1 String 大对象拆分
把一个大 JSON 拆成多个块:
user:profile:1:part:0user:profile:1:part:1- ...
优点:单次读写变小
缺点:需要拼装、版本控制
3.1.2 Hash 拆分(字段分桶)
原:user:tags:{uid} 一个 hash 放几万字段
改:按字段 hash 分桶:
user:tags:{uid}:0user:tags:{uid}:1- ...(比如 64 桶)
3.1.3 List/Set/ZSet 拆分(按分页/时间/范围)
例如消息列表:
msg:{uid}:202512按月拆- 或
msg:{uid}:page:{p}按页拆
3.2 方案二:避免全量读取(别用 HGETALL/SMEMBERS)
- Hash:用
HMGET/HSCAN - Set:用
SSCAN - ZSet:用
ZRANGE分页(带 LIMIT) - List:用
LRANGE分页
3.3 方案三:压缩/编码优化(减少传输与内存)
- JSON → MessagePack / Protobuf
- 使用更短字段名
- 对大文本压缩(gzip/snappy)
但注意:压缩会增加 CPU,得压测权衡
3.4 方案四:删除大 Key 用异步(避免阻塞)
3.4.1 UNLINK(推荐)
bash
UNLINK bigkey
3.4.2 过期删除
给 key 设置短 TTL,让它自然过期(适合可接受延迟删除的业务)。
4. 热点 Key 解决方案(多级缓存、分片热点、逻辑过期)
4.1 多级缓存(本地缓存 Caffeine + Redis)
热点 key 读多写少:
- 本地缓存命中直接返回
- Redis 作为二级
优点:立刻减少 Redis QPS
缺点:一致性更复杂(但对配置类很合适)
4.2 热点 Key 复制 N 份(读扩散)
把一个热点 key 复制成 N 份:
hot:config:0hot:config:1- ...
hot:config:N-1
读时随机读一份;写时更新所有副本(或异步批量更新)。
4.3 逻辑过期 + 异步刷新(热点最稳)
- key 物理 TTL 设很长
- value 带
expireAt - 逻辑过期后返回旧值,同时只触发一个线程刷新
优点:不会击穿 DB
缺点:短时间读旧值
4.4 限流/降级(保护系统)
热点持续异常时:
- 限流
- 返回默认值/旧值
- 或降级到静态配置
5. 读写模型与一致性注意事项
- 热点 key 复制:写时带版本号,读时校验版本(避免读旧副本)
- 多级缓存:发布事件清理本地缓存(或短 TTL)
- 大 key 拆分:需要版本号/分片校验,避免部分更新拼装错乱
6. 预防清单(上线前就该做的事)
- 限制单 value 最大大小、单集合最大元素数
- 严禁/审查:HGETALL、SMEMBERS、KEYS
- 缓存 TTL 加随机抖动
- 大 key 删除用 UNLINK
- 业务侧统计 TopN 热点 key(采样)
- 热点接口预留本地缓存/降级开关
7. Java/Spring Boot 代码模板
7.1 热点 key 本地缓存(Caffeine)+ Redis 二级
java
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.time.Duration;
public class HotKeyCache {
private final Cache<String, String> local = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(30))
.build();
private final StringRedisTemplate redis;
public HotKeyCache(StringRedisTemplate redis) {
this.redis = redis;
}
public String get(String key) {
String v = local.getIfPresent(key);
if (v != null) return v;
v = redis.opsForValue().get(key);
if (v != null) {
local.put(key, v);
}
return v;
}
public void invalidate(String key) {
local.invalidate(key);
}
}
7.2 热点 key 复制 N 份(读扩散)
java
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.ThreadLocalRandom;
public class HotKeyReplica {
private final StringRedisTemplate redis;
private final int replicas;
public HotKeyReplica(StringRedisTemplate redis, int replicas) {
this.redis = redis;
this.replicas = replicas;
}
private String replicaKey(String baseKey, int idx) {
return baseKey + ":" + idx;
}
public String get(String baseKey) {
int idx = ThreadLocalRandom.current().nextInt(replicas);
return redis.opsForValue().get(replicaKey(baseKey, idx));
}
public void setAll(String baseKey, String value) {
for (int i = 0; i < replicas; i++) {
redis.opsForValue().set(replicaKey(baseKey, i), value);
}
}
}
7.3 大 Key 删除用 UNLINK(避免阻塞)
java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
public class SafeDelete {
private static final DefaultRedisScript<Long> UNLINK_SCRIPT = new DefaultRedisScript<>(
"return redis.call('unlink', KEYS[1])", Long.class);
private final StringRedisTemplate redis;
public SafeDelete(StringRedisTemplate redis) {
this.redis = redis;
}
public void unlink(String key) {
redis.execute(UNLINK_SCRIPT, Collections.singletonList(key));
}
}
8. 面试标准回答(1~2 分钟)
大 key 指单个 key 过大或集合元素过多,会导致 Redis 单线程阻塞、网络抖动、复制与持久化变慢,删除也会卡住。解决通常是拆分 key、避免全量读取、压缩编码,以及删除时用 UNLINK 异步释放。
热点 key 指某个 key 访问 QPS 极高,会把单节点 CPU/带宽打满。解决通常是多级缓存、本地缓存、热点 key 复制 N 份做读扩散、逻辑过期异步刷新,以及必要时限流降级。
线上需要监控与定位:bigkeys、MEMORY USAGE、commandstats、业务侧 TopN key 采样等。
一句话总结
- 大 key:拆分 + 分页读 + UNLINK 删除
- 热点 key:本地缓存 + 读扩散 + 逻辑过期
- 预防 > 救火:上线前限制 key 形态和大小