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
特点 :
在线扫描,不会阻塞
使用SCAN渐进遍历
只能找到每种类型的最大key
#
2.2 MEMORY命令 redis-cli MEMORY USAGE mykey redis-cli MEMORY USAGE mykey SAMPLES 50 redis-cli DEBUG OBJECT mykey
#
pip install rdbtools python-lzf rdb -c memory dump.rdb > memory.csv sort -t, -k4 -nr memory.csv | head -20
#
2.4 自定义扫描脚本 #!/bin/bash THRESHOLD=10240 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 redis-cli HLEN bighash redis-cli DEBUG OBJECT bighash redis-cli HKEYS bighash | head -10 redis-cli HGET bighash field1 | wc -c
#
3.2 分析List大Key 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
#
3.4 分析String大Key redis-cli STRLEN bigstring redis-cli DEBUG OBJECT bigstring
四、大Key治理方案 #
4.1 String大Key拆分 @Service public class BigKeySplitService { @Autowired private StringRedisTemplate redis; public void splitBigStringToHash (String key) { String bigJson = redis.opsForValue().get(key); if (bigJson == null ) return ; User user = JSON.parseObject(bigJson, User.class); 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); redis.delete(key); } }
#
4.2 Hash大Key拆分 @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拆分 @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); redis.opsForList().trim(key, 0 , 9999 ); redis.expire(key, 30 , TimeUnit.DAYS); } }
#
4.4 Set/ZSet大Key拆分 @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); } public Set<ZSetOperations.TypedTuple<String>> getGlobalTop10() { 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 渐进式删除 DEL bigkey DEL bigstring HSCAN bighash 0 COUNT 100 HDEL bighash field1 field2 ... while (LLEN biglist > 0) { LPOP biglist // 或一次弹出多个 LRANGE biglist 0 99 LTRIM biglist 100 -1 } RENAME biglist biglist:tobedeleted SSCAN bigset 0 COUNT 100 SREM bigset member1 member2 ... ZSCAN bigzset 0 COUNT 100 ZREM bigzset member1 member2 ...
#
5.2 使用UNLINK异步删除 UNLINK bigkey redis-cli INFO stats | grep lazyfree_pending_objects
#
5.3 Java渐进删除 @Service public class ProgressiveDeleteService { @Autowired private StringRedisTemplate redis; 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()); } redis.delete(key); } public void progressiveDeleteList (String key, int batchSize) { while (true ) { Long len = redis.opsForList().size(key); if (len == null || len == 0 ) break ; 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 THRESHOLD=1048576 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治理的核心原则:
预防为主 :设计时避免单key过大
定期扫描 :发现大Key及时处理
渐进处理 :避免阻塞Redis
合理拆分 :按业务维度分片
监控告警 :及时发现新产生的大Key
核心要点
String:简单的键值对,适合缓存、计数器
Hash:存储对象属性,适合用户信息、配置
List:有序列表,适合消息队列、最新列表
Set:无序去重,适合共同好友、抽奖
ZSet:有序集合,适合排行榜、积分系统
总结 选择合适的数据结构是使用 Redis 的关键。在实际项目中,根据业务需求选择合适的类型,可以提升性能和开发效率。