Redis大Key问题诊断

Redis大Key问题诊断

Redis 的五种数据结构各有特色,用对了才能发挥它的优势。很多人只用到了 String 和 Hash,却不知道 List、Set、ZSet 在特定场景下更合适。本文从应用场景出发,讲什么时候用什么类型。

一、什么是大Key

#

1.1 大Key的定义

数据类型 大Key标准
String value > 10KB
Hash 元素数 > 5000 或 总大小 > 1MB
List 元素数 > 5000 或 总大小 > 1MB
Set 元素数 > 5000 或 总大小 > 1MB
ZSet 元素数 > 5000 或 总大小 > 1MB

注意:具体标准需要根据业务场景和Redis性能调整。

#

1.2 大Key的危害

1. 阻塞Redis(单线程)
DEL bigkey → 阻塞数秒甚至数十秒
HGETALL bighash → 返回大量数据,阻塞

2. 内存不均衡(Cluster)
某个节点有大Key → 该节点内存使用远高于其他节点

3. 网络拥塞
传输大Key → 带宽占用高,其他请求延迟增加

4. 主从复制延迟
同步大Key → 主从复制长时间阻塞

二、大Key识别

#

2.1 redis-cli –bigkeys

# 扫描大Key(在线执行,会消耗一定性能)
redis-cli --bigkeys

# 输出示例:
# -------- summary -------
# Sampled 502 keys in the keyspace!
# Total key length in bytes is 3452 (avg len 6.88)
# Biggest string found 'user:10001' has 1048576 bytes
# Biggest hash found 'config:app' has 5020 fields
# Biggest list found 'logs:2024' has 85432 items

特点

  • 在线扫描,不会阻塞
  • 使用SCAN渐进遍历
  • 只能找到每种类型的最大key

#

2.2 MEMORY命令

# 查看单个key的内存使用
redis-cli MEMORY USAGE mykey
# (integer) 1048576

# 查看key的内存使用详情
redis-cli MEMORY USAGE mykey SAMPLES 50

# 查看key的编码和序列化长度
redis-cli DEBUG OBJECT mykey
# Value at:0x7f... refcount:1 encoding:raw serializedlength:1048576 lru:... lru_seconds_idle:0

#

2.3 rdbtools分析

# 安装rdbtools
pip install rdbtools python-lzf

# 分析RDB文件
rdb -c memory dump.rdb > memory.csv

# 查看内存占用最大的key
sort -t, -k4 -nr memory.csv | head -20

# CSV格式:
# database,type,key,size_in_bytes,encoding,num_elements,len_largest_element
# 0,string,user:10001,1048576,string,1,1048576
# 0,hash,config:app,524288,hashtable,5020,1024

#

2.4 自定义扫描脚本

#!/bin/bash
# find_bigkeys.sh

THRESHOLD=10240 # 10KB

echo "Scanning for keys larger than ${THRESHOLD} bytes..."

redis-cli --scan | while read key; do
size=$(redis-cli MEMORY USAGE "$key" 2>/dev/null)
if [ -n "$size" ] && [ "$size" -gt "$THRESHOLD" ]; then
type=$(redis-cli TYPE "$key")
echo "Key: $key, Type: $type, Size: $size bytes"
fi
done

#

2.5 Java识别大Key

@Component
public class BigKeyScanner {

@Autowired
private StringRedisTemplate redis;

public void scanBigKeys(int thresholdKb) {
ScanOptions options = ScanOptions.scanOptions()
.match("*")
.count(100)
.build();

Cursor<byte[]> cursor = redis.executeWithStickyConnection(
connection -> connection.scan(options)
);

while (cursor.hasNext()) {
String key = new String(cursor.next());
Long size = redis.execute((RedisCallback<Long>) connection ->
connection.memoryCommands().memoryUsage(key.getBytes())
);

if (size != null && size > thresholdKb * 1024L) {
String type = redis.type(key);
System.out.printf("BigKey found: key=%s, type=%s, size=%d bytes%n",
key, type, size);
}
}
}
}

三、大Key分析

#

3.1 分析Hash大Key

# 查看Hash的字段数量
redis-cli HLEN bighash

# 查看Hash的编码(ziplist vs hashtable)
redis-cli DEBUG OBJECT bighash
# encoding:ziplist 或 encoding:hashtable

