Redis 大 Key 与热 Key 问题分析与解决
大 Key 和热 Key 是 Redis 生产环境中最常见的性能问题。大 Key 导致网络阻塞、内存不均,热 Key 导致单点过载。本文将深入分析这两类问题的识别和解决方案。
一、大 Key 问题
1.1 什么是大 Key
大 Key 定义:
- String 类型:value > 10KB
- 集合类型(List/Set/Zset/Hash):元素个数 > 5000
- 特殊情况:单个 key 的 value > 1MB
大 Key 的危害:
大 Key 影响:
┌─────────────────────────────────────┐
│ 网络阻塞 │
│ - 读取大 Key 占用大量网络带宽 │
│ - 阻塞其他命令执行 │
│ - 典型:读取 10MB key 需 100ms+ │
├─────────────────────────────────────┤
│ 内存不均 │
│ - Cluster 模式下导致数据倾斜 │
│ - 单节点内存占用过高 │
│ - 影响扩容和缩容 │
├─────────────────────────────────────┤
│ 删除阻塞 │
│ - 删除大 Key 阻塞主线程 │
│ - UNLINK 异步删除(推荐) │
├─────────────────────────────────────┤
│ 序列化开销 │
│ - RDB/AOF 序列化耗时 │
│ - 网络传输慢 │
└─────────────────────────────────────┘
1.2 大 Key 识别
方法 1:redis-cli 分析
# 分析整个数据库
redis-cli --bigkeys
# 输出示例
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -X option to quit.
# Biggest key found: user:profile:12345 (15.2MB) [hash]
# Summary of big keys found:
# memsize type key
# 15.2MB hash user:profile:12345
# 8.5MB string article:content:98765
# 5.3MB list queue:task:waiting
# 3.2MB zset leaderboard:global
# 2.1MB set tag:products:all
# Average key size by type:
# string: 256 bytes
# list: 1.2KB
# hash: 45KB
# set: 2.3KB
# zset: 1.8KB
方法 2:使用 MEMORY 命令
# 查看单个 key 的内存占用
redis-cli MEMORY USAGE user:profile:12345
# 输出:15938352 (bytes) = 15.2MB
# 详细内存分析
redis-cli MEMORY STATS
# 输出示例
# 1) "peak.allocated"
# 2) (integer) 1073741824
# 3) "total.allocated"
# 4) (integer) 536870912
# 5) "startup.allocated"
# 6) (integer) 1048576
# ...
# 27) "db.0"
# 28) 1) "overhead"
# 2) (integer) 1048576
# 3) "tables"
# 4) (integer) 2
# 5) "keys"
# 6) (integer) 10000
# 7) "keys-per-slot"
# 8) (integer) 61
方法 3:使用 Redis Rump
# redis-rump 是专门分析大 key 的工具
# https://github.com/sripathikrishnan/redis-rump
# 安装
git clone https://github.com/sripathikrishnan/redis-rump.git
cd redis-rump
# 运行
python redis-rump.py --host 127.0.0.1 --port 6379 --output rump_output.txt
# 分析输出
cat rump_output.txt | sort -k2 -nr | head -20
方法 4:Java 程序扫描
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import java.util.*;
public class BigKeyScanner {
private Jedis jedis;
private long sizeThreshold = 10 * 1024; // 10KB
private long countThreshold = 5000;
public BigKeyScanner(Jedis jedis) {
this.jedis = jedis;
}
/**
* 扫描大 Key
*/
public List<BigKeyInfo> scanBigKeys() {
List<BigKeyInfo> bigKeys = new ArrayList<>();
String cursor = ScanParams.SCAN_POINTER_START;
ScanParams scanParams = new ScanParams();
scanParams.count(1000);
while (true) {
ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
cursor = scanResult.getCursor();
List<String> keys = scanResult.getResult();
for (String key : keys) {
BigKeyInfo info = checkBigKey(key);
if (info != null) {
bigKeys.add(info);
System.out.println("发现大 Key: " + info);
}
}
if (ScanParams.SCAN_POINTER_START.equals(cursor)) {
break;
}
}
return bigKeys;
}
/**
* 检查单个 key
*/
private BigKeyInfo checkBigKey(String key) {
String type = jedis.type(key);
switch (type) {
case "string":
String value = jedis.get(key);
if (value != null && value.getBytes().length > sizeThreshold) {
return new BigKeyInfo(key, type, value.getBytes().length, 0);
}
break;
case "list":
long listSize = jedis.llen(key);
if (listSize > countThreshold) {
Long memory = jedis.memoryUsage(key);
return new BigKeyInfo(key, type, memory != null ? memory : 0, listSize);
}
break;
case "set":
long setSize = jedis.scard(key);
if (setSize > countThreshold) {
Long memory = jedis.memoryUsage(key);
return new BigKeyInfo(key, type, memory != null ? memory : 0, setSize);
}
break;
case "zset":
long zsetSize = jedis.zcard(key);
if (zsetSize > countThreshold) {
Long memory = jedis.memoryUsage(key);
return new BigKeyInfo(key, type, memory != null ? memory : 0, zsetSize);
}
break;
case "hash":
long hashSize = jedis.hlen(key);
if (hashSize > countThreshold) {
Long memory = jedis.memoryUsage(key);
return new BigKeyInfo(key, type, memory != null ? memory : 0, hashSize);
}
break;
}
return null;
}
/**
* 大 Key 信息
*/
static class BigKeyInfo {
String key;
String type;
long memory;
long count;
BigKeyInfo(String key, String type, long memory, long count) {
this.key = key;
this.type = type;
this.memory = memory;
this.count = count;
}
@Override
public String toString() {
return String.format("key=%s, type=%s, memory=%d bytes, count=%d",
key, type, memory, count);
}
}
}
1.3 大 Key 解决方案
方案 1:拆分大 Key
import redis.clients.jedis.Jedis;
import java.util.List;
import java.util.ArrayList;
public class BigKeySplitter {
private Jedis jedis;
public BigKeySplitter(Jedis jedis) {
this.jedis = jedis;
}
/**
* 拆分大 List
* 原始:queue:tasks (10000 个元素)
* 拆分后:queue:tasks:0, queue:tasks:1, ... (每个 1000 个元素)
*/
public void splitLargeList(String key, int batchSize) {
long totalSize = jedis.llen(key);
List<String> values = jedis.lrange(key, 0, -1);
int batchCount = 0;
List<String> batch = new ArrayList<>();
for (String value : values) {
batch.add(value);
if (batch.size() >= batchSize) {
String newKey = key + ":" + batchCount;
jedis.rpushAll(newKey, batch.toArray(new String[0]));
batch.clear();
batchCount++;
}
}
// 处理剩余
if (!batch.isEmpty()) {
String newKey = key + ":" + batchCount;
jedis.rpushAll(newKey, batch.toArray(new String[0]));
}
// 删除原 key(使用 UNLINK 异步删除)
jedis.unlink(key);
System.out.println("拆分完成:共 " + batchCount + " 个新 key");
}
/**
* 拆分大 Hash
* 原始:user:profile:12345 (50000 个 field)
* 拆分后:user:profile:12345:0, user:profile:12345:1, ... (每个 5000 个 field)
*/
public void splitLargeHash(String key, int batchSize) {
Map<String, String> allFields = jedis.hgetAll(key);
int batchCount = 0;
Map<String, String> batch = new ArrayList<>();
int currentBatch = 0;
for (Map.Entry<String, String> entry : allFields.entrySet()) {
batch.put(entry.getKey(), entry.getValue());
if (batch.size() >= batchSize) {
String newKey = key + ":" + batchCount;
jedis.hmset(newKey, batch);
batch.clear();
batchCount++;
}
}
// 处理剩余
if (!batch.isEmpty()) {
String newKey = key + ":" + batchCount;
jedis.hmset(newKey, batch);
}
// 删除原 key
jedis.unlink(key);
System.out.println("拆分完成:共 " + batchCount + " 个新 key");
}
/**
* 拆分大 String
* 原始:article:content:123 (10MB)
* 拆分后:article:content:123:0, article:content:123:1, ... (每个 1MB)
*/
public void splitLargeString(String key, int chunkSize) {
String value = jedis.get(key);
byte[] bytes = value.getBytes();
int chunkCount = (bytes.length + chunkSize - 1) / chunkSize;
for (int i = 0; i < chunkCount; i++) {
int start = i * chunkSize;
int end = Math.min(start + chunkSize, bytes.length);
byte[] chunk = Arrays.copyOfRange(bytes, start, end);
String newKey = key + ":" + i;
jedis.set(newKey, new String(chunk));
}
// 删除原 key
jedis.unlink(key);
System.out.println("拆分完成:共 " + chunkCount + " 个新 key");
}
}
方案 2:异步删除
# 错误方式:使用 DEL 删除大 Key(阻塞主线程)
redis-cli DEL user:profile:12345
# 阻塞时间:可能数百毫秒
# 正确方式:使用 UNLINK 异步删除(非阻塞)
redis-cli UNLINK user:profile:12345
# 立即返回,后台异步删除
# Java 实现
public void deleteBigKey(String key) {
// 异步删除(推荐)
jedis.unlink(key);
}
// 分批删除(适用于超大型 key)
public void deleteBigKeyInBatches(String key) {
String type = jedis.type(key);
switch (type) {
case "list":
long listSize = jedis.llen(key);
while (listSize > 0) {
// 每次删除 1000 个元素
for (int i = 0; i < 1000 && listSize > 0; i++) {
jedis.lpop(key);
listSize--;
}
Thread.sleep(10); // 避免阻塞
}
break;
case "hash":
// 类似处理
break;
case "set":
// 类似处理
break;
case "zset":
// 类似处理
break;
}
// 最后删除空 key
jedis.del(key);
}
方案 3:压缩存储
import java.io.*;
import java.util.zip.*;
import java.util.Base64;
public class CompressedStorage {
private Jedis jedis;
public CompressedStorage(Jedis jedis) {
this.jedis = jedis;
}
/**
* 压缩存储
*/
public void setCompressed(String key, String value) {
try {
// 压缩
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzos = new GZIPOutputStream(baos);
gzos.write(value.getBytes("UTF-8"));
gzos.close();
// Base64 编码
String compressed = Base64.getEncoder()
.encodeToString(baos.toByteArray());
// 存储
jedis.set(key, compressed);
System.out.println("原始大小:" + value.getBytes().length + " bytes");
System.out.println("压缩后:" + compressed.getBytes().length + " bytes");
System.out.println("压缩率:" +
(1.0 - (double) compressed.getBytes().length / value.getBytes().length) * 100 + "%");
} catch (IOException e) {
throw new RuntimeException("压缩失败", e);
}
}
/**
* 解压读取
*/
public String getCompressed(String key) {
try {
String compressed = jedis.get(key);
if (compressed == null) {
return null;
}
// Base64 解码
byte[] bytes = Base64.getDecoder().decode(compressed);
// 解压
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
GZIPInputStream gzis = new GZIPInputStream(bais);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = gzis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toString("UTF-8");
} catch (IOException e) {
throw new RuntimeException("解压失败", e);
}
}
}
二、热 Key 问题
2.1 什么是热 Key
热 Key 定义:
- 访问频率远高于其他 key
- 单秒访问 > 1000 次
- 集中在单个节点
热 Key 的危害:
热 Key 影响:
┌─────────────────────────────────────┐
│ 单点过载 │
│ - 大量请求打到单个节点 │
│ - CPU/网络资源耗尽 │
│ - 影响其他 key 访问 │
├─────────────────────────────────────┤
│ 集群倾斜 │
│ - Cluster 模式下热点问题 │
│ - 单节点成为瓶颈 │
│ - 无法通过扩容解决 │
├─────────────────────────────────────┤
│ 缓存失效风险 │
│ - 热 Key 失效导致雪崩 │
│ - 数据库压力激增 │
└─────────────────────────────────────┘
2.2 热 Key 识别
方法 1:redis-cli —hotkeys
# Redis 4.0.3+ 支持
redis-cli --hotkeys
# 输出示例
# # Hot keys found:
# 98234 hits:1234567 bytes:1024 string:product:stock:12345
# 56789 hits:987654 bytes:512 string:user:session:67890
# 34567 hits:543210 bytes:2048 zset:leaderboard:daily
方法 2:MONITOR 命令分析
# 实时监控命令
redis-cli MONITOR | grep -o '"[^"]*"' | sort | uniq -c | sort -rn | head -20
# 输出示例
# 12345 "GET" "product:stock:12345"
# 9876 "GET" "user:session:67890"
# 5432 "ZCARD" "leaderboard:daily"
方法 3:使用 redis-faina
# redis-faina 是 Facebook 开发的分析工具
# https://github.com/facebookarchive/redis-faina
# 保存慢查询日志
redis-cli config set slowlog-log-slower-than 0
redis-cli config set slowlog-max-len 10000
# 导出慢查询
redis-cli slowlog get > slow.log
# 分析
python faina.py --input=slow.log --output=analysis.txt
方法 4:Java 程序监控
import redis.clients.jedis.Jedis;
import java.util.*;
import java.util.concurrent.*;
public class HotKeyMonitor {
private Jedis jedis;
private Map<String, LongAdder> keyAccessCount = new ConcurrentHashMap<>();
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public HotKeyMonitor(Jedis jedis) {
this.jedis = jedis;
startMonitoring();
}
/**
* 开始监控
*/
private void startMonitoring() {
// 每秒统计一次
scheduler.scheduleAtFixedRate(() -> {
Map<String, Long> hotKeys = getHotKeys(1000);
if (!hotKeys.isEmpty()) {
System.out.println("热 Key 发现:");
hotKeys.forEach((key, count) ->
System.out.println(" " + key + ": " + count + " 次/秒")
);
}
keyAccessCount.clear();
}, 1, 1, TimeUnit.SECONDS);
}
/**
* 记录 key 访问
*/
public void recordAccess(String key) {
keyAccessCount.computeIfAbsent(key, k -> new LongAdder()).increment();
}
/**
* 获取热 Key
*/
public Map<String, Long> getHotKeys(long threshold) {
Map<String, Long> hotKeys = new HashMap<>();
keyAccessCount.forEach((key, adder) -> {
long count = adder.sum();
if (count >= threshold) {
hotKeys.put(key, count);
}
});
// 按访问次数排序
return hotKeys.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
}
/**
* 停止监控
*/
public void stop() {
scheduler.shutdown();
}
}
2.3 热 Key 解决方案
方案 1:本地缓存
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import redis.clients.jedis.Jedis;
import java.util.concurrent.TimeUnit;
public class HotKeyWithLocalCache {
private Jedis jedis;
private Cache<String, String> localCache;
public HotKeyWithLocalCache(Jedis jedis) {
this.jedis = jedis;
// 本地缓存:最大 10000 个 key,过期时间 5 秒
this.localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
}
/**
* 读取带本地缓存
*/
public String get(String key) {
// 先查本地缓存
String value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 查 Redis
value = jedis.get(key);
if (value != null) {
localCache.put(key, value);
}
return value;
}
/**
* 写入并清除本地缓存
*/
public void set(String key, String value) {
jedis.set(key, value);
localCache.invalidate(key);
}
}
方案 2:热 Key 备份(多副本)
import redis.clients.jedis.Jedis;
import java.util.*;
public class HotKeyReplication {
private Jedis jedis;
private Random random = new Random();
public HotKeyReplication(Jedis jedis) {
this.jedis = jedis;
}
/**
* 热 Key 多副本存储
* 原始:product:stock:12345
* 副本:product:stock:12345:replica:0, product:stock:12345:replica:1, ...
*/
public void createHotKeyReplicas(String key, int replicaCount) {
String value = jedis.get(key);
if (value == null) {
return;
}
// 创建多个副本
for (int i = 0; i < replicaCount; i++) {
String replicaKey = key + ":replica:" + i;
jedis.set(replicaKey, value);
}
System.out.println("创建 " + replicaCount + " 个副本");
}
/**
* 随机读取副本(负载均衡)
*/
public String getWithLoadBalance(String key, int replicaCount) {
// 随机选择一个副本
int replicaIndex = random.nextInt(replicaCount);
String replicaKey = key + ":replica:" + replicaIndex;
String value = jedis.get(replicaKey);
if (value != null) {
return value;
}
// 副本不存在,读取原 key
return jedis.get(key);
}
/**
* 更新所有副本
*/
public void updateWithReplicas(String key, String value, int replicaCount) {
// 更新原 key
jedis.set(key, value);
// 更新所有副本
for (int i = 0; i < replicaCount; i++) {
String replicaKey = key + ":replica:" + i;
jedis.set(replicaKey, value);
}
}
}
方案 3:读写分离
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.List;
import java.util.Random;
public class ReadWriteSeparation {
private JedisPool writePool;
private List<JedisPool> readPools;
private Random random = new Random();
public ReadWriteSeparation(JedisPool writePool, List<JedisPool> readPools) {
this.writePool = writePool;
this.readPools = readPools;
}
/**
* 写操作(主节点)
*/
public void set(String key, String value) {
try (Jedis jedis = writePool.getResource()) {
jedis.set(key, value);
}
}
/**
* 读操作(随机从节点)
*/
public String get(String key) {
if (readPools.isEmpty()) {
try (Jedis jedis = writePool.getResource()) {
return jedis.get(key);
}
}
// 随机选择一个从节点
int index = random.nextInt(readPools.size());
try (Jedis jedis = readPools.get(index).getResource()) {
return jedis.get(key);
}
}
}
方案 4:分布式缓存
import redis.clients.jedis.Jedis;
import java.util.*;
public class DistributedHotKeyCache {
private List<Jedis> jedisList;
private Random random = new Random();
public DistributedHotKeyCache(List<Jedis> jedisList) {
this.jedisList = jedisList;
}
/**
* 一致性哈希存储热 Key
*/
public void set(String key, String value) {
// 选择节点
Jedis jedis = selectNode(key);
jedis.set(key, value);
}
/**
* 读取
*/
public String get(String key) {
Jedis jedis = selectNode(key);
return jedis.get(key);
}
/**
* 选择节点(简单哈希)
*/
private Jedis selectNode(String key) {
int hash = Math.abs(key.hashCode());
int index = hash % jedisList.size();
return jedisList.get(index);
}
/**
* 广播更新(强一致性场景)
*/
public void broadcastSet(String key, String value) {
for (Jedis jedis : jedisList) {
jedis.set(key, value);
}
}
}
三、综合优化方案
3.1 架构设计
热 Key 优化架构:
┌─────────────────────────────────────────┐
│ 应用层 │
│ ┌─────────┐ ┌─────────┐ │
│ │本地缓存 │ │本地缓存 │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ └──────┬─────┘ │
│ │ 读写分离 │
├──────────────┼───────────────────────────┤
│ 缓存层 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Redis 1 │ │ Redis 2 │ │ Redis 3 │ │
│ │ (Master)│ │(Slave) │ │(Slave) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────┘
3.2 监控告警
import redis.clients.jedis.Jedis;
import java.util.*;
public class BigHotKeyAlert {
private Jedis jedis;
private long bigKeySizeThreshold = 10 * 1024; // 10KB
private long hotKeyCountThreshold = 1000; // 1000 次/秒
public BigHotKeyAlert(Jedis jedis) {
this.jedis = jedis;
}
/**
* 定期检查
*/
public void periodicCheck() {
// 检查大 Key
List<String> bigKeys = findBigKeys();
if (!bigKeys.isEmpty()) {
sendAlert("发现大 Key: " + bigKeys);
}
// 检查热 Key
List<String> hotKeys = findHotKeys();
if (!hotKeys.isEmpty()) {
sendAlert("发现热 Key: " + hotKeys);
}
}
/**
* 发送告警
*/
private void sendAlert(String message) {
System.out.println("⚠️ 告警:" + message);
// 实际项目中发送邮件/短信/钉钉
}
private List<String> findBigKeys() {
// 实现大 Key 扫描逻辑
return new ArrayList<>();
}
private List<String> findHotKeys() {
// 实现热 Key 扫描逻辑
return new ArrayList<>();
}
}
四、总结
4.1 大 Key 处理要点
-
预防为主
- 设计时避免存储大对象
- 设置合理的 key 大小限制
-
及时发现
- 定期扫描大 Key
- 监控内存使用
-
合理拆分
- List/Set 分片存储
- String 压缩存储
- Hash 按 field 拆分
-
安全删除
- 使用 UNLINK 异步删除
- 分批删除超大型 key
4.2 热 Key 处理要点
-
识别热 Key
- 使用 —hotkeys 参数
- 监控访问频率
-
多级缓存
- 本地缓存(Caffeine/Guava)
- Redis 缓存
- 数据库
-
负载均衡
- 读写分离
- 多副本备份
- 分布式缓存
-
预防雪崩
- 设置不同过期时间
- 永不过期的热 Key
- 降级熔断机制
参考资料
- Redis 官方文档 - Memory Optimization
- Redis 大 Key 问题分析
- 《Redis 深度历险:核心原理与应用实践》第 8 章