热点 Key 不是靠猜的:京东 HotKey 探测机制拆解

摘要: Redis 热 key 真正麻烦的地方,不是缓存本身,而是热点往往来得很突然,等慢日志、连接池、CPU 报警时已经晚了。本文结合京东云开发者社区的 JdHotkey 设计实践和京东零售开源仓库,拆解 HotKey 探测的 client、worker、dashboard、etcd 四层结构,讲清 Java 后端如何在缓存被打穿前发现热点 Key,并把它转成本地缓存、限流、降级或熔断动作。

在这里插入图片描述

1. 缓存被打穿之前,谁先发现热点?

昨天我们聊过热点互动洪峰:Bitmap 兜状态、Kafka 削峰、SDS 做计数快照、Caffeine + Redis + MySQL 做三级缓存、单飞锁合并 miss、限流退避防重建风暴。

但那篇文章默认了一个前提:

系统已经知道哪个 key 是热点。

真实线上系统里,这一步更难。热点可能来自秒杀商品、活动页、直播间、爬虫用户、恶意 IP,也可能来自某个接口突然被集中访问。

你事后看 Redis 慢日志、连接池、CPU、DB 回源量,当然能知道出事了;问题是,那已经晚了。

所以今天只讲一个问题:

热点 Key 到底是怎么被系统实时发现的?

京东开源过一个热 key 探测框架 JdHotkey。京东云开发者社区的文章里说,它用于处理突发热点数据、热点用户、热点接口等场景,核心目标是在较短时间内识别热 key,并把它推送到服务端 JVM 内存里。Gitee 上的 jd-platform-opensource/hotkey 仓库也把它定位为“毫秒级探测热点数据,毫秒级推送至服务器集群内存”的框架。

它最值得学的不是“又加一层缓存”,而是把热点治理拆成了两步:

第一步:实时发现哪个 key 热了。
第二步:热 key 进 JVM,本地缓存、限流、降级或熔断。

2. 普通缓存为什么发现不了热点?

常见 cache-aside:

请求进来
  -> 查 Redis
  -> 命中:返回
  -> 未命中:查数据库
  -> 回写 Redis

Redis 官方 cache-aside 文档也把它作为读多写少场景的常见解法:应用先查 Redis,miss 后回源主库,再把结果写回缓存。

问题是:普通 cache-aside 是被动的。

它只回答“这个请求是否命中缓存”,回答不了“这个 key 是否正在变成热点”。

比如:

/sku/query 整体 5 万 QPS
其中 skuId=10086 这一个 key 占 3 万 QPS

接口整体 QPS 看起来正常,Redis 集群平均负载也可能看起来还行。但这个 key 如果落在某个 Redis 分片上,那一个分片会先被打满。

热点 Key 的麻烦在于:

  • 平均 QPS 会掩盖单 key 峰值。
  • Redis 命中率高,不代表没有单分片热点。
  • 慢日志、CPU、连接池报警通常太晚。
  • 人工配置本地缓存跟不上突发流量。

所以热点治理的第一步不是缓存,而是探测。

在这里插入图片描述

3. 京东 HotKey 的一句话理解

一句话:

客户端采集访问 key,worker 聚合判断热度,dashboard 配规则,etcd 同步规则和热 key,客户端拿到热 key 后放进 JVM 内存,再由业务决定缓存、限流、降级或熔断。

它不是 Redis 插件,也不是改造 Redis 客户端。京东云文章里明确提到,JdHotkey 不依赖 Redis,本质是一个独立的热 key 探测系统。

它关心的是“key 字符串”:

类型 key 示例
热商品 skuId_10086
热店铺 shopId_888
热接口 /sku/query
刷子用户 userId_123
恶意 IP ip_1.2.3.4
用户访问某商品 userId_123:/sku/query:skuId_10086

所以它不只服务缓存,还能服务限流、风控、降级和热点统计。

4. 四个组件:client、worker、dashboard、etcd

在这里插入图片描述

4.1 client:嵌进 Java 应用的探测入口

client 是业务应用里引入的 jar。

它负责:

  • 按规则收集待探测 key。
  • 定时批量上报给 worker。
  • 监听规则、worker 地址、热 key 变化。
  • 把探测出来的热 key 放入本地 Caffeine 缓存。
  • 对外提供“这个 key 是不是热 key”的判断能力。