# 查看字段大小分布
redis-cli HKEYS bighash | head -10
redis-cli HGET bighash field1 | wc -c

#

3.2 分析List大Key

# 查看List长度
redis-cli LLEN biglist

# 查看部分元素
redis-cli LRANGE biglist 0 10

# 查看元素大小
redis-cli LINDEX biglist 0 | wc -c

#

3.3 分析Set/ZSet大Key

# 查看元素数量
redis-cli SCARD bigset
redis-cli ZCARD bigzset

# 查看编码
redis-cli DEBUG OBJECT bigset
# encoding:intset 或 encoding:hashtable

#

3.4 分析String大Key

# 查看大小
redis-cli STRLEN bigstring

# 查看编码
redis-cli DEBUG OBJECT bigstring
# encoding:raw 或 encoding:embstr

四、大Key治理方案

#

4.1 String大Key拆分

// 原始:一个String存储大JSON
// key: user:1001, value: 1MB JSON

// 拆分:使用Hash存储
// key: user:1001, fields: 多个小字段

@Service
public class BigKeySplitService {

@Autowired
private StringRedisTemplate redis;

// 将大String拆分为Hash
public void splitBigStringToHash(String key) {
String bigJson = redis.opsForValue().get(key);
if (bigJson == null) return;

User user = JSON.parseObject(bigJson, User.class);

// 拆分为Hash字段
Map<String, String> hash = new HashMap<>();
hash.put("name", user.getName());
hash.put("email", user.getEmail());
hash.put("phone", user.getPhone());
// ... 其他字段

String hashKey = key.replace("user:", "user:hash:");
redis.opsForHash().putAll(hashKey, hash);

// 删除原大key
redis.delete(key);
}
}

#

4.2 Hash大Key拆分

// 原始:一个Hash有10000个字段
// key: article:tags, fields: 10000个文章ID -> 标签

// 拆分:按范围分片
@Service
public class HashShardService {

@Autowired
private StringRedisTemplate redis;

private static final int SHARD_COUNT = 10;

public void addTag(Long articleId, String tag) {
int shard = (int) (articleId % SHARD_COUNT);
String key = "article:tags:" + shard;
redis.opsForHash().put(key, String.valueOf(articleId), tag);
}

public String getTag(Long articleId) {
int shard = (int) (articleId % SHARD_COUNT);
String key = "article:tags:" + shard;
return (String) redis.opsForHash().get(key, String.valueOf(articleId));
}
}

#

4.3 List大Key拆分

// 原始:一个List有100000个元素
// key: logs:2024

// 拆分:按时间分片
@Service
public class ListShardService {

@Autowired
private StringRedisTemplate redis;

public void addLog(String log) {
String key = "logs:" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
redis.opsForList().leftPush(key, log);

// 每个List最多保留10000条
redis.opsForList().trim(key, 0, 9999);

// 设置过期时间
redis.expire(key, 30, TimeUnit.DAYS);
}
}

#

4.4 Set/ZSet大Key拆分

// 原始:一个ZSet有100000个元素
// key: leaderboard:global

// 拆分:按范围分片
@Service
public class ZSetShardService {

@Autowired
private StringRedisTemplate redis;

private static final int SHARD_COUNT = 10;

public void addScore(Long userId, double score) {
int shard = (int) (userId % SHARD_COUNT);
String key = "leaderboard:" + shard;
redis.opsForZSet().add(key, String.valueOf(userId), score);
}

// 获取全局前10需要合并多个ZSet
public Set<ZSetOperations.TypedTuple<String>> getGlobalTop10() {
// 使用ZUNIONSTORE合并
String tempKey = "leaderboard:temp:" + System.currentTimeMillis();
String[] keys = new String[SHARD_COUNT];
for (int i = 0; i < SHARD_COUNT; i++) {
keys[i] = "leaderboard:" + i;
}

redis.opsForZSet().unionAndStore(keys[0], Arrays.asList(keys), tempKey);
Set<ZSetOperations.TypedTuple<String>> result =
redis.opsForZSet().reverseRangeWithScores(tempKey, 0, 9);
redis.delete(tempKey);

return result;
}
}

五、大Key删除

#

5.1 渐进式删除

# 不好的做法:直接删除大Key(会阻塞)
DEL bigkey

