• 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 编译时必须开启 true,否则运行时无法获取参数名。

    6.4 BatchCollector — 双缓冲批量聚合(核心性能设计)

    这是整个 SDK 中最关键的性能组件,采用双缓冲 Map 轮转模式(借鉴京东 hotkey 项目),实现写入线程和上报线程之间的零阻塞。

    public class BatchCollector {
    
        private final ConcurrentHashMap map0 = new ConcurrentHashMap<>(256);
        private final ConcurrentHashMap 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 activeMap =
                (turn.get() % 2 == 0) ? map0 : map1;
            activeMap.computeIfAbsent(fullKey, k -> new LongAdder()).increment();
        }
    
        public List lockAndGetKeys() {
            long current = turn.incrementAndGet();
            ConcurrentHashMap readMap =
                (current % 2 == 0) ? map0 : map1;
    
            List keys = new ArrayList<>(readMap.size());
            for (Map.Entry 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 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 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 executePipelineBatch(List batch, long windowMs, long now) {
        List pipelineResults = redisTemplate.executePipelined(
            (RedisCallback) 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 依赖

    
    <dependency>
        <groupId>com.hotkeygroupId>
        <artifactId>hotkey-spring-boot-starterartifactId>
        <version>1.0.0-SNAPSHOTversion>
    dependency>
    
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    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 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

  • 相关阅读:
    图片转换成base64格式的优缺点
    骨灰级大 BOOS 总结出内部不传之密:多线程高并发笔记 + 视频版
    【软考】14.1 面向对象基本概念/分析设计测试
    pytorch框架下的逻辑回归代码解读
    Python轻量级Web框架:Bottle库
    使用 qshell 定时备份数据库文件到七牛云并删除7天前的备份(windows版)
    STM32 TIM(一)定时中断
    mbedtls开发环境搭建
    Verilog:【4】脉冲发生器(pulse_gen.sv)
    AGV|RGV小车RFID传感读卡器CK-G06A开发与用户手册技术说明
  • 原文地址:https://www.cnblogs.com/sun-10387834/p/22981404