从架构设计到模块解析,从数据流转到上手使用,一文讲透热Key检测平台的完整实现。
目录
- 一、项目背景与定位
- 二、系统架构总览
- 三、技术选型
- 四、项目模块结构
- 五、数据流转全链路
- [六、SDK 模块深度解析](#六、SDK 模块深度解析)
- [七、Server 服务端深度解析](#七、Server 服务端深度解析)
- 八、数据库设计
- 九、控制台与可视化
- 十、完整使用指南
- 十一、关键设计决策与性能分析
- 十二、总结与展望
一、项目背景与定位
1.1 什么是热Key?
在高并发的业务场景中,某些数据(如秒杀商品ID、热门课程ID、爆款商品SKU)会在短时间内被大量请求反复访问。这些被高频命中的数据就是所谓的热Key。
热Key的危害不容小觑:
- 数据库压力集中:大量请求打到同一行数据,导致数据库连接池耗尽
- 缓存击穿:热Key过期瞬间,海量请求穿透到数据库
- 资源倾斜:单机/单节点承载过多请求,集群负载均衡失效
1.2 HotKey Platform 做了什么?
HotKey Platform 是一个实时热Key检测与可视化平台,核心能力包括:
| 能力 | 说明 |
|---|---|
| 无侵入采集 | 通过注解 + AOP 零侵入地拦截业务方法调用 |
| 高性能聚合 | 双缓冲 Map + LongAdder 实现业务线程零阻塞 |
| 滑动窗口检测 | 基于 Redis ZSet + Lua 脚本实现精确的滑动窗口计数 |
| 自动发现注册 | 业务系统启动时自动向平台注册应用和规则 |
| 可视化控制台 | 仪表盘、趋势图、Top10 排行、QPS 实时监控 |
| 生命周期管理 | 热Key自动冷却、过期记录自动清理 |
二、系统架构总览
2.1 三大角色
┌──────────────────┐ ┌─────────────┐ ┌──────────────────┐
│ 业务系统(SDK) │ ──────→ │ Redis │ ──────→ │ hotkey-server │
│ hotkey-spring- │ 上报 │ 数据中转站 │ 扫描 │ 检测 + 持久化 │
│ boot-starter │ │ │ │ + 控制台 │
└──────────────────┘ └─────────────┘ └──────────────────┘
| 角色 | 部署位置 | 职责 |
|---|---|---|
| hotkey-spring-boot-starter(SDK) | 嵌入业务系统 JVM | 拦截方法调用、采集 Key、批量上报到 Redis |
| Redis | 独立中间件 | 高速数据中转站,接收 SDK 上报的访问计数,供服务端扫描 |
| hotkey-server(服务端) | 独立服务(端口 8089) | 定时扫描 Redis、判定热Key、持久化到 MySQL、提供控制台 API |
2.2 架构全景图
┌─────────────────────────────────────────────────────────────────────┐
│ 业务系统(Demo) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ student-demo │ │ mall-demo │ ← Spring Boot 应用 │
│ │ :8090 │ │ :8091 │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ @HotKey 注解 │ @HotKey 注解 │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ hotkey-spring-boot-starter(SDK) │ │
│ │ HotKeyAspect → BatchCollector → RedisReporter │ │
│ │ HotKeyRegistry(启动时自动注册) │ │
│ └──────────────────────┬───────────────────────────────────┘ │
└─────────────────────────┼───────────────────────────────────────────┘
│ 写入 Redis ZSet
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Redis │
│ hotkey:registry:{appName} ← 注册键(TTL 24h) │
│ hotkey:{appName}:{rule}:{key} ← 有序集合(滑动窗口数据) │
└──────────────────────────┬──────────────────────────────────────────┘
│ SCAN + Lua 嗅探
▼
┌─────────────────────────────────────────────────────────────────────┐
│ hotkey-server(:8089) │
│ DetectorService ← 每 2s 嗅探扫描 + Pipeline 批量 Lua │
│ StatsService ← 仪表盘/记录/趋势查询 │
│ MySQL ← hotkey_record / hotkey_stats / hotkey_rule │
└──────────────────────────┬──────────────────────────────────────────┘
│ REST API
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 前端仪表盘(ECharts) │
│ 趋势折线图 | 应用占比饼图 | Top10 柱状图 | 检测 QPS 实时曲线 │
│ 规则堆叠柱状图 | 阈值雷达图 | 活跃状态饼图 | 规则访问分布 │
└─────────────────────────────────────────────────────────────────────┘
三、技术选型
| 技术 | 版本 | 用途 |
|---|---|---|
| Java | 17 | 开发语言 |
| Spring Boot | 3.5.3 | 应用框架 |
| MyBatis-Plus | 3.5.17 | ORM 持久层 |
| Spring AOP | --- | 方法拦截与注解增强 |
| Spring SpEL | --- | 动态表达式解析提取 Key 值 |
| Redis (Lettuce) | --- | 高速数据中转 + Lua 原子操作 |
| MySQL | 5.7+ / 8.0 | 热Key记录与统计数据持久化 |
| Maven | 3.8+ | 多模块构建与依赖管理 |
| ECharts | --- | 前端可视化图表 |
四、项目模块结构
hotkey-platform/
├── pom.xml # 父 POM(依赖版本统一管理)
├── hotkey-spring-boot-starter/ # SDK 模块(业务系统引入)
│ ├── annotation/HotKey.java # @HotKey 注解定义
│ ├── aspect/HotKeyAspect.java # AOP 切面拦截 + SpEL 解析
│ ├── collector/BatchCollector.java # 双缓冲批量收集器
│ ├── reporter/RedisReporter.java # 定时 Lua 脚本上报
│ ├── registry/HotKeyRegistry.java # 启动时自动注册应用与规则
│ └── autoconfigure/ # Spring Boot 自动装配
├── hotkey-server/ # 监控平台服务端
│ ├── controller/ # REST API(热Key/规则/应用/认证)
│ ├── service/DetectorService.java # 嗅探检测 + Pipeline 批量判定
│ ├── service/StatsService.java # 统计查询 + 分页 + 过期清理
│ ├── service/AppService.java # 应用管理
│ ├── service/RuleService.java # 规则管理
│ ├── entity/ # 数据实体
│ ├── mapper/ # MyBatis-Plus Mapper
│ └── static/ # 前端(index.html + app.js + ECharts)
├── hotkey-demo-student/ # 演示-学生系统(:8090)
│ ├── controller/StudentController # 提供 /sweep、/batch 等压测接口
│ └── service/StudentService # @HotKey 标记的业务方法
└── hotkey-demo-mall/ # 演示-商城系统(:8091)
├── controller/MallController # 同上
└── service/MallService # 商品/用户/订单/库存查询
五、数据流转全链路
一条热Key从产生到最终冷却,经历以下 8 个阶段:
① 方法调用 → ② AOP拦截 → ③ SpEL提取Key → ④ 双Map本地聚合
→ ⑤ Lua脚本写入Redis → ⑥ 服务端定时扫描判定 → ⑦ MySQL持久化
→ ⑧ 热Key冷却与过期清理
完整数据流图:
业务系统 JVM
┌──────────────────────────────────────────────────────────────┐
│ │
│ Controller → Service │
│ │ │
│ ┌───────▼────────┐ │
│ │ @HotKey 注解 │ ① 方法被调用 │
│ │ rule="student" │ │
│ │ key="#id" │ │
│ └───────┬────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ HotKeyAspect │ ② AOP 拦截 │
│ │ SpEL 解析 │ ③ 提取 keyValue = "42" │
│ └───────┬────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ BatchCollector │ ④ 双Map + LongAdder 聚合 │
│ │ map0 / map1 │ "app:student:42" → count=15 │
│ └───────┬────────┘ │
│ │ 每 500ms 轮转 │
│ ┌───────▼────────┐ │
│ │ RedisReporter │ ⑤ Lua 原子写入 ZSet │
│ │ 定时上报 │ ZADD hotkey:app:student:42 │
│ └───────┬────────┘ │
└──────────────────┼───────────────────────────────────────────┘
│
┌──────▼──────┐
│ Redis │ ZSet 滑动窗口
│ ZSet 存储 │ score = 时间戳, member = 唯一标识
└──────┬──────┘
│ 每 2 秒扫描
┌──────────────────┼───────────────────────────────────────────┐
│ ┌───────▼────────┐ │
│ │ DetectorService │ ⑥ 加载活跃规则 │
│ │ SCAN + Pipeline│ 匹配 Redis Key 模式 │
│ │ Lua 计数判定 │ count >= threshold → 热Key! │
│ └───────┬────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ MySQL 持久化 │ ⑦ hotkey_record(热Key记录) │
│ │ │ hotkey_stats (分钟级统计) │
│ └───────┬────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ 多重清理机制 │ ⑧ 冷却标记 + 记录删除 │
│ │ │ Redis Key TTL 自动过期 │
│ └────────────────┘ │
│ │
│ hotkey-server │
└──────────────────────────────────────────────────────────────┘
六、SDK 模块深度解析
SDK 模块(hotkey-spring-boot-starter)是整个系统的入口,嵌入在业务系统 JVM 中运行。它负责拦截方法调用、提取 Key 值、本地聚合、批量上报到 Redis。
6.1 @HotKey 注解 --- 声明式监控入口
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HotKey {
/** 规则名称(与控制台配置的规则匹配) */
String rule();
/** SpEL 表达式,用于从方法参数中提取被监控的 Key */
String key();
}
使用示例:
java
@Service
public class StudentService {
@HotKey(rule = "student", key = "#id")
public Student getStudentById(Long id) {
// 业务逻辑...
}
@HotKey(rule = "student", key = "#name")
public Student getStudentByName(String name) {
// 业务逻辑...
}
}
设计要点:
- 注解标注在
ElementType.METHOD上,保留策略为RUNTIME,确保 AOP 能在运行时通过反射读取 rule属性将方法调用与规则配置绑定,一个规则可以对应多个方法key属性使用 SpEL 表达式,支持从任意参数名、嵌套属性中提取值
6.2 HotKeyAspect --- AOP 切面拦截
当业务方法被调用时,Spring AOP 的 @Around 通知拦截执行流程:
java
@Aspect
@Slf4j
@RequiredArgsConstructor
public class HotKeyAspect {
private final BatchCollector collector;
private final ExpressionParser parser = new SpelExpressionParser();
private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
@Value("${hotkey.app-name:default-app}")
private String appName;
@Around("@annotation(hotKey)")
public Object intercept(ProceedingJoinPoint joinPoint, HotKey hotKey) throws Throwable {
try {
String keyValue = extractKey(joinPoint, hotKey.key());
if (keyValue != null && !keyValue.isEmpty()) {
collector.collect(appName, hotKey.rule(), keyValue);
}
} catch (Exception e) {
log.warn("[热Key SDK] 切面异常: {}", e.getMessage());
}
return joinPoint.proceed(); // 无论采集是否成功,都执行业务方法
}
}
关键设计原则:
- 零侵入:采集逻辑包裹在 try-catch 中,任何异常只打 warn 日志,不影响业务方法执行
- 先采集后执行 :在
joinPoint.proceed()之前采集,确保即使方法抛异常也能记录本次调用 - 异步无阻塞 :
collector.collect()仅写入内存 Map,耗时为 O(1)
6.3 SpEL 表达式解析
SpEL 解析过程将注解中的表达式(如 "#id")结合方法实际参数,计算出具体的 Key 值:
注解: @HotKey(rule = "student", key = "#id")
方法: getStudentById(Long id)
调用: getStudentById(42L)
SpEL 解析过程:
1. 获取方法参数名: ["id"]
2. 获取方法参数值: [42]
3. 构建 EvaluationContext,设置变量: #id = 42
4. 解析表达式 "#id" → 得到值 42
5. toString() → keyValue = "42"
支持的 SpEL 表达式示例:
| 表达式 | 说明 | 示例值 |
|---|---|---|
#id |
直接取参数值 | "42" |
#name |
取字符串参数 | "张三" |
#user.id |
取对象嵌套属性 | "100" |
#request.getHeader('X-Id') |
调用方法 | "abc" |
注意 :Maven 编译时必须开启
<parameters>true</parameters>,否则运行时无法获取参数名。
6.4 BatchCollector --- 双缓冲批量聚合(核心性能设计)
这是整个 SDK 中最关键的性能组件 ,采用双缓冲 Map 轮转模式 (借鉴京东 hotkey 项目),实现写入线程和上报线程之间的零阻塞。
java
public class BatchCollector {
private final ConcurrentHashMap<String, LongAdder> map0 = new ConcurrentHashMap<>(256);
private final ConcurrentHashMap<String, LongAdder> map1 = new ConcurrentHashMap<>(256);
private final AtomicLong turn = new AtomicLong(0);
public void collect(String appName, String rule, String keyValue) {
String fullKey = appName + ":" + rule + ":" + keyValue;
ConcurrentHashMap<String, LongAdder> activeMap =
(turn.get() % 2 == 0) ? map0 : map1;
activeMap.computeIfAbsent(fullKey, k -> new LongAdder()).increment();
}
public List<String> lockAndGetKeys() {
long current = turn.incrementAndGet();
ConcurrentHashMap<String, LongAdder> readMap =
(current % 2 == 0) ? map0 : map1;
List<String> keys = new ArrayList<>(readMap.size());
for (Map.Entry<String, LongAdder> entry : readMap.entrySet()) {
int count = (int) entry.getValue().sum();
keys.add(entry.getKey() + ":" + count);
}
readMap.clear();
return keys;
}
}
轮转时序图:
时间轴 ──────────────────────────────────────────────→
业务线程: 写map0 写map0 写map0 写map1 写map1
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌───────┬───────┬───────┐ ┌───────┬───────┐
map0 │ +1+1+1│ +1 │ +1 │清0 │ │ │ ...
└───────┴───────┴───────┘ └───────┴───────┘
┌───────┬───────┬───────┐ ┌───────┬───────┐
map1 │ │ │ │清0│ +1+1 │ +1 │ ...
└───────┴───────┴───────┘ └───────┴───────┘
▲
上报线程: │
──── 500ms ──── [lockAndGetKeys] ──── 500ms ────
读map0, 清空map0 读map1, 清空map1
为什么用 LongAdder 而不是 AtomicLong?
| 对比项 | AtomicLong | LongAdder |
|---|---|---|
| 高并发写入 | 所有线程 CAS 竞争同一个 value | 内部 Cell 数组分散竞争 |
| 性能 | 高并发下 CPU 空转严重 | 接近线性扩展 |
| 读取 | 直接读 value | 需 sum() 遍历 Cell(仅在上报时调用,频率低) |
| 适用场景 | 低并发 | 高并发写入、低频率读取(完美匹配本场景) |
6.5 RedisReporter --- Lua 脚本原子写入
RedisReporter 内部启动一个 ScheduledExecutorService,每隔 reportPeriodMs(默认 500ms)执行一次上报:
java
private void report() {
List<String> keys = collector.lockAndGetKeys();
long now = System.currentTimeMillis();
for (String entry : keys) {
int lastColon = entry.lastIndexOf(':');
String fullKey = entry.substring(0, lastColon);
int count = Integer.parseInt(entry.substring(lastColon + 1));
String redisKey = "hotkey:" + fullKey;
String member = now + "-" + Thread.currentThread().getId();
redisTemplate.execute(redisScript,
Collections.singletonList(redisKey),
String.valueOf(windowMs), String.valueOf(now), member, String.valueOf(count));
}
}
Lua 脚本详解:
lua
-- KEYS[1]: Redis Key,格式为 "hotkey:{appName}:{ruleKey}:{keyValue}"
-- ARGV[1]: 滑动窗口大小(毫秒)
-- ARGV[2]: 当前时间戳(毫秒)
-- ARGV[3]: 成员标识(时间戳-线程ID)
-- ARGV[4]: 本批次的访问计数
-- 第一步:移除滑动窗口之外的过期成员
redis.call('ZREMRANGEBYSCORE', redisKey, 0, now - window)
-- 第二步:将本次计数展开为多条记录写入 ZSet
for i = 1, count do
redis.call('ZADD', redisKey, now, member .. '-' .. i)
end
-- 第三步:设置 Key 的 TTL 为窗口的 2 倍
redis.call('PEXPIRE', redisKey, window * 2)
-- 第四步:返回当前窗口内的总成员数
return redis.call('ZCARD', redisKey)
Redis ZSet 数据结构示意:
Key: hotkey:student-management:student:42
ZSet 内容(score = 写入时间戳):
┌──────────────────────┬─────────────────┐
│ member │ score │
├──────────────────────┼─────────────────┤
│ 1726000000000-14-1 │ 1726000000000 │
│ 1726000000000-14-2 │ 1726000000000 │
│ 1726000000500-14-1 │ 1726000000500 │
│ 1726000000500-14-2 │ 1726000000500 │
│ 1726000000500-14-3 │ 1726000000500 │
└──────────────────────┴─────────────────┘
ZCARD = 窗口内所有成员数 = 该 Key 在窗口内的总访问次数
6.6 HotKeyRegistry --- 应用自动注册
应用启动时,HotKeyRegistry 监听 ApplicationReadyEvent,自动扫描所有 @HotKey 注解,将应用信息和规则写入 Redis:
java
@EventListener(ApplicationReadyEvent.class)
public void registerOnStartup() {
// 扫描所有 Bean 中带 @HotKey 注解的方法,收集规则名
Set<String> rules = new HashSet<>();
for (Object bean : allBeans.values()) {
Class<?> targetClass = AopUtils.getTargetClass(bean); // 穿透 CGLIB 代理
for (Method method : targetClass.getDeclaredMethods()) {
HotKey hotKey = method.getAnnotation(HotKey.class);
if (hotKey != null) rules.add(hotKey.rule());
}
}
// 写入 Redis 注册键
String registryKey = "hotkey:registry:" + appName;
String registryValue = ruleList + ";windowMs=" + windowMs + ",threshold=" + threshold;
redisTemplate.opsForValue().set(registryKey, registryValue, 24, TimeUnit.HOURS);
}
Redis 注册键格式:
Key: hotkey:registry:{appName}
Value: student,score;windowMs=60000,threshold=20
TTL: 24 小时
关键细节 :使用
AopUtils.getTargetClass()穿透 CGLIB 代理,确保能读到原始类上的@HotKey注解。代理类的方法上没有注解,必须拿到目标类。
6.7 自动装配机制
业务系统只需引入一个依赖,Spring Boot 自动完成所有装配:
Spring Boot 启动
│
▼
读取 META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports
│
▼
加载 HotKeyAutoConfiguration
│
├── @EnableConfigurationProperties(HotKeyProperties.class)
│ → 绑定 application.yml 中 hotkey.* 配置
│
├── @ConditionalOnClass(StringRedisTemplate.class)
│ → 仅当 classpath 有 Redis 依赖时激活
│
├── @Bean BatchCollector → 创建双Map收集器
├── @Bean HotKeyAspect → 注册 AOP 切面
├── @Bean RedisReporter → 启动定时上报线程
└── @Bean HotKeyRegistry → 注册启动监听器
配置项说明:
yaml
hotkey:
app-name: student-management # 应用名称,必须与控制台注册的一致
window-ms: 60000 # 滑动窗口大小(毫秒),默认 60 秒
threshold: 20 # 热Key判定阈值
report-period-ms: 500 # 批量上报周期(毫秒),默认 500ms
七、Server 服务端深度解析
hotkey-server 是独立部署的监控服务端,负责定时扫描 Redis、判定热Key、持久化到 MySQL、提供 REST API 和前端控制台。
7.1 DetectorService --- 热Key嗅探引擎
这是服务端的核心组件,承载了 5 个定时任务:
| 任务 | 频率 | 职责 |
|---|---|---|
scanHotKeys() |
每 2 秒 | SCAN + Pipeline 嗅探热Key,写入 MySQL |
autoRegisterAppsAndRules() |
每 30 秒 | 扫描 Redis 注册键,自动创建 App/Rule |
coolExpiredKeys() |
每 10 秒 | 将超时未活跃的热Key标记为已冷却 |
cleanCooledRecords() |
每 2 分钟 | 删除冷却超过 30 分钟的记录 |
cleanExpiredStats() |
每 5 分钟 | 删除超过 30 分钟的分钟级统计数据 |
嗅探扫描流程
┌─────────────────────────────────────────────────────────────┐
│ DetectorService.scanHotKeys() │
│ 每 2 秒执行一次 │
└──────────────────────────┬──────────────────────────────────┘
│
┌────────────▼────────────┐
│ 1. 加载所有活跃规则 │
│ 过滤已下线应用 │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ 2. 对每条规则用 SCAN │
│ 遍历匹配的 Redis key │
│ pattern: hotkey:app: │
│ rule:* │
│ count: 1000 │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ 3. 每 500 个 key 一批 │
│ Pipeline 批量执行 │
│ Lua 计数脚本 │
└────────────┬────────────┘
│
┌────────┴────────┐
│ │
count >= threshold count < threshold
│ │
┌────────▼────────┐ 跳过
│ 判定为热Key! │
│ markAsHot() │
└─────────────────┘
Pipeline 批量执行(性能关键):
java
private List<Long> executePipelineBatch(List<String> batch, long windowMs, long now) {
List<Object> pipelineResults = redisTemplate.executePipelined(
(RedisCallback<Object>) connection -> {
for (String key : batch) {
connection.eval(SCRIPT_BYTES, ReturnType.INTEGER, 1,
keyBytes, windowBytes, nowBytes);
}
return null;
});
// 转换结果...
}
将 500 个 key 的 Lua 脚本执行打包成一次网络往返,相比逐 key 执行减少 499 次网络 RTT,检测 QPS 从约 100 提升到 5000-10000+。
服务端 Lua 计数脚本
与 SDK 上报脚本不同,此脚本仅做窗口清理 + 计数,不写入数据:
lua
-- KEYS[1]: Redis ZSet Key
-- ARGV[1]: 规则配置的窗口大小(毫秒)
-- ARGV[2]: 当前时间戳
redis.call('ZREMRANGEBYSCORE', redisKey, 0, now - window)
return redis.call('ZCARD', redisKey)
markAsHot --- 热Key持久化
java
private void markAsHot(Rule rule, String keyValue, int count) {
// 1. 查询是否已存在活跃记录(去重)
HotKeyRecord existing = recordMapper.selectOne(
WHERE app_name = ? AND rule_key = ? AND key_value = ? AND status = 1);
if (existing != null) {
// 已存在 → 更新访问次数和最近检测时间
existing.setAccessCount(count);
existing.setLastSeen(LocalDateTime.now());
recordMapper.updateById(existing);
} else {
// 不存在 → 插入新记录
HotKeyRecord record = new HotKeyRecord(...);
recordMapper.insert(record);
}
// 2. 同步写入分钟级统计表(用于趋势图表)
HotKeyStats stats = new HotKeyStats(...);
stats.setStatMinute(LocalDateTime.now().withSecond(0).withNano(0));
statsMapper.insert(stats);
}
热Key生命周期管理
[首次检测为热Key] → 插入 hotkey_record(status=1 活跃)
↓
[后续检测仍为热Key] → 更新 accessCount + lastSeen
↓
[冷却检查:lastSeen + windowSec*2 < now] → status=0 已冷却
↓
[清理检查:冷却超过 30 分钟] → 删除记录
7.2 StatsService --- 统计与查询
提供仪表盘数据、分页查询、趋势分析和过期清理:
| 方法 | 功能 | 数据来源 |
|---|---|---|
getDashboard() |
热Key总数 + 今日新增 | hotkey_record |
listRecords() |
分页查询热Key记录(支持按应用/规则筛选) | hotkey_record |
getTrend() |
分钟级趋势数据 | hotkey_stats |
clearRecords() |
清空所有热Key记录 | --- |
cleanExpiredStats() |
每 5 分钟清理 30 分钟前的统计数据 | --- |
7.3 认证机制
- 固定账号密码:
admin/admin - 登录后生成 UUID Token,存入 Redis(TTL = 2 小时)
- 前端
localStorage保存 Token,每次 API 请求通过Authorization头传递 - 使用 Spring
HandlerInterceptor校验 Token(非 Spring Security,轻量级实现)
八、数据库设计
8.1 表结构总览
┌─────────────────┐ ┌─────────────────┐
│ hotkey_app │ │ hotkey_rule │
│ 接入应用注册表 │────→│ 规则配置表 │
│ │ │ │
│ id │ │ id │
│ app_name (唯一) │ │ app_name │
│ app_desc │ │ rule_key │
│ status │ │ rule_desc │
│ created_at │ │ window_sec │
└─────────────────┘ │ threshold │
│ duration │
│ status │
└─────────────────┘
│ 规则触发
▼
┌─────────────────┐ ┌─────────────────┐
│ hotkey_record │ │ hotkey_stats │
│ 热Key记录表 │ │ 分钟级统计表 │
│ │ │ │
│ id │ │ id │
│ app_name │ │ app_name │
│ rule_key │ │ rule_key │
│ key_value │ │ key_value │
│ access_count │ │ stat_minute │
│ first_seen │ │ access_count │
│ last_seen │ │ │
│ status │ │ │
│ created_at │ │ │
└─────────────────┘ └─────────────────┘
8.2 各表详细说明
| 表名 | 职责 | 写入时机 | 清理策略 |
|---|---|---|---|
hotkey_app |
注册接入的应用 | 控制台手动创建 / SDK 自动注册 | 手动删除 |
hotkey_rule |
配置检测规则 | 控制台手动创建 / SDK 自动注册 | 手动删除 |
hotkey_record |
记录被判定为热Key的数据 | DetectorService 每 2 秒判定后写入 | 冷却 30 分钟后自动删除 |
hotkey_stats |
分钟级访问统计(趋势图数据源) | DetectorService 判定热Key时同步写入 | 每 5 分钟自动清理 30 分钟前数据 |
8.3 关键索引
sql
-- hotkey_rule: 防止同一应用下重复规则
UNIQUE KEY uk_app_rule (app_name, rule_key)
-- hotkey_record: 加速按应用+规则查询
INDEX idx_app_rule (app_name, rule_key)
INDEX idx_created (created_at)
-- hotkey_stats: 加速趋势查询
INDEX idx_app_minute (app_name, stat_minute)
九、控制台与可视化
9.1 功能菜单
| 菜单 | 功能 | 数据来源 |
|---|---|---|
| 仪表盘 | 统计卡片 + 8 个可视化图表 | dashboard / records / rules / trend / metrics API |
| 热Key记录 | 查看、筛选、清空热Key记录 | /api/hotkeys/records |
| 规则管理 | CRUD 规则配置 | /api/rules |
| 应用管理 | CRUD 接入应用、启用/禁用 | /api/apps |
9.2 仪表盘图表
| 图表 | 类型 | 数据源 | 说明 |
|---|---|---|---|
| 应用热Key占比 | 环形饼图 | records 按 appName 聚合 | 各应用热Key访问量占比 |
| 热Key Top 10 排行 | 横向柱状图 | records 按 accessCount 排序 | 访问量最高的 10 个 Key |
| 热Key趋势 | 折线图 | stats 按时间排列 | 支持 30 分钟 ~ 24 小时切换 |
| 各应用规则热度对比 | 分组柱状图 | rules 按 appName 分组 | 各应用配置的规则分布 |
| 规则阈值配置对比 | 雷达图 | rules 的 window/threshold/duration | 三维度对比各规则配置 |
| 热Key活跃状态分布 | 环形图 | records 按 status 聚合 | 活跃 vs 已冷却的比例 |
| 各规则访问次数分布 | 双柱状图 | records 按 ruleKey 聚合 | 总访问 vs 平均访问对比 |
| 检测 QPS 实时曲线 | 折线图 | 内存 ConcurrentLinkedDeque | QPS / 扫描耗时 / 扫描数量 |
9.3 API 接口清单
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/auth/login |
管理员登录 |
| GET | /api/auth/me |
获取当前用户信息 |
| POST | /api/auth/logout |
退出登录 |
| GET | /api/hotkeys/dashboard |
仪表盘统计数据 |
| GET | /api/hotkeys/records?page=1&pageSize=20&appName=&ruleKey= |
热Key记录分页查询 |
| GET | /api/hotkeys/trend?minutes=60 |
热Key趋势数据 |
| GET | /api/hotkeys/metrics |
检测性能指标(QPS 等) |
| DELETE | /api/hotkeys/records/clear |
清空所有热Key记录 |
| GET | /api/rules?appName= |
查询规则列表 |
| POST | /api/rules |
创建规则 |
| PUT | /api/rules |
更新规则 |
| DELETE | /api/rules/{id} |
删除规则 |
| GET | /api/apps |
查询应用列表 |
| POST | /api/apps |
创建应用 |
| PUT | /api/apps/{id}/status?status=0/1 |
启用/禁用应用 |
| DELETE | /api/apps/{id} |
删除应用 |
十、完整使用指南
10.1 环境要求
| 组件 | 版本要求 | 说明 |
|---|---|---|
| JDK | 17+ | 编译和运行 |
| Maven | 3.8+ | 项目构建 |
| MySQL | 5.7+ / 8.0 | 存储热Key记录和规则 |
| Redis | 6.0+ | 滑动窗口计数 |
10.2 编译与部署
第一步:编译整个项目
bash
cd hotkey-platform
mvn clean compile
第二步:初始化数据库
bash
mysql -u root -p < hotkey-server/src/main/resources/db/init.sql
脚本会创建数据库 hotkey_platform、4 张表,以及预置示例数据。
第三步:修改服务端配置
编辑 hotkey-server/src/main/resources/application.yml:
yaml
server:
port: 8089
spring:
datasource:
url: jdbc:mysql://localhost:3306/hotkey_platform?useSSL=false&serverTimezone=Asia/Shanghai
username: root # ← 改为你的 MySQL 用户名
password: 123456 # ← 改为你的 MySQL 密码
data:
redis:
host: 127.0.0.1 # ← 改为你的 Redis 地址
port: 6379
第四步:启动服务端
bash
# 开发环境
mvn spring-boot:run -pl hotkey-server
# 生产环境
mvn clean package -pl hotkey-server -am
java -jar hotkey-server/target/hotkey-server-1.0.0-SNAPSHOT.jar
第五步:打包 SDK 到本地仓库
bash
mvn clean install -pl hotkey-spring-boot-starter -am
10.3 业务系统接入(三步完成)
Step 1:添加 Maven 依赖
xml
<!-- 热Key SDK -->
<dependency>
<groupId>com.hotkey</groupId>
<artifactId>hotkey-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<!-- 前提:业务系统必须有 Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Step 2:配置 application.yml
yaml
hotkey:
app-name: student-management # 应用名称
window-ms: 60000 # 滑动窗口大小(毫秒)
threshold: 20 # 热Key判定阈值
report-period-ms: 500 # 批量上报周期(毫秒)
spring:
data:
redis:
host: 127.0.0.1
port: 6379
Step 3:在代码中添加注解
java
@Service
public class StudentService {
@HotKey(rule = "student", key = "#id")
public Student getStudentById(Long id) {
return studentMapper.selectById(id);
}
@HotKey(rule = "student", key = "#name")
public List<Student> listStudents(String name) {
return studentMapper.selectByName(name);
}
}
重启业务系统,看到以下日志说明接入成功:
[HotKey SDK] Redis上报器已启动, 上报周期=500ms, 窗口时间=60000ms
[HotKey SDK] 应用已注册 'student-management', 规则: [student], 窗口: 60000ms, 阈值: 20
10.4 注册应用与规则
方式一:通过 API 手动注册
bash
# 注册应用
curl -X POST http://localhost:8089/api/apps \
-H "Content-Type: application/json" \
-d '{"appName":"student-management","appDesc":"学生管理系统"}'
# 配置规则
curl -X POST http://localhost:8089/api/rules \
-H "Content-Type: application/json" \
-d '{
"appName":"student-management",
"ruleKey":"student",
"ruleDesc":"按ID或姓名查询学生",
"windowSec":60,
"threshold":20,
"duration":300
}'
方式二:SDK 自动注册
业务系统启动后,SDK 的 HotKeyRegistry 会自动将应用信息和规则写入 Redis。Server 端的 autoRegisterAppsAndRules() 每 30 秒扫描一次 Redis 注册键,自动发现并创建应用和规则。无需任何手动操作。
10.5 验证与测试
bash
# 频繁调用被监控的接口
curl http://localhost:8090/api/student/42
curl http://localhost:8090/api/student/42
# ... 重复 20 次以上 ...
# 或者使用批量压测接口
curl "http://localhost:8090/api/student/batch?iterations=100"
curl "http://localhost:8090/api/student/sweep?startId=1&uniqueIds=100&repeats=25"
# 查看热Key仪表盘
curl http://localhost:8089/api/hotkeys/dashboard
# 查看热Key记录
curl http://localhost:8089/api/hotkeys/records
当某个 Key 在 60 秒内被访问超过 20 次,就会被判定为热Key并出现在记录中。
10.6 演示模块说明
项目自带两个演示应用,方便快速体验:
| 演示应用 | 端口 | 规则 | 接口 |
|---|---|---|---|
hotkey-demo-student |
8090 | student / score | /api/student/{id}、/api/student/name、/api/student/score/{id} |
hotkey-demo-mall |
8091 | product / user / order / stock | /api/mall/product/{id}、/api/mall/user/{id} 等 |
两个 Demo 都提供了压测接口:
| 接口 | 说明 |
|---|---|
/batch?iterations=100 |
批量热Key生成,一次请求产生 N×2 个热Key条目 |
/sweep?startId=1&uniqueIds=1000&repeats=25 |
ID自增压测,每个ID调用repeats次 |
/fast/{id} |
零延迟快速接口,压测极限用 |
十一、关键设计决策与性能分析
11.1 为什么选择 Redis 作为中间层?
业务系统 ──直接写入──→ MySQL?
✗ 高并发下数据库连接池成为瓶颈
✗ 每次方法调用都写库,I/O 开销巨大
业务系统 ──批量写入──→ Redis ──定时扫描──→ MySQL?
✓ SDK 只写 Redis(内存操作,微秒级延迟)
✓ 服务端定时批量判定后写 MySQL(降低 DB 压力)
✓ Redis ZSet 天然支持滑动窗口计数
✓ Redis TTL 自动清理过期数据
11.2 为什么用 ZSet 而不是简单的 INCR 计数器?
| 方案 | 优点 | 缺点 |
|---|---|---|
| INCR 计数器 | 简单 | 无法实现滑动窗口,只能固定窗口 |
| List + LTRIM | 可实现滑动窗口 | 非原子操作,需多次 RTT |
| ZSet + Lua | 原子滑动窗口,一次 RTT | Lua 脚本稍复杂 |
ZSet 方案的核心思想:每个访问事件作为 ZSet 中的一个成员,score 为时间戳 。通过 ZREMRANGEBYSCORE 移除窗口外的旧成员,再用 ZCARD 统计窗口内的成员数,整个过程在 Lua 脚本中原子执行。
11.3 滑动窗口原理
时间轴: ──────────────────────────────────────────→
|←──── window (60s) ────→|
| |
过期数据 │ 有效数据(窗口内) │ 未来
| |
ZREMRANGEBYSCORE 清除 ZCARD 统计这部分
11.4 性能指标估算
假设 report-period-ms = 500,100 个业务线程同时调用:
| 环节 | 耗时 | 说明 |
|---|---|---|
| AOP 拦截 + SpEL 解析 | ~1μs | 内存操作 |
| BatchCollector.collect() | ~0.1μs | LongAdder.increment() |
| Redis Lua 脚本执行 | ~0.5ms | 网络 RTT + Lua 执行 |
| DetectorService 扫描 | ~10ms | 每 2 秒一次,Pipeline 批量 |
| MySQL 写入 | ~2ms | 单条 INSERT/UPDATE |
对业务线程的影响 :仅增加 ~1μs(AOP + collect),完全无感。Redis 写入在独立线程中异步执行,不阻塞业务。
11.5 数据一致性保证
| 场景 | 处理方式 |
|---|---|
| SDK 上报过程中 Redis 宕机 | Lua 脚本原子执行,要么全成功要么全失败;catch 异常仅打 warn |
| 多个 SDK 实例并发上报同一 Key | Redis ZSet 天然并发安全,ZCARD 返回全局计数 |
| DetectorService 扫描与上报并发 | Redis SCAN + Lua 脚本原子执行,读取时数据一致 |
| 业务方法抛异常 | Key 已采集(在 proceed() 之前),不影响计数 |
十二、总结与展望
12.1 核心亮点回顾
| 设计点 | 亮点 |
|---|---|
| 双缓冲 Map 轮转 | 写入线程和上报线程零阻塞,借鉴京东 hotkey 成熟方案 |
| LongAdder 高并发计数 | 比 AtomicLong 在高并发下性能数量级提升 |
| Lua 原子滑动窗口 | 一次 RTT 完成清理+写入+计数,数据一致性有保障 |
| SCAN + Pipeline | 避免 KEYS 阻塞 Redis,批量执行降低网络开销 |
| 应用自动注册 | SDK 启动自动向平台注册,零手动配置 |
| 完整生命周期 | 热Key自动冷却、记录自动清理、统计数据定时清理 |
| 零侵入接入 | 一个注解 + 一个依赖,对业务代码完全无侵入 |
12.2 完整生命周期示例
以 student-management 系统查询学号 42 为例:
14:00:00.000 用户请求 GET /api/student/42
→ StudentService.getStudentById(42)
→ @HotKey(rule="student", key="#id") 触发
→ HotKeyAspect 拦截,SpEL 解析 #id = "42"
→ BatchCollector.collect("student-management", "student", "42")
→ map0["student-management:student:42"].increment() → count=1
→ joinPoint.proceed() 执行实际查询逻辑
14:00:00.001 又一次请求... → count=2
... (1 分钟内持续累加) ...
14:00:00.500 RedisReporter 定时触发
→ lockAndGetKeys() 切换到 map1,读取 map0
→ 得到 ["student-management:student:42:85"]
→ Lua 脚本写入 Redis ZSet(85 条成员)
→ PEXPIRE hotkey:...:42 120000
14:00:02.000 DetectorService.scanHotKeys() 触发
→ SCAN "hotkey:student-management:student:*"
→ Pipeline 批量 Lua 计数 → count=85
→ 85 >= 20 → 判定为热Key!
→ INSERT hotkey_record + hotkey_stats
14:00:04.000 再次扫描 → count=92 → UPDATE access_count=92
... (5 分钟后,该 Key 不再被访问) ...
14:05:10.000 Redis ZSet TTL 到期,Key 自动删除
14:05:20.000 coolExpiredKeys() → lastSeen + 120s < now → status=0 已冷却
14:35:20.000 cleanCooledRecords() → 冷却超过 30 分钟 → 删除记录
项目地址 :https://gitee.com/sun-guo-qiang/hotkey-platform.git
文档版本 :v1.0
适用版本 :hotkey-platform 1.0.0-SNAPSHOT
最后更新:2026-09-15