热点终结者!Hotkey框架:一键解决项目突发危机

33 阅读9分钟

hotkey 入门

hotkey 简述

最近在学习过程中,偶然发现了京东发布的一篇关于HotKey框架的技术文章,觉得十分有意思,于是决定动手搭建并记录下来。本文将简要介绍这一框架所解决的问题,更详细的说明请参阅官方文档京东毫秒级热key探测框架设计HotKey框架的主要功能在于能够动态生成热点键(HotKey),并将其缓存,从而显著提升系统的响应速度。虽然使用Redis本地缓存也可以实现类似的效果,但这些传统方法往往是在首次调用接口后即开始缓存数据(很有可能只访问了一次,后续便不再访问),这可能会导致不必要的内存占用。相比之下,HotKey框架更加智能化,它能够在面对突发流量时,如某个商品突然成为爆款,用户访问量激增时,及时将该商品信息缓存。随着热度的消退,即当该商品的访问频率显著下降时,HotKey框架又会自动将该商品从缓存中移除,有效避免了资源浪费。

或许有读者会好奇,这样的框架是如何设计的呢?接下来,让我们一起探讨其架构设计。

105737_e5b876cd_303698.webp

具体都是什么含义,大家参照上述给出的官方文档可能会了解的更加清楚。

接下来我会按照我理解的方式来阐述一下,可能有些不准确,具体以官网为主:

  1. 配置信息获取

    • Dashboard通过界面允许管理员设置各个APP的key规则,这些规则随后被存储在etcd集群中。
    • 当Hotkey的服务端(Client端)启动时或定期地,它会从etcd集群中拉取这些配置信息,以便了解如何检测和处理热key。
  2. 服务端规则应用

    • 服务端(Client端)获取到etcd中的配置信息后,会将这些规则应用到其业务逻辑中。
    • 这些规则可能包括热key的定义(如在一定时间内出现特定次数的key被视为热key)、热key的处理方式(如本地缓存、拒绝访问等)。
  3. 热key判断与推送

    • 服务端(Client端)在其运行过程中,会不断检测来自用户的请求,并根据从etcd获取的规则来判断哪些key是热key。
    • 一旦检测到热key,服务端会将这些热key信息推送给worker端进行计算和进一步处理。
  4. Worker端计算

    • Worker端是一个独立部署的Java程序,它负责接收来自服务端(Client端)的热key推送。
    • Worker端会对这些热key进行累加计算,以确认它们是否仍然符合热key的定义。
    • 当确认某个key是热key后,Worker端会将这些信息回传给服务端(Client端)或直接推送到相关的服务集群内存中。
  5. 内存存储与访问

    • 一旦热key被确认并推送到服务集群内存中,这些热key及其相关的数据就会被存储在JVM内存中。
    • 客户端(即使用Hotkey的服务)可以访问这些内存中的热key数据,以快速响应用户的请求,从而减轻后端数据存储层的压力。
  6. 可视化监控与管理

    • Dashboard不仅用于设置规则,还提供了可视化界面来监控热key的状态和数量。
    • 管理员可以通过Dashboard来查看哪些key被识别为热key、它们的访问频率等信息。
    • 此外,Dashboard还允许管理员手动添加或删除热key,以及调整规则设置。

现在,我已经为大家介绍了它的功能和结构,接下来将直接进入实际操作环节。关于 Hotkey 的搭建,您可以直接参考官方提供的官方的安装教程

hotkey 安装与配置

1、安装Etcd

建议从 github 上下载稳定的版本(最新的版本低两个版本号) Etcd 即可,选择对应的操作系统版本。本次测试中,笔者使用的是 etcd 的 Windows 版本,版本号为 v3.5.15

下载后解压压缩包,会得到 3 个脚本:

  • etcd:etcd 服务本身
  • etcdctl:客户端,用于操作 etcd,比如读写数据
  • etcdutl:备份恢复工具

image.png

此时进入该目录,输入cmd打开终端,然后输入etcd即可启动。服务默认占用 2379 和 2380(服务默认占用 2379 和 2380 端口)

