tencent cloud

腾讯云分布式缓存数据库(兼容 Redis)

热 Key 与 大 Key 排查与优化实践

下载
聚焦模式
字号
最后更新时间: 2026-09-16 16:57:24
本文介绍在云分布式缓存数据库(兼容 Redis)中,大 Key 与热 Key 的定义、识别方法、解决方案及预防措施。

业务场景

大 Key 与热 Key 是缓存类业务最常见的两类隐患,在日常运维中,业务方通常会遇到以下情况:
问题难以提前发现: 实例各项监控指标正常,但某次业务高峰期突然出现大面积访问超时。
故障根因难以定位: 实例整体 QPS 未达上限,某个分片却已 CPU 打满或连接数耗尽。
扩容无法解决问题: 增加分片数量后,压力仍集中在原有节点,访问延迟没有改善。
上述现象大多源于单个 Key 的体积或访问量偏离了实例的正常水位。本文说明这两类问题的判定方法、排查手段与优化方案,帮助您在事前预防、事中发现、事后治理各阶段形成闭环。

基本定义

大 Key 定义

大 Key 指 Redis 中某个 Key 对应的 Value 过大或集合成员过多,占用大量内存空间,本质是大 Value 问题。运维阶段可按下表阈值判定实例中是否存在大 Key。
数据类型
判定标准
String
单个 Value 大于10MB
Hash
元素个数大于10,000,或总体积大于100MB
List
元素个数大于10,000,或总体积大于100MB
Set
元素个数大于10,000,或总体积大于100MB
ZSet
元素个数大于10,000,或总体积大于100MB
需要说明的是,上表用于识别已经存在的大 Key,设计阶段应遵循更严格的推荐控制值(String 不超过10KB、集合类型元素个数不超过5,000),具体请参见 Key 与 Value 设计原则。实际判定还需结合业务场景与实例规格调整,集群架构下同时关注 Key 在各分片的分布是否均匀。

热 Key 定义

热 Key 指一段时间内某个 Key 的访问量远超过其他 Key,导致大量 QPS 或带宽集中在特定 Redis 实例或分片。
常见表现:
一个包含2,000个 Field 的 Hash 类型的 Key,每秒收到大量 HGETALL 操作请求。
一个包含10,000个成员的 ZSet 类型的 Key,每秒收到大量 ZRANGE 操作请求。
判定方法:
热 Key 没有绝对的 QPS 数值标准。同样3,000 QPS 的单 Key,在总 QPS 为5,000的实例上占比达60%,是明显的热 Key;在总 QPS 为50万的实例上占比不足1%,则属正常访问。因此判定的关键不是单 Key 的绝对访问量,而是它相对于实例整体的偏离程度。下表列出三个可用于判定的维度。
判定维度
判定方式
访问集中度
通过 DBbrain 热 Key 分析获取访问量排名,观察排名首位的 Key 与其后 Key 是否存在数量级差异
相对占比
计算单 Key 请求量占实例总请求量的比例、单 Key 流量占实例出带宽的比例,占比越高风险越大
资源余量
结合实例规格评估该 Key 所在节点的 CPU、带宽、连接数是否已接近上限
可以看出,三个维度均以"相对水位"而非固定数值作为依据。具体的告警阈值需由业务方自行确定,建议在业务平稳期先采集一段时间的访问分布数据作为业务基线,后续以显著偏离基线作为判定依据。同一套阈值在不同实例规格、不同业务模型下并不通用。

现象与影响

大 Key 的影响

问题
具体表现
内存使用不均匀
集群架构中,某分片内存使用率远超其他分片,可能触发 maxmemory 上限,导致重要 Key 被逐出甚至 OOM
请求阻塞超时
命令执行采用单线程模型,操作大 Key 的耗时较长(如 DELHGETALL),期间后续请求只能排队等待
同步中断 / 主从切换
对大 Key 的删除或 RENAME 操作可能长时间阻塞主节点,引发主从同步中断或故障切换
网络拥塞
1MB的大 Key 每秒访问1,000次即产生约1GB/s流量,可能占满实例带宽

热 Key 的影响

问题
具体表现
CPU 使用率持续偏高
热点请求集中在单个节点,该节点 CPU 成为瓶颈,整体服务性能下降
访问倾斜
存在热 Key 的分片承担远超其他分片的访问压力,可能出现连接数耗尽甚至节点异常;由于访问集中在单一 Key,横向扩容分片也无法分散该 Key 的压力
缓存击穿
热 Key 过期或所在节点异常的瞬间,高度集中的流量会直接穿透至后端数据库,数据库无法承接时可能引发连锁故障

原因分析