业务代码应该把它当成热点信号源:

if (JdHotKeyStore.isHotKey(key)) {
    // 本地缓存、限流、降级、默认值等业务逻辑
}

注意:HotKey 框架只负责告诉你 key 是否热,不负责 value。比如 Redis 的 sku:10086 被判定为热 key 后,value 仍然需要业务自己加载,再放进本地缓存。

4.2 worker:真正做热度聚合和判断

worker 是独立部署的 Java 程序。

它负责:

  • 从 etcd 读取规则。
  • 接收 client 批量上报的 key。
  • 在时间窗口内聚合 key 出现次数。
  • 达到规则阈值后判定为 hot key。
  • 把 hot key 推送给对应 APP 的客户端。

worker 解决的是单机看不到全局热度的问题。

集群 100 台机器
某个 sku 总访问 5000 次/秒
平均到每台机器只有 50 次/秒

单机看起来不热,整体其实已经很热。worker 聚合后,才能在集群维度判断。

4.3 dashboard:规则和人工干预入口

dashboard 是控制台。

它负责:

  • 配置规则。
  • 查看热 key。
  • 手工添加或删除热 key。

规则示例:

skuId_ 开头:1 秒超过 100 次算热
userId_ 开头:2 秒超过 20 次算热
api_ 开头:1 秒超过 1000 次算热

商品、用户、接口、IP 的阈值不能混在一起。dashboard 的价值就是把业务规则从代码里拿出来。

4.4 etcd:配置中心和一致性通道

etcd 主要负责:

  • 存规则配置。
  • 存 worker 地址。
  • 存探测出来的 hot key。
  • 让 client、worker、dashboard 监听变化。

这里需要的是配置同步、监听、注册发现和一致性通知。etcd 更适合做这种协调型工作。

它保证:

一个 key 被判定为热
  -> 所有相关 client 都能收到

一个 key 被人工删除
  -> 所有相关 client 都能删除本地缓存

5. 探测链路:一个 key 怎么进 JVM 内存?

完整链路:

业务请求访问 key
  -> client 判断这个 key 是否命中规则
  -> client 在本地窗口内累加
  -> 每 500ms 批量发送待测 key 到 worker
  -> worker 按规则聚合计数
  -> 达到阈值,判定为 hot key
  -> worker 推送 hot key 给 client
  -> client 放入本地 Caffeine
  -> 业务后续访问走本地缓存、限流或降级

在这里插入图片描述

几个关键点:

5.1 只有命中规则的 key 才上报

不是所有字符串都应该上报。

HotKey 探测不是日志系统,不负责记录所有访问。它只负责尽快识别可能有风险的 key。

5.2 client 先本地累加,再批量发送

京东文章里提到,client 默认每 500ms 批量发送一次待测 key;这段时间内,client 先在本地收集并累加。

这比每次访问都发给 worker 更合理:

  • 网络请求少。
  • 先做局部聚合。
  • worker 收到的是增量统计,不是每一次访问。

5.3 固定 key 发到固定 worker

同一个 key 不能分散到多个 worker,否则每个 worker 只看到局部计数。

所以可以用:

hash(key) % workerCount

固定 key 发送到固定 worker,聚合才准确。

5.4 热了之后不再反复上报

一个 key 已经被识别为热 key,后续重点就不是继续证明它热,而是让客户端在本地处理它。继续上报只会浪费探测资源。

6. HotKey 探测出来后,Java 侧怎么处理?

JdHotkey 只回答:

这个 key 是否热?

后续动作由业务自己决定。

6.1 本地 Caffeine 缓存

Redis 热 key 最常见的处理方式,是把 value 放进 JVM 本地缓存。

String key = "sku:" + skuId;

if (JdHotKeyStore.isHotKey(key)) {
    Product product = localCache.getIfPresent(key);
    if (product != null) {
        return product;
    }

    product = redis.get(key);
    JdHotKeyStore.smartSet(key, product);
    return product;
}

return redis.get(key);

Caffeine 的配置文档支持 maximumSizeexpireAfterWriterefreshAfterWrite 等参数。所以本地缓存必须有容量、过期和刷新策略,不能当无界 Map。