tutieshi_640x311_16s.gif

image.png

2、下载hotkey源码

直接在 Gitee(也就是上述安装参考文档) 上下载 Hotkey 的压缩包,解压后使用 IntelliJ IDEA 打开项目,并下载所需的依赖项。这一步可能需要一些时间,请耐心等待。

image.png

在等待依赖项下载的过程中,请顺手将本项目所依赖的JDK版本更改为JDK 8

worker端

你可以自由地修改个人的配置信息。

image.png

你可以根据自身需求自定义配置 etcd 的地址和启动端口,之后再启动 worker 节点。

注意:如果启动过程中报错出现以下错误,参考文章

Caused by: org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1

dashboard端

创建数据库并导入resource下db.sql文件。 配置一下application.yml里的数据库相关和etcdServer地址。启动dashboard项目,访问ip:端口,即可看到界面。

image.png

如何所示

image.png

默认账号密码为:

admin 123456

image.png

编译client端

进行打包,想要使用hotkey功能的项目,引入该依赖即可。

image.png

此时,Maven 会自动处理各个模块间的依赖关系,将它们打包成 JAR 文件,并安装到本地的 Maven 仓库中。这样一来,在本地的其他项目中就可以方便地引用这些依赖了。下面将会演示如何添加该依赖~

3、初始化应用

创建一个应用

首先,让我们搭建一个简易的Spring Boot项目。为了便于测试,我们将引入 Knife4j。鉴于我们的 Spring Boot 版本为 2.x,因此需要引入相应的依赖。

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
    <version>4.4.0</version>
</dependency>

yml文件配置为

# 接口文档配置
knife4j:
  enable: true
  openapi:
    title: "接口文档"
    version: 1.0
    group:
      default:
        api-rule: package
        # 监听的包 修改成相对应的controller包名即可
        api-rule-resources:
          - com.hu.controller
client 初始化

此时我们再来引入client的依赖

<dependency>
    <groupId>com.jd.platform.hotkey</groupId>
    <artifactId>hotkey-client</artifactId>
    <version>0.0.4-SNAPSHOT</version>
</dependency>

初始化代码

@Configuration
@ConfigurationProperties(prefix = "hotkey")
@Data
public class HotKeyConfig {
    /**
     * Etcd 服务器完整地址
     */
    private String etcdServer = "http://127.0.0.1:2379";
    /**
     * 应用名称
     */
    private String appName = "app";
    /**
     * 本地缓存最大数量
     */
    private int caffeineSize = 10000;
    /**
     * 批量推送 key 的间隔时间
     */
    private long pushPeriod = 1000L;
    /**
     * 初始化 hotkey
     */
    @Bean
    public void initHotkey() {
        ClientStarter.Builder builder = new ClientStarter.Builder();
        ClientStarter starter = builder.setAppName(appName)
                .setCaffeineSize(caffeineSize)
                .setPushPeriod(pushPeriod)
                .setEtcdServer(etcdServer)
                .build();
        starter.startPipeline();
    }
}
# 热 key 探测
hotkey:
  app-name: hotkeydemo
  caffeine-size: 10000
  push-period: 1000
  etcd-server: http://localhost:2379

配置好之后,我们来启动项目,检查能否正常启动项目。

dashboard配置

在我们上面只是打开了dashboard的界面,并没有进行相关配置,我们需要创建appName相关规则(怎么判定是热key)

创建appName

此时我们来创建一个用户,其中所属APP:跟上述配置的yml文件中的app-name值保持一致即可,如果不一致,那么监听的就不是一个应用,也就无法生效。

image.png

制定规则

需要指定对应的appName,然后制定规则,json字符串

image.png

[
    {
        "duration": 600,
        "key": "hotkey_id_",
        "prefix": true,
        "interval": 5,
        "threshold": 10,
        "desc": "热键测试id"
    }
]

上述规则意思就是:以hotkey_id_为前缀的key,5s内请求次数达到了10次,那么就判定为热key,存储600s

4、验证