大 Key 产生原因

1. Key-Value 设置不当: 使用 String 类型存储大体量二进制文件或大型 JSON/XML,导致 Value 过大。
2. 无效数据未及时清理: List、Set 等类型的成员持续累积但未定期清理过期或无效数据。
3. 业务分析不准确: 上线前未充分评估数据规模,未对 Key 进行合理拆分,导致单个 Key 中成员数量过多。
4. 消费侧代码异常: 消息队列(List)消费端故障,导致数据只增不减。

热 Key 产生原因

预期之外的访问量陡增:
电商场景:突然出现的爆款商品、秒杀活动。
内容场景:访问量暴涨的热点新闻。
直播场景:直播间刷屏点赞、弹幕互动。
游戏场景:某区域大量玩家集中互动。

排查方法

方法一:DBbrain 控制台诊断(推荐)

腾讯云分布式缓存数据库接入了数据库智能管家(DBbrain)的诊断优化功能,可辅助快速发现实例中的大 Key 与热 Key:
大 Key 排查: 请参见 内存分析
热 Key 排查: 请参见 热 Key 分析

方法二:命令行工具排查

在不方便使用控制台的场景,可通过 Redis 自带的命令行工具快速排查。为避免密码出现在进程列表和 Shell 历史中,建议通过 REDISCLI_AUTH 环境变量传递密码,而非使用 -a 参数:
# 通过环境变量传递密码,避免明文暴露
export REDISCLI_AUTH='<密码>'

# 排查大 Key:按数据类型分别输出元素个数最多的 Key
redis-cli -h <实例地址> -p <端口> --bigkeys

# 排查热 Key:基于 LFU 访问频次统计输出高频 Key
redis-cli -h <实例地址> -p <端口> --hotkeys
两条命令均基于 SCAN 游标遍历全部 Key,不会长时间阻塞主线程,但在数据量较大时会带来额外请求开销,建议在业务低峰期执行。集群架构下需分别连接各个分片节点执行,单次连接仅能统计当前节点的数据。
需要注意两条命令的统计口径差异:--bigkeys 统计的是元素个数(String 为字节长度)而非实际内存占用,因此结果只能反映元素规模,若需按内存占用定位,应使用方法一的内存分析功能;--hotkeys 则依赖 LFU 淘汰策略,需先调整实例的淘汰策略参数才能使用。
说明:
使用 --hotkeys 前需将实例参数 maxmemory-policy 设置为 allkeys-lfuvolatile-lfu,否则命令将直接报错。该参数决定实例的 Key 逐出行为,修改后可能改变现有数据的淘汰顺序,请评估对业务的影响后再调整。

方法三:业务层埋点

在业务代码中包装 Redis 客户端,记录每个 Key 的访问次数与耗时,再由定时任务定期输出 TopN 热 Key。以下为 Java + Jedis 的示例组件。
使用时将 jedis.get(key) 替换为 HotKeyDetector.get(jedis, key) 完成埋点,再通过 ScheduledExecutorService 每60秒调用一次 printTopN(10) 输出结果并执行 reset() 复位。其他客户端(Lettuce、Redisson)与命令(sethget 等)可参照该组件扩展。
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 淘汰策略,需调整实例参数
业务层埋点
精准定位、长期持续监控
统计维度可自定义,可关联业务上下文
需改造代码,引入少量性能开销
概括而言,日常排查优先使用 DBbrain,它同时覆盖大 Key 与热 Key 且无需改造业务;临时应急或无控制台权限时使用命令行自检;需要长期监控热 Key 分布或关联业务维度分析时,再引入业务层埋点。

解决方法

大 Key 优化

方案一:渐进式清理无效数据

适用于 List、Set、Hash 等集合类型中积压了大量过期或无效成员的场景。避免直接使用 DEL 命令阻塞主线程,改用渐进式清理:
# Hash:HSCAN + HDEL 渐进式删除过期成员,循环执行直至清理完成
HSCAN key cursor COUNT 100
HDEL key field1 field2 ...

# List:LTRIM 分批裁剪,仅保留前 1,000 条
LTRIM key 0 999

# Redis 4.0+:UNLINK 异步删除整个大 Key,避免阻塞
UNLINK large_key

方案二:压缩大 Key 的 Value

对可压缩的文本类数据(JSON、XML、大段文本),在写入前先压缩,读取后解压:
// 写入:压缩后以二进制形式存入
// 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);
采用该方案前需评估三方面代价:压缩会消耗额外 CPU,且压缩效果取决于数据重复度,字段结构重复的 JSON、XML 文本收益明显,已压缩过的图片、视频等二进制数据则几乎没有收益;压缩后 Value 为二进制数据,需使用字节数组形式的读写接口,且无法再通过 Redis 命令直接查看内容,会增加排查难度;若压缩后 Value 仍超过阈值,说明数据规模已超出单 Key 的合理承载范围,需进一步结合拆分方案处理。

