HotKey Platform — 热Key检测平台

从架构设计到模块解析,从数据流转到上手使用,一文讲透热Key检测平台的完整实现。


目录


一、项目背景与定位

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 注解 — 声明式监控入口

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HotKey {
    /** 规则名称(与控制台配置的规则匹配) */
    String rule();
    /** SpEL 表达式,用于从方法参数中提取被监控的 Key */
    String key();
}

使用示例:

@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 通知拦截执行流程:

@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 项目),实现写入线程和上报线程之间的零阻塞

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)执行一次上报:

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 脚本详解:

-- 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:

@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       → 注册启动监听器

配置项说明:

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 批量执行(性能关键):

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 上报脚本不同,此脚本仅做窗口清理 + 计数,不写入数据:

-- KEYS[1]: Redis ZSet Key
-- ARGV[1]: 规则配置的窗口大小(毫秒)
-- ARGV[2]: 当前时间戳

redis.call('ZREMRANGEBYSCORE', redisKey, 0, now - window)
return redis.call('ZCARD', redisKey)

markAsHot — 热Key持久化

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 关键索引

-- 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 编译与部署

第一步:编译整个项目

cd hotkey-platform
mvn clean compile

第二步:初始化数据库

mysql -u root -p < hotkey-server/src/main/resources/db/init.sql

脚本会创建数据库 hotkey_platform、4 张表,以及预置示例数据。

第三步:修改服务端配置

编辑 hotkey-server/src/main/resources/application.yml

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

第四步:启动服务端

# 开发环境
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 到本地仓库

mvn clean install -pl hotkey-spring-boot-starter -am

10.3 业务系统接入(三步完成)

Step 1:添加 Maven 依赖

<!-- 热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

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:在代码中添加注解

@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 手动注册

# 注册应用
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 验证与测试

# 频繁调用被监控的接口
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

posted @ 2026-09-15 13:56  佛祖让我来巡山  阅读(37)  评论(0)    收藏  举报

佛祖让我来巡山博客站 - 创建于 2018-08-15

开发工程师个人站,内容主要是网站开发方面的技术文章,大部分来自学习或工作,部分来源于网络,希望对大家有所帮助。

Bootstrap中文网