1. 项目背景与目标
在电商大促、秒杀、热点商品详情页这类场景里,流量往往在极短时间内集中打到少数几个数据上------这就是「热点」。如果热点数据没有被提前识别和隔离,请求会瞬间压垮缓存与数据库,引发缓存击穿、数据库雪崩。
JD Hotkey 是京东零售开源的通用热点探测与秒杀系统(仓库地址:https://gitee.com/jd-platform-opensource/hotkey),它的核心目标不是「抢购」,而是在毫秒级识别出热点 key,并把热点流量从主链路中隔离出去,从而保护后端存储。
本文从工程视角完整走一遍这个项目的实战过程,包括:
- 热点探测的整体架构与设计思想
- 核心模块划分与源码结构
- 热点 key 的探测算法与滑动窗口统计
- 热点数据的本地缓存与规则推送
- 秒杀场景下的限流与降级
- 本地部署、压测与接入实践
说明:本文基于 Gitee 上京东开源的 JD Hotkey 项目整理,用于技术学习与架构研究。项目采用 Apache-2.0 协议,实际接入请以仓库最新文档为准。
2. 项目定位与核心思想
2.1 它解决什么问题
传统做法是「缓存 + 限流 + 熔断」,但这些手段大多是被动防御:等流量打上来才发现问题。JD Hotkey 的思路是主动探测:
- 在应用侧(SDK)统计每个 key 的访问频次;
- 通过滑动窗口在本地判断某个 key 是否「热」;
- 一旦判定为热点,立即上报给中心服务;
- 中心服务把热点规则推送给所有应用实例;
- 各实例对热点 key 走本地缓存,不再穿透到 Redis / DB。
2.2 关键设计取舍
| 设计点 | 做法 | 好处 |
|---|---|---|
| 探测位置 | 放在客户端 SDK | 无额外网络开销,统计最真实 |
| 上报方式 | 异步、批量、去中心化 | 不阻塞业务主链路 |
| 热点判定 | 滑动窗口 + 阈值 | 兼顾灵敏度和误判率 |
| 规则下发 | 中心服务推送 | 全局一致,秒级生效 |
| 热点数据 | 本地缓存(Caffeine) | 彻底挡住穿透流量 |
3. 整体架构
JD Hotkey 采用「客户端 SDK + 中心服务」的经典架构:
#mermaid-svg-rYs1QxpUSpEywhMt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rYs1QxpUSpEywhMt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rYs1QxpUSpEywhMt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rYs1QxpUSpEywhMt .error-icon{fill:#552222;}#mermaid-svg-rYs1QxpUSpEywhMt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rYs1QxpUSpEywhMt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rYs1QxpUSpEywhMt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rYs1QxpUSpEywhMt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rYs1QxpUSpEywhMt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rYs1QxpUSpEywhMt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rYs1QxpUSpEywhMt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rYs1QxpUSpEywhMt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rYs1QxpUSpEywhMt .marker.cross{stroke:#333333;}#mermaid-svg-rYs1QxpUSpEywhMt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rYs1QxpUSpEywhMt p{margin:0;}#mermaid-svg-rYs1QxpUSpEywhMt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rYs1QxpUSpEywhMt .cluster-label text{fill:#333;}#mermaid-svg-rYs1QxpUSpEywhMt .cluster-label span{color:#333;}#mermaid-svg-rYs1QxpUSpEywhMt .cluster-label span p{background-color:transparent;}#mermaid-svg-rYs1QxpUSpEywhMt .label text,#mermaid-svg-rYs1QxpUSpEywhMt span{fill:#333;color:#333;}#mermaid-svg-rYs1QxpUSpEywhMt .node rect,#mermaid-svg-rYs1QxpUSpEywhMt .node circle,#mermaid-svg-rYs1QxpUSpEywhMt .node ellipse,#mermaid-svg-rYs1QxpUSpEywhMt .node polygon,#mermaid-svg-rYs1QxpUSpEywhMt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rYs1QxpUSpEywhMt .rough-node .label text,#mermaid-svg-rYs1QxpUSpEywhMt .node .label text,#mermaid-svg-rYs1QxpUSpEywhMt .image-shape .label,#mermaid-svg-rYs1QxpUSpEywhMt .icon-shape .label{text-anchor:middle;}#mermaid-svg-rYs1QxpUSpEywhMt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rYs1QxpUSpEywhMt .rough-node .label,#mermaid-svg-rYs1QxpUSpEywhMt .node .label,#mermaid-svg-rYs1QxpUSpEywhMt .image-shape .label,#mermaid-svg-rYs1QxpUSpEywhMt .icon-shape .label{text-align:center;}#mermaid-svg-rYs1QxpUSpEywhMt .node.clickable{cursor:pointer;}#mermaid-svg-rYs1QxpUSpEywhMt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rYs1QxpUSpEywhMt .arrowheadPath{fill:#333333;}#mermaid-svg-rYs1QxpUSpEywhMt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rYs1QxpUSpEywhMt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rYs1QxpUSpEywhMt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rYs1QxpUSpEywhMt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rYs1QxpUSpEywhMt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rYs1QxpUSpEywhMt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rYs1QxpUSpEywhMt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rYs1QxpUSpEywhMt .cluster text{fill:#333;}#mermaid-svg-rYs1QxpUSpEywhMt .cluster span{color:#333;}#mermaid-svg-rYs1QxpUSpEywhMt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rYs1QxpUSpEywhMt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rYs1QxpUSpEywhMt rect.text{fill:none;stroke-width:0;}#mermaid-svg-rYs1QxpUSpEywhMt .icon-shape,#mermaid-svg-rYs1QxpUSpEywhMt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rYs1QxpUSpEywhMt .icon-shape p,#mermaid-svg-rYs1QxpUSpEywhMt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rYs1QxpUSpEywhMt .icon-shape .label rect,#mermaid-svg-rYs1QxpUSpEywhMt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rYs1QxpUSpEywhMt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rYs1QxpUSpEywhMt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rYs1QxpUSpEywhMt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
业务应用 + Hotkey SDK
本地滑动窗口统计
是否超过热点阈值
正常访问 Redis / DB
异步上报热点 Key
Hotkey 中心服务
规则计算与聚合
推送热点规则到所有实例
SDK 本地缓存热点数据
后续请求直接命中本地缓存
几个关键角色:
- SDK(客户端):嵌入业务应用,负责统计、判定、上报、本地缓存。
- 中心服务(Worker/Server):接收上报、聚合计算、下发规则。
- 配置中心:承载规则与开关,支持动态调整。
- 本地缓存:热点 key 的实际数据落点,通常用 Caffeine。
4. 源码结构
从 Gitee 仓库拉取代码后,主要模块大致如下:
text
hotkey/
├── hotkey-common/ # 公共常量、工具、数据结构
├── hotkey-client/ # 客户端 SDK:统计、上报、缓存
├── hotkey-server/ # 中心服务:接收上报、规则计算
├── hotkey-dashboard/ # 可视化控制台(部分版本提供)
├── hotkey-example/ # 接入示例
└── pom.xml
克隆与构建:
bash
git clone https://gitee.com/jd-platform-opensource/hotkey.git
cd hotkey
mvn clean package -DskipTests
提示:项目为 Java + Maven 工程,建议使用 JDK 8 及以上版本,具体版本要求以仓库
pom.xml为准。
5. 热点探测原理
5.1 滑动窗口统计
SDK 在本地为每个 key 维护一个计数窗口。常见实现是把时间切成若干小格(如 1 秒一格),统计最近 N 格的访问总量:
java
// 简化示意:滑动窗口计数
public class SlidingWindow {
private final long[] buckets;
private final long windowMillis;
private final int bucketCount;
private int currentIndex;
public SlidingWindow(int bucketCount, long windowMillis) {
this.bucketCount = bucketCount;
this.windowMillis = windowMillis;
this.buckets = new long[bucketCount];
}
public synchronized void increment() {
buckets[currentIndex]++;
}
public synchronized long sum() {
long total = 0;
for (long b : buckets) {
total += b;
}
return total;
}
public synchronized void slide() {
currentIndex = (currentIndex + 1) % bucketCount;
buckets[currentIndex] = 0;
}
}
实际项目中窗口的推进由定时任务驱动,避免每次访问都做时间计算。
5.2 热点判定
当某个 key 在窗口内的访问量超过配置阈值,SDK 就把它标记为候选热点,并异步上报。判定逻辑通常还会结合:
- 访问量阈值:单位时间内的 QPS;
- 去重上报:同一个 key 在一个周期内只上报一次;
- 冷却时间:避免热点抖动导致频繁上报。
6. 客户端接入实战
6.1 引入依赖
xml
<dependency>
<groupId>com.jd.platform</groupId>
<artifactId>hotkey-client</artifactId>
<version>${hotkey.version}</version>
</dependency>
6.2 初始化 SDK
应用启动时完成一次初始化,指定中心服务地址与应用名:
java
import com.jd.platform.hotkey.client.ClientStarter;
public class HotkeyBootstrap {
public static void main(String[] args) {
ClientStarter.Builder builder = new ClientStarter.Builder();
builder.setAppName("demo-app")
.setCpuNum(4)
.setEtcdServer("http://127.0.0.1:2379")
.setDashBoardServer("http://127.0.0.1:8081");
ClientStarter.start(builder);
}
}
不同版本的配置项名称可能略有差异,请以仓库 README 与示例工程为准。
6.3 在业务中埋点
在读取缓存或数据库之前,先让 SDK 统计一次访问:
java
import com.jd.platform.hotkey.client.callback.HotKey;
public String getProduct(String skuId) {
// 1. 统计访问,触发热点探测
HotKey.getInstance().addKey(skuId);
// 2. 优先读本地热点缓存
String cached = HotKey.getInstance().get(skuId);
if (cached != null) {
return cached;
}
// 3. 未命中则回源
String value = loadFromRedis(skuId);
HotKey.getInstance().set(skuId, value);
return value;
}
这样,一旦某个 skuId 被判定为热点,后续请求会直接命中本地缓存,不再打到 Redis。
7. 中心服务与规则下发
7.1 上报与聚合
SDK 上报的热点 key 会进入中心服务,中心服务按应用维度聚合,计算最终的「热点规则」。规则通常包含:
- 热点 key 列表;
- 生效时间与过期时间;
- 缓存策略(是否本地缓存、缓存时长)。
7.2 规则推送
规则通过配置中心(如 etcd)或长连接推送到所有实例。SDK 收到规则后:
- 把对应 key 加入本地热点集合;
- 后续访问该 key 时走本地缓存路径;
- 规则过期后自动移除,恢复常规链路。
"配置中心" "Hotkey 中心服务" "业务应用 SDK" "配置中心" "Hotkey 中心服务" "业务应用 SDK" #mermaid-svg-66bR6Fx9HIyqVPef{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-66bR6Fx9HIyqVPef .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-66bR6Fx9HIyqVPef .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-66bR6Fx9HIyqVPef .error-icon{fill:#552222;}#mermaid-svg-66bR6Fx9HIyqVPef .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-66bR6Fx9HIyqVPef .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-66bR6Fx9HIyqVPef .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-66bR6Fx9HIyqVPef .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-66bR6Fx9HIyqVPef .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-66bR6Fx9HIyqVPef .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-66bR6Fx9HIyqVPef .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-66bR6Fx9HIyqVPef .marker{fill:#333333;stroke:#333333;}#mermaid-svg-66bR6Fx9HIyqVPef .marker.cross{stroke:#333333;}#mermaid-svg-66bR6Fx9HIyqVPef svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-66bR6Fx9HIyqVPef p{margin:0;}#mermaid-svg-66bR6Fx9HIyqVPef .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-66bR6Fx9HIyqVPef text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-66bR6Fx9HIyqVPef .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-66bR6Fx9HIyqVPef .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-66bR6Fx9HIyqVPef .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-66bR6Fx9HIyqVPef .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-66bR6Fx9HIyqVPef #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-66bR6Fx9HIyqVPef .sequenceNumber{fill:white;}#mermaid-svg-66bR6Fx9HIyqVPef #sequencenumber{fill:#333;}#mermaid-svg-66bR6Fx9HIyqVPef #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-66bR6Fx9HIyqVPef .messageText{fill:#333;stroke:none;}#mermaid-svg-66bR6Fx9HIyqVPef .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-66bR6Fx9HIyqVPef .labelText,#mermaid-svg-66bR6Fx9HIyqVPef .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-66bR6Fx9HIyqVPef .loopText,#mermaid-svg-66bR6Fx9HIyqVPef .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-66bR6Fx9HIyqVPef .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-66bR6Fx9HIyqVPef .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-66bR6Fx9HIyqVPef .noteText,#mermaid-svg-66bR6Fx9HIyqVPef .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-66bR6Fx9HIyqVPef .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-66bR6Fx9HIyqVPef .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-66bR6Fx9HIyqVPef .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-66bR6Fx9HIyqVPef .actorPopupMenu{position:absolute;}#mermaid-svg-66bR6Fx9HIyqVPef .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-66bR6Fx9HIyqVPef .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-66bR6Fx9HIyqVPef .actor-man circle,#mermaid-svg-66bR6Fx9HIyqVPef line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-66bR6Fx9HIyqVPef :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} "上报热点 Key" "聚合计算规则" "写入热点规则" "推送规则变更" "更新本地热点集合"
8. 秒杀场景实践
JD Hotkey 在秒杀场景中的典型用法是热点商品隔离 + 限流:
8.1 热点商品本地缓存
秒杀开始前,通过预热把热门 SKU 提前标记为热点,让流量在活动开始瞬间就命中本地缓存:
java
// 活动前预热热点 key
public void preheat(List<String> hotSkuIds) {
for (String skuId : hotSkuIds) {
HotKey.getInstance().addKey(skuId);
HotKey.getInstance().set(skuId, loadFromRedis(skuId));
}
}
8.2 配合限流
热点隔离解决的是「读」的压力,秒杀的「写」还需要限流兜底。常见做法是:
- 网关层做令牌桶限流;
- 库存扣减走 Redis 原子操作;
- 超卖校验放在数据库唯一约束或 Lua 脚本中。
java
// 简化示意:基于 Redis 的库存扣减
public boolean deductStock(String skuId) {
String script =
"if redis.call('get', KEYS[1]) and tonumber(redis.call('get', KEYS[1])) > 0 then " +
" return redis.call('decr', KEYS[1]) " +
"else return -1 end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("stock:" + skuId)
);
return result != null && result >= 0;
}
9. 部署与压测
9.1 本地部署
典型部署顺序:
- 启动 etcd(或项目支持的配置中心);
- 启动 hotkey-server;
- 启动 hotkey-dashboard(可选,用于观察);
- 业务应用接入 SDK 并启动。
bash
# 以实际脚本为准
sh bin/start-server.sh
sh bin/start-dashboard.sh
9.2 压测验证
用 JMeter 或 wrk 对同一批 key 施压,观察:
- 热点 key 是否被正确识别;
- 本地缓存命中率是否上升;
- Redis QPS 是否明显下降;
- 规则下发延迟是否在可接受范围。
| 指标 | 压测前 | 压测后(热点生效) |
|---|---|---|
| Redis QPS | 高 | 显著下降 |
| 平均 RT | 较高 | 明显降低 |
| 热点识别延迟 | --- | 秒级 |
10. 常见问题与调优
| 现象 | 可能原因 | 调优方向 |
|---|---|---|
| 热点识别不及时 | 窗口过大或阈值过高 | 缩小窗口、降低阈值 |
| 误判过多 | 阈值过低 | 提高阈值,增加冷却时间 |
| 规则下发延迟 | 配置中心压力大 | 减少推送频率,批量下发 |
| 本地缓存不一致 | 缓存时长过长 | 缩短 TTL,增加主动失效 |
| 内存占用高 | 热点 key 过多 | 限制热点集合大小,设置淘汰策略 |
调优的核心原则是:宁可漏判少量热点,也不要误判大量普通 key,否则本地缓存会退化成「全量缓存」,反而增加内存压力。
11. 项目总结
JD Hotkey 的价值在于把「热点探测」这件事做成了可复用的基础设施:
- 主动探测:在客户端统计,第一时间发现热点;
- 异步上报:不阻塞业务主链路;
- 全局规则:中心服务统一计算,秒级下发;
- 本地隔离:热点流量在应用内消化,保护后端存储。
它和「抢购脚本」完全是两回事------前者是保护系统的架构组件 ,后者是绕过系统的自动化工具。理解 JD Hotkey 的设计,对做高并发系统、秒杀系统、缓存治理都很有参考价值。
后续可以继续研究的方向:热点探测算法的精度优化、多级缓存联动、与 Sentinel 等限流组件的整合等。