方案三:拆分大 Key

将单个大 Key 拆分为多个小 Key,按业务维度分片存储:
# 拆分前
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 的 Field
user:cart:1001:shard_1 → 存放取模结果为 1 的 Field
...
拆分粒度需结合业务访问模式设计,避免拆分后单次业务操作需多轮网络往返,反而增加延迟。
集群架构下还需考虑哈希槽分布:拆分产生的多个子 Key 会因哈希槽不同而落到不同分片,此时 MGET 等多 Key 命令可能无法跨槽执行。若需批量读取,应使用哈希标签将子 Key 固定到同一分片,例如 {user:profile:1001}:basic{user:profile:1001}:extend,花括号内的内容相同即可保证落在同一槽位。但需注意,哈希标签会使这组 Key 集中在单一分片,若该 Key 同时是热 Key,反而会加剧访问倾斜,因此批量读取需求与分片打散需求应结合实际场景权衡。

方案四:转存非适用数据

若 String 类型用于存储大文件(图片、视频 BLOB 等),建议将数据转存至对象存储(COS),Redis 中仅保留访问 URL 或元数据:
# 转存前
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..."}

热 Key 优化

方案一:读写分离

如果热 Key 压力来自读请求,开启读写分离架构,通过增加只读节点将读请求分流至只读副本,适用于读多写少的场景。需要说明的是,只读副本的数据通过主从同步获得,存在一定延迟,对数据一致性要求极高的读请求应路由至主节点。具体操作请参见 读写分离

方案二:本地缓存

在应用层使用本地缓存(如 Caffeine、Guava)缓存热点数据,大部分请求在应用进程内直接返回,无需访问 Redis:
// 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);
}
});
该方案适用于读密集型热 Key,且业务能容忍短暂的数据不一致。其中过期时间决定了数据不一致窗口的长度,需按业务对一致性的容忍度确定:时间越短一致性越好,但回源 Redis 的频率越高、削减压力的效果越弱,实际取值需在两者之间权衡,上述代码中的 50ms 仅为示例。

方案三:多副本打散

将热 Key 复制为多个内容相同的副本,分散到不同分片,读取时随机选择一个副本:
# 原始热 Key
hot:product:1001

# 打散为多副本
hot:product:1001:0 → 副本 0
hot:product:1001:1 → 副本 1
hot: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 → 数据库
绝大部分热点请求在前两层缓存即被消化,Redis 仅承载少量回源流量。各层缓存的过期时间应逐层递减,越靠近用户的层级过期时间越短,以此在削减后端压力的同时控制数据不一致的范围。

预防措施

大 Key 与热 Key 的最佳治理方式是事前预防,在业务设计阶段就规避风险:

设计阶段

1. Key 拆分设计: 上线前评估数据规模和增长速度,对可能增长为大型集合的 Key 提前建立分片策略。
2. 过期时间设置: 所有 Key 必须设置合理的过期时间,避免数据无限堆积。
3. 数据结构选型: 根据数据特征选择合适的数据类型:
需按 Field 单独过期的描述信息 → Hash + 定时清理
需有序且分布均匀的关键值 → ZSet(分页读取替代 ZRANGE 0 -1
需大量成员的关系型描述信息 → 考虑拆分为多个小 Set

运行阶段

1. 监控告警: 对内存使用率、带宽使用率、Key 总量、CPU 使用率等指标配置告警。阈值建议先采集业务平稳期的指标水位作为基线,再在基线之上预留处理时间余量确定告警线,例如内存使用率的告警线应保证从触发告警到达到上限之间,有足够时间完成扩容或清理。收到告警后按本文排查流程处理。具体操作请参见 监控功能(5 秒粒度)配置告警
2. 定期巡检: 定期通过 DBbrain 或命令行工具检查是否存在大 Key 与热 Key,在问题影响业务前提前干预。
3. 容量规划: 定期评估业务增长趋势,提前扩容或拆分。

编码规范

1. 禁止使用 KEYS *SMEMBERSHGETALL 等全量遍历命令,改用 SCANSSCANHSCAN 分批获取。
2. 批量操作优先使用 PipelineMGET/MSET,避免循环单次命令。
3. 大 Key 删除使用 UNLINK 异步删除(Redis 4.0+)。

帮助和支持

本页内容是否解决了您的问题?

填写满意度调查问卷,共创更好文档体验。

文档反馈