在着手编写测试代码之前,我们应当先熟悉官方安装指南中列出的几个关键方法,这将有助于后续的代码编写工作。

在完成上述步骤并确保无误后,我们可以开始进行测试了。首先,让我们创建一个 Controller 类,并编写相应的测试代码。

@GetMapping("getCacheValue/{id}")
public String getCacheValue(@PathVariable("id") String id) {
    String infoFromDb = "";
    Object infoFromCache = JdHotKeyStore.getValue("hotkey_id_" + id);
    if (infoFromCache == null) {
        // 缓存中没有数据,根据业务逻辑获得数据
        infoFromDb = "something information from db~~~";
        // 如果是热键则会设置,如果不是则不设置
        JdHotKeyStore.smartSet("hotkey_id_" + id, infoFromDb);
    } else {
        //使用缓存好的value即可
        return infoFromCache.toString();
    }
    return infoFromDb;
}

然后启动项目,访问Knife4j的文档地址:http://ip:port/doc.html即可查看文档。然后进行调试,

image.png

当在5s内发超过10次请求时大家再来打断点查看效果

image.png

此时也可以在dashboard中查看是否有缓存

image.png

此时抛出一个问题:如果当5s内,刚好是第10次请求来临,那么此时判断该键是不是热key?那么第11次请求呢?如果是热key,那么是从缓存中获取数据还是根据业务逻辑获取数据?第12次呢?小伙伴们需要仔细思考官网给出的这几个方法的含义哦。本文并没有过多解释其方法的特性。

5、优化

实际上,到此为止我们已经完成了对热键(hotkey)处理的基本介绍。不过,在这里我们将进一步优化这一过程——通过采用面向切面编程(AOP)技术来封装热键处理逻辑。这样一来,我们就无需为每个接口单独编写大量内嵌代码,从而提高了代码的可维护性和复用性。

有小伙伴们对于切面不太了解的可以去笔者写的一篇文章快速入门自定义注解。话不多说直接上代码。

自定义注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HotKeyCacheable {
    /**
     * key 支持spel表达式
     * @return
     */
    String key() default "";
    /**
     * 前缀
     * @return
     */
    String prefix() default "hotkey_id_";
}

切面

@Component
@Aspect
public class HotKeyAspect {
     // spel 解析
    private final ExpressionParser parser = new SpelExpressionParser();

    @Around("@annotation(hotKeyCacheable)")
    public Object logAround(ProceedingJoinPoint joinPoint, HotKeyCacheable hotKeyCacheable) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        EvaluationContext context = new StandardEvaluationContext(joinPoint.getThis());
        for (int i = 0; i < method.getParameterCount(); i++) {
            context.setVariable(method.getParameters()[i].getName(), joinPoint.getArgs()[i]);
        }
        String keyExpression = hotKeyCacheable.key();
        String key = (String) parser.parseExpression(keyExpression).getValue(context);
        // 前缀
        String prefix = hotKeyCacheable.prefix();
        String hotKey = prefix + key;
        Object infoFromCache = JdHotKeyStore.getValue(hotKey);
        Object infoFromDb;
        if (infoFromCache == null) {
            infoFromDb = joinPoint.proceed();
            JdHotKeyStore.smartSet(hotKey, infoFromDb);
            return infoFromDb;
        } else {
            //使用缓存好的value即可
            return infoFromCache.toString();
        }
    }
}

关于SpEL(Spring Expression Language)表达式的解析代码,大家不必死记硬背。只需了解这一工具的存在,当需要使用时,可以借助AI自动生成相应的代码。这样既能提高效率,又能避免记忆负担。

接下来,让我们对 Controller 进行一番改造吧。

@GetMapping("getCacheValue/{id}")
@HotKeyCacheable(key = "#id") // 支持spel表达式了
public String getCacheValue(@PathVariable("id") String id) {
    return "something information from db~~~" + id;
}

相比之下,是否感觉更加简洁明了呢?

至此,本文已接近尾声。希望上述内容能为您带来帮助。如果您觉得有所收获,欢迎关注,让我们在知识的旅途中不再迷失方向~