热 key 与大 key 治理

举报
yd_227606267 发表于 2026/11/05 00:36:25 2026/11/05
【摘要】 从发现到治理,讲清热 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 或数据库层。

常见坑 / 最佳实践

  1. 不要在线上直接 KEYS * 找大 key。 全量遍历会阻塞主线程,必须用 SCAN 系列命令并加 -i 休眠。
  2. DEL 大 key 会阻塞主线程。 百万元素的 Hash 删除可能耗时数百毫秒,一律用 UNLINK,低版本用 HSCAN 分批删。
  3. 大 key 会拖慢持久化与迁移。 BGSAVE 时大 key 的 COW 放大更明显,Cluster 槽迁移也会因单个大 key 卡住。
  4. 热 key 的本地缓存必须设短 TTL。 否则数据更新后长时间不生效,且多实例之间不一致。
  5. key 拆分后要保证读写一致。 读随机分片、写全部分片时,任一分片写失败都会造成数据偏差,需要补偿或对账。

小结

  • 热 key 是访问集中,大 key 是体积过大,两者表现相似但治理路径不同。
  • 大 key 用 --bigkeys、MEMORY USAGE、RDB 离线分析定位,治理手段是拆分、压缩、改结构和 UNLINK。
  • 热 key 用 --hotkeys(需 LFU)、MONITOR、客户端埋点定位,治理手段是本地缓存、key 拆分、读写分离和限流。
  • Cluster 下热 key 会造成分片倾斜,拆分时要注意槽位分布与原子操作的限制。
  • 所有治理都要配合监控:单分片 CPU、网络带宽、命令延迟 P99,才能验证效果。
  • 治理是持续过程,容量、访问模式都会随业务变化,建议把大 key 扫描和热 key 统计做成定期任务,而不是等到告警才处理。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。