6.2 限流或拒绝访问

如果 key 代表刷子用户或恶意 IP:

userId_123
ip_1.2.3.4

热了之后不一定缓存,而是限流、拒绝、返回默认响应。

6.3 接口降级

如果 key 代表某个接口:

api_/sku/query

热了之后可以短时间返回简化结果、默认值,或者关闭部分非核心计算。

6.4 单飞锁合并首次回源

HotKey 负责发现,single-flight 负责合并第一次加载。

CompletableFuture<Value> future = inflight.computeIfAbsent(key, k ->
    CompletableFuture.supplyAsync(() -> loadValue(k), executor)
        .whenComplete((v, ex) -> inflight.remove(k))
);

return future.join();

否则热 key 第一次进本地缓存前,多个线程仍可能一起回源。

7. 它和普通二级缓存有什么区别?

很多人会问:这不就是 JVM 本地缓存吗?

不是。

二级缓存只是结果,HotKey 探测才是前置机制。

对比点 普通二级缓存 HotKey 探测
是否知道 key 热不热 不知道,通常无脑缓存 实时统计后判断
是否集群一致 常常不一致 通过 etcd 推送保持一致
是否能处理刷子用户 不适合 可以把用户 ID 当 key
是否能处理热接口 不适合 可以把接口路径当 key
是否需要业务规则 较少 规则是核心
主要价值 减少部分 Redis/DB 访问 在热点刚出现时快速保护系统

8. 面试怎么讲京东 HotKey?

如果面试官问:

Redis 热 key 怎么处理?

不要只说“加本地缓存”。

可以这样讲:

我会把热 key 问题拆成两步。

第一步是发现:不能靠人工猜,也不能只看接口平均 QPS。
可以参考京东 HotKey 的思路,在业务 client 侧采集待测 key,按规则批量上报 worker,由 worker 在时间窗口内聚合判断,达到阈值后通过 etcd 推送给所有客户端。

第二步是处理:客户端拿到 hot key 后,可以把 value 放进 Caffeine 本地缓存,也可以对用户、IP、接口做限流、降级或熔断。对于首次加载,还要配合单飞锁,避免多个线程同时回源。

这段回答比“Redis 热 key 放本地缓存”更完整,因为它讲清楚了发现、聚合、规则、通知和处理动作。

9. Java 后端落地清单

如果你不是直接引入 JdHotkey,而是自己做简化版,也要保留这些关键点:

  1. 先定义 key 维度:商品、用户、接口、IP、组合维度。
  2. 规则必须可配置:不同前缀、不同业务使用不同阈值。
  3. client 不全量上报:先按规则过滤,再本地聚合,再批量发送。
  4. 同一个 key 固定打到同一个 worker:否则聚合不准。
  5. worker 做窗口统计:目标是快速判断,不是精确日志。
  6. 热 key 要推送到全部相关 client:否则集群行为不一致。
  7. 本地缓存要有容量和过期:Caffeine 不能当无界 Map。
  8. 热 key 只是一种信号:后续动作可以是缓存、限流、降级、熔断、默认值。
  9. 首次加载要防并发回源:HotKey + 单飞锁一起用。
  10. 监控探测系统本身:worker QPS、队列长度、推送延迟、hot key 数量都要看。

在这里插入图片描述

总结

热点 Key 不是靠猜的。

真正可上线的高并发缓存系统,不能只在 Redis 被打穿之后再补救,而要在热点刚出现时就发现它。

京东 HotKey 的机制可以概括成:

client 采集 key
worker 聚合判断
dashboard 配规则
etcd 同步推送
client 本地保护

这套机制背后的工程判断很清楚:

缓存治理不是从“缓存什么”开始,而是从“如何发现危险的 key”开始。

参考资料

  • 京东云开发者社区:京东毫秒级热 key 探测框架设计与实践,已完美支撑 618 大促:https://developer.jdcloud.com/article/2855
  • Gitee:京东零售 hotkey 开源仓库:https://gitee.com/jd-platform-opensource/hotkey
  • Redis 官方文档:Cache-aside:https://redis.io/docs/latest/develop/use-cases/cache-aside/
  • Caffeine Wiki:Specification:https://github.com/ben-manes/caffeine/wiki/Specification
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