热 key 与大 key 治理
Redis 的性能问题里,热 key 和大 key 占了大多数。它们的表现相似:延迟升高、CPU 打满、网络带宽跑满,但成因和治理手段完全不同。热 key 是访问集中在少数 key 上,大 key 是单个 key 的数据量过大。本文给出两者的定位方法和具体治理方案,命令均可直接执行。
一、先分清问题类型
| 维度 | 热 key | 大 key |
| --- | --- | --- |
| 本质 | QPS 集中 | 单 key 体积大 |
| 典型表现 | 单分片 CPU 高、其他分片空闲 | 网络带宽突增、DEL 阻塞、持久化慢 |
| 影响范围 | 所在节点 | 所在节点,且可能拖慢 fork 与迁移 |
| 主要手段 | 本地缓存、key 拆分、读写分离 | 拆分、压缩、改用其他结构 |
大 key 也可能变成热 key:一个百万成员的 Hash 被高频 HGETALL,既大又热。治理时要先确认主因。
二、大 key 的判定标准
没有绝对阈值,但可以参考:
- String 超过 10 KB;
- 集合类(List/Hash/Set/ZSet)元素超过 5000 个,或总大小超过 1 MB;
- 单次
HGETALL/SMEMBERS返回超过 1 MB。
Redis 官方建议集合类元素控制在 5000 以内。超过这个量级,单条命令的耗时和网络包大小都会显著上升。
三、定位大 key
方法一:redis-cli --bigkeys。 扫描全库并对每种类型找最大的 key,底层用 SCAN,不会阻塞。
redis-cli -h 10.0.0.11 -p 6379 --bigkeys -i 0.1
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Biggest string found so far 'product:detail:8821' with 51200 bytes
[12.34%] Biggest hash found so far 'cart:user:10086' with 12043 fields
[45.67%] Biggest list found so far 'feed:timeline:2001' with 88231 items
-------- summary -------
Sampled 100000 keys in the keyspace!
Total key length in bytes is 2456789 (avg len 24.57)
Biggest string found 'product:detail:8821' has 51200 bytes
Biggest hash found 'cart:user:10086' has 12043 fields
Biggest list found 'feed:timeline:2001' has 88231 items
-i 0.1 表示每 100 次 SCAN 休眠 0.1 秒,降低对线上影响。它只报告每种类型最大的一个,不覆盖全部大 key。
方法二:MEMORY USAGE。 精确查单个 key 的字节数。
redis-cli MEMORY USAGE cart:user:10086
(integer) 1048576
方法三:离线分析 RDB。 用 rdb-tools 或 redis-rdb-cli 解析快照,能拿到全量 key 的大小分布,不影响线上。
rdb -c memory /data/redis/dump.rdb --bytes 10240 -f /tmp/bigkeys.csv
方法四:SCAN + MEMORY USAGE 脚本化。
redis-cli --scan --pattern 'cart:*' | while read -r key; do
size=$(redis-cli MEMORY USAGE "$key")
if [ "$size" -gt 1048576 ]; then
echo "$key $size"
fi
done
四、大 key 的治理
拆分。 按业务维度把一个大 Hash 拆成多个小 Hash。例如购物车按用户分片:
# 原结构:cart:user:10086 存所有商品
# 拆分为:cart:user:10086:0 ~ cart:user:10086:9,按 sku_id % 10 路由
HSET cart:user:10086:3 sku:8821 2
拆分粒度要结合访问模式:如果总是整取,拆分后需要多次请求,应评估是否改用 String 存序列化对象。
压缩。 存储前用 gzip 或 zstd 压缩 JSON,适合读多写少、对 CPU 不敏感的场景。注意压缩后仍是 String,MEMORY USAGE 会变小但反序列化成本上升。
改结构。 用更紧凑的类型替代:
- 用
Hash存对象字段,而不是把整个 JSON 塞进 String,可以按字段更新; - 用
Bitmap存布尔标记,1 亿用户只要 12.5 MB; - 用
HyperLogLog做去重计数,固定 12 KB; - 用
ZSet存排行榜,只保留 Top N,定期ZREMRANGEBYRANK裁剪。
异步删除。 删除大 key 用 UNLINK 而不是 DEL:
UNLINK cart:user:10086
UNLINK 先在主线程摘除 key,再把内存释放交给后台线程,避免阻塞。Redis 4.0 以下没有 UNLINK,只能分批 HDEL/SREM。
控制过期。 给大 key 设置合理 TTL,避免长期堆积;同时注意不要让它和大量 key 同时过期。
五、热 key 的定位
方法一:redis-cli --hotkeys。 需要 maxmemory-policy 为 lfu 系列,基于 LFU 计数器统计。
redis-cli --hotkeys
# Scanning the entire keyspace to find hot keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Hot key 'product:detail:8821' found so far with counter 245
[23.45%] Hot key 'seckill:stock:9001' found so far with counter 512
-------- summary -------
Sampled 100000 keys in the keyspace!
hot key found with counter: 512 keyname: seckill:stock:9001
hot key found with counter: 245 keyname: product:detail:8821
方法二:MONITOR。 实时打印所有命令,能直接看到热点,但开销极大,只能在低峰期短时间使用。
redis-cli MONITOR | head -n 1000
方法三:客户端埋点。 在 Redis 客户端 SDK 层统计每个 key 的调用次数,按分钟聚合上报。这是生产环境最推荐的方式,无侵入 Redis,且能定位到具体调用方。
方法四:代理层统计。 如果用了 Twemproxy、Codis 或自研代理,可以在代理层做 key 访问计数。
定位时要区分"瞬时热"和"持续热"。秒杀、整点活动造成的瞬时热 key,峰值可能达到平时的百倍,靠加机器解决成本太高,更适合限流加本地缓存;而商品详情这类持续热 key,说明业务本身有强热点,应该从数据建模层面拆分。另外要结合时间维度看,--hotkeys 反映的是累计计数,活动结束后的残留计数会误导判断,需要配合分钟级监控。
六、热 key 的治理
本地缓存。 在应用层用 Caffeine 缓存热点数据,把 Redis 的 QPS 降下来。关键是本地缓存的 TTL 要短(1~5 秒),并接受短暂不一致。
Cache<String, Product> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(3))
.build();
public Product getProduct(long id) {
String key = "product:detail:" + id;
return localCache.get(key, k -> loadFromRedisOrDb(id));
}
key 拆分。 把一个热 key 复制成 N 份,读请求随机选一份,写时更新所有副本。
private static final int SHARDS = 10;
public String getStock(long skuId) {
int shard = ThreadLocalRandom.current().nextInt(SHARDS);
String key = "seckill:stock:" + skuId + ":" + shard;
String value = redis.opsForValue().get(key);
return value != null ? value : redis.opsForValue().get("seckill:stock:" + skuId);
}
写操作要同步所有分片,读多写少的场景收益明显;写频繁时反而增加复杂度。
读写分离。 热 key 的读请求路由到多个从节点,分摊单节点压力。注意从节点有复制延迟,不适合强一致读。
限流与降级。 对热点接口做单机限流,超过阈值直接返回兜底数据,保护 Redis 和数据库。
七、Cluster 下的特殊问题
在 Cluster 中,热 key 会集中在某一个分片,导致该分片 CPU 打满而其他分片空闲。Redis 7.0 的 CLUSTER COUNTKEYSINSLOT 可以确认分布:
redis-cli -c CLUSTER KEYSLOT seckill:stock:9001
(integer) 12345
redis-cli -c CLUSTER COUNTKEYSINSLOT 12345
(integer) 1
治理手段是给 key 加随机后缀拆分,让不同副本落到不同槽位。注意拆分后无法再用 {} 做跨副本的原子操作,秒杀扣减这类场景应把原子性放到 Lua 或数据库层。
常见坑 / 最佳实践
- 不要在线上直接
KEYS *找大 key。 全量遍历会阻塞主线程,必须用SCAN系列命令并加-i休眠。 DEL大 key 会阻塞主线程。 百万元素的 Hash 删除可能耗时数百毫秒,一律用UNLINK,低版本用HSCAN分批删。- 大 key 会拖慢持久化与迁移。
BGSAVE时大 key 的 COW 放大更明显,Cluster 槽迁移也会因单个大 key 卡住。 - 热 key 的本地缓存必须设短 TTL。 否则数据更新后长时间不生效,且多实例之间不一致。
- key 拆分后要保证读写一致。 读随机分片、写全部分片时,任一分片写失败都会造成数据偏差,需要补偿或对账。
小结
- 热 key 是访问集中,大 key 是体积过大,两者表现相似但治理路径不同。
- 大 key 用
--bigkeys、MEMORY USAGE、RDB 离线分析定位,治理手段是拆分、压缩、改结构和UNLINK。 - 热 key 用
--hotkeys(需 LFU)、MONITOR、客户端埋点定位,治理手段是本地缓存、key 拆分、读写分离和限流。 - Cluster 下热 key 会造成分片倾斜,拆分时要注意槽位分布与原子操作的限制。
- 所有治理都要配合监控:单分片 CPU、网络带宽、命令延迟 P99,才能验证效果。
- 治理是持续过程,容量、访问模式都会随业务变化,建议把大 key 扫描和热 key 统计做成定期任务,而不是等到告警才处理。
- 点赞
- 收藏
- 关注作者
评论(0)