# 好的做法:渐进删除

# String:直接删除(String删除快)
DEL bigstring

# Hash:使用HSCAN渐进删除
HSCAN bighash 0 COUNT 100
HDEL bighash field1 field2 ...

# List:使用LTRIM或LPOP/RPOP
# 方式1:逐步弹出
while (LLEN biglist > 0) {
LPOP biglist
// 或一次弹出多个
LRANGE biglist 0 99
LTRIM biglist 100 -1
}

# 方式2:直接改名后异步删除
RENAME biglist biglist:tobedeleted
# 然后用上述方式逐步删除

# Set:使用SSCAN渐进删除
SSCAN bigset 0 COUNT 100
SREM bigset member1 member2 ...

# ZSet:使用ZSCAN渐进删除
ZSCAN bigzset 0 COUNT 100
ZREM bigzset member1 member2 ...

#

5.2 使用UNLINK异步删除

# Redis 4.0+ 支持UNLINK
# UNLINK在后台线程中删除,不会阻塞

UNLINK bigkey

# 检查后台删除进度
redis-cli INFO stats | grep lazyfree_pending_objects

#

5.3 Java渐进删除

@Service
public class ProgressiveDeleteService {

@Autowired
private StringRedisTemplate redis;

// 渐进删除Hash
public void progressiveDeleteHash(String key, int batchSize) {
ScanOptions options = ScanOptions.scanOptions()
.match("*")
.count(batchSize)
.build();

Cursor<Map.Entry<Object, Object>> cursor =
redis.opsForHash().scan(key, options);

List<Object> fieldsToDelete = new ArrayList<>();
while (cursor.hasNext()) {
fieldsToDelete.add(cursor.next().getKey());

if (fieldsToDelete.size() >= batchSize) {
redis.opsForHash().delete(key, fieldsToDelete.toArray());
fieldsToDelete.clear();

// 短暂休眠,避免阻塞
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}

// 删除剩余的
if (!fieldsToDelete.isEmpty()) {
redis.opsForHash().delete(key, fieldsToDelete.toArray());
}

// 最后删除空key
redis.delete(key);
}

// 渐进删除List
public void progressiveDeleteList(String key, int batchSize) {
while (true) {
Long len = redis.opsForList().size(key);
if (len == null || len == 0) break;

// 使用LTRIM批量删除
redis.opsForList().trim(key, batchSize, -1);

try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}

redis.delete(key);
}
}

六、监控和告警

#

6.1 大Key监控

#!/bin/bash
# monitor_bigkeys.sh

THRESHOLD=1048576 # 1MB

# 获取大Key列表
redis-cli --bigkeys | grep "Biggest" | while read line; do
echo "$line"
# 发送告警
echo "$line" | mail -s "Redis BigKey Alert" admin@company.com
done

#

6.2 内存不均衡监控(Cluster)

# 检查各节点内存使用
for node in node1 node2 node3; do
mem=$(redis-cli -h $node INFO memory | grep used_memory: | cut -d: -f2)
echo "$node: $mem bytes"
done

# 计算最大差异

七、总结

阶段 工具/方法 说明
识别 redis-cli –bigkeys 在线扫描,找最大key
识别 MEMORY USAGE 精确查看单个key大小
识别 rdbtools 离线分析RDB文件
分析 DEBUG OBJECT 查看编码和长度
治理 拆分 大Hash/List/Set分片
治理 渐进删除 HSCAN+HDEL, LTRIM
治理 UNLINK Redis 4.0+ 异步删除

大Key治理的核心原则:

  1. 预防为主:设计时避免单key过大
  2. 定期扫描:发现大Key及时处理
  3. 渐进处理:避免阻塞Redis
  4. 合理拆分:按业务维度分片
  5. 监控告警:及时发现新产生的大Key

核心要点

  1. String:简单的键值对,适合缓存、计数器

  2. Hash:存储对象属性,适合用户信息、配置

  3. List:有序列表,适合消息队列、最新列表

  4. Set:无序去重,适合共同好友、抽奖

  5. ZSet:有序集合,适合排行榜、积分系统

总结

选择合适的数据结构是使用 Redis 的关键。在实际项目中,根据业务需求选择合适的类型,可以提升性能和开发效率。


   转载规则


《Redis大Key问题诊断》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录