数据类型 | 判定标准 |
String | 单个 Value 大于10MB |
Hash | 元素个数大于10,000,或总体积大于100MB |
List | 元素个数大于10,000,或总体积大于100MB |
Set | 元素个数大于10,000,或总体积大于100MB |
ZSet | 元素个数大于10,000,或总体积大于100MB |
HGETALL 操作请求。ZRANGE 操作请求。判定维度 | 判定方式 |
访问集中度 | 通过 DBbrain 热 Key 分析获取访问量排名,观察排名首位的 Key 与其后 Key 是否存在数量级差异 |
相对占比 | 计算单 Key 请求量占实例总请求量的比例、单 Key 流量占实例出带宽的比例,占比越高风险越大 |
资源余量 | 结合实例规格评估该 Key 所在节点的 CPU、带宽、连接数是否已接近上限 |
问题 | 具体表现 |
内存使用不均匀 | 集群架构中,某分片内存使用率远超其他分片,可能触发 maxmemory 上限,导致重要 Key 被逐出甚至 OOM |
请求阻塞超时 | 命令执行采用单线程模型,操作大 Key 的耗时较长(如 DEL、HGETALL),期间后续请求只能排队等待 |
同步中断 / 主从切换 | 对大 Key 的删除或 RENAME 操作可能长时间阻塞主节点,引发主从同步中断或故障切换 |
网络拥塞 | 1MB的大 Key 每秒访问1,000次即产生约1GB/s流量,可能占满实例带宽 |
问题 | 具体表现 |
CPU 使用率持续偏高 | 热点请求集中在单个节点,该节点 CPU 成为瓶颈,整体服务性能下降 |
访问倾斜 | 存在热 Key 的分片承担远超其他分片的访问压力,可能出现连接数耗尽甚至节点异常;由于访问集中在单一 Key,横向扩容分片也无法分散该 Key 的压力 |
缓存击穿 | 热 Key 过期或所在节点异常的瞬间,高度集中的流量会直接穿透至后端数据库,数据库无法承接时可能引发连锁故障 |
REDISCLI_AUTH 环境变量传递密码,而非使用 -a 参数:# 通过环境变量传递密码,避免明文暴露export REDISCLI_AUTH='<密码>'# 排查大 Key:按数据类型分别输出元素个数最多的 Keyredis-cli -h <实例地址> -p <端口> --bigkeys# 排查热 Key:基于 LFU 访问频次统计输出高频 Keyredis-cli -h <实例地址> -p <端口> --hotkeys
SCAN 游标遍历全部 Key,不会长时间阻塞主线程,但在数据量较大时会带来额外请求开销,建议在业务低峰期执行。集群架构下需分别连接各个分片节点执行,单次连接仅能统计当前节点的数据。--bigkeys 统计的是元素个数(String 为字节长度)而非实际内存占用,因此结果只能反映元素规模,若需按内存占用定位,应使用方法一的内存分析功能;--hotkeys 则依赖 LFU 淘汰策略,需先调整实例的淘汰策略参数才能使用。--hotkeys 前需将实例参数 maxmemory-policy 设置为 allkeys-lfu 或 volatile-lfu,否则命令将直接报错。该参数决定实例的 Key 逐出行为,修改后可能改变现有数据的淘汰顺序,请评估对业务的影响后再调整。jedis.get(key) 替换为 HotKeyDetector.get(jedis, key) 完成埋点,再通过 ScheduledExecutorService 每60秒调用一次 printTopN(10) 输出结果并执行 reset() 复位。其他客户端(Lettuce、Redisson)与命令(set、hget 等)可参照该组件扩展。import redis.clients.jedis.Jedis;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.atomic.AtomicLong;public class HotKeyDetector {// Key → 访问次数private static final ConcurrentHashMap<String, AtomicLong> KEY_COUNT = new ConcurrentHashMap<>();// Key → 累计耗时(纳秒)private static final ConcurrentHashMap<String, AtomicLong> KEY_TIME = new ConcurrentHashMap<>();/*** 包装 Jedis get 操作,自动统计每个 Key 的访问次数与耗时。*/public static String get(Jedis jedis, String key) {long start = System.nanoTime();try {return jedis.get(key);} finally {record(key, System.nanoTime() - start);}}private static void record(String key, long nanos) {KEY_COUNT.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();KEY_TIME.computeIfAbsent(key, k -> new AtomicLong()).addAndGet(nanos);}/*** 打印 TopN 热 Key,建议通过定时任务(如每 60 秒)调用。*/public static void printTopN(int n) {System.out.println("===== Top " + n + " Hot Keys =====");KEY_COUNT.entrySet().stream().sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get())).limit(n).forEach(e -> {String key = e.getKey();long count = e.getValue().get();long totalNs = KEY_TIME.getOrDefault(key, new AtomicLong()).get();double avgMs = count > 0 ? (totalNs / 1_000_000.0) / count : 0;System.out.printf("Key: %s | Count: %d | Avg: %.2f ms%n",key, count, avgMs);});}/*** 重置统计(每次打印后调用,避免数据累积)。*/public static void reset() {KEY_COUNT.clear();KEY_TIME.clear();}}
方法 | 适用场景 | 优势 | 劣势 |
DBbrain 控制台 | 日常运维、常规排查 | 可视化、支持历史数据分析、无需改造业务代码 | 需具备控制台访问权限 |
redis-cli --bigkeys | 命令行环境下快速自检大 Key | 无需额外部署,开箱可用 | 仅统计元素个数,不反映内存占用 |
redis-cli --hotkeys | 命令行环境下快速自检热 Key | 无需额外部署,开箱可用 | 依赖 LFU 淘汰策略,需调整实例参数 |
业务层埋点 | 精准定位、长期持续监控 | 统计维度可自定义,可关联业务上下文 | 需改造代码,引入少量性能开销 |
DEL 命令阻塞主线程,改用渐进式清理:# Hash:HSCAN + HDEL 渐进式删除过期成员,循环执行直至清理完成HSCAN key cursor COUNT 100HDEL key field1 field2 ...# List:LTRIM 分批裁剪,仅保留前 1,000 条LTRIM key 0 999# Redis 4.0+:UNLINK 异步删除整个大 Key,避免阻塞UNLINK large_key
// 写入:压缩后以二进制形式存入// gzipCompress / gzipDecompress 可基于 java.util.zip.GZIPOutputStream 自行实现byte[] compressed = gzipCompress(rawJsonString);jedis.set(key.getBytes(), compressed);// 读取:取出二进制数据后解压还原byte[] data = jedis.get(key.getBytes());String rawJson = gzipDecompress(data);
# 拆分前user:profile:1001 → 2MB JSON# 拆分后(按字段分组)user:profile:1001:basic → 基础信息user:profile:1001:extend → 扩展信息user:profile:1001:settings → 设置信息
# Hash 拆分:对 Field 做哈希取模,分散到 N 个子 Hash# 分片编号 = hash(field) % N,写入与读取使用同一规则定位user:cart:1001:shard_0 → 存放取模结果为 0 的 Fielduser:cart:1001:shard_1 → 存放取模结果为 1 的 Field...
MGET 等多 Key 命令可能无法跨槽执行。若需批量读取,应使用哈希标签将子 Key 固定到同一分片,例如 {user:profile:1001}:basic、{user:profile:1001}:extend,花括号内的内容相同即可保证落在同一槽位。但需注意,哈希标签会使这组 Key 集中在单一分片,若该 Key 同时是热 Key,反而会加剧访问倾斜,因此批量读取需求与分片打散需求应结合实际场景权衡。# 转存前file:report:2025-q4 → 15MB PDF BLOB# 转存后file:report:2025-q4:url → "https://bucket.cos.region.myqcloud.com/report-2025-q4.pdf"file:report:2025-q4:meta → {"size": 15728640, "md5": "abc123..."}
// Caffeine 本地缓存热点数据private static final LoadingCache<String, String> HOT_KEY_CACHE = Caffeine.newBuilder().maximumSize(1000) // 最大缓存条目数.expireAfterWrite(50, TimeUnit.MILLISECONDS) // 过期时间,控制数据不一致窗口.build(key -> {// 缓存未命中时回源 Redis,从连接池获取连接try (Jedis jedis = JEDIS_POOL.getResource()) {return jedis.get(key);}});
# 原始热 Keyhot:product:1001# 打散为多副本hot:product:1001:0 → 副本 0hot:product:1001:1 → 副本 1hot:product:1001:2 → 副本 2
// 读取时随机选择一个副本,将请求分散到不同分片int index = ThreadLocalRandom.current().nextInt(REPLICA_COUNT);String key = "hot:product:" + productId + ":" + index;try (Jedis jedis = JEDIS_POOL.getResource()) {String value = jedis.get(key);}
用户请求 → Nginx 本地缓存 → 应用本地缓存(Caffeine) → Redis → 数据库
ZRANGE 0 -1)KEYS *、SMEMBERS、HGETALL 等全量遍历命令,改用 SCAN、SSCAN、HSCAN 分批获取。Pipeline 或 MGET/MSET,避免循环单次命令。UNLINK 异步删除(Redis 4.0+)。文档反馈