JD HotKey 入门使用
根据官方仓库描述:hotkey 是京东APP后台热数据探测框架,历经多次高压压测和京东 618、双 11 大促考验。
-
作用:
自动统计接口调用次数,人为配置规则来判断是不是热点数据。并且可支持分布式。
例子 :
-
现在有一个接口是 /api/qusetion/{id} 有时候不知道怎么回事这个接口在 1s 内用户访问了100w 次,如果每次都去请求像 mysql 这样的持久化数据库, IO 花费的资源是巨大,造成资源浪费不说,搞不好数据库压力太大导致直接挂了(那后果就大了……)。当然我们会想到用像 redis 去做一层缓存,redis 没命中才去找 mysql . 但是 redis 是需要我们人为手动的去控制那些数据该缓存,那些数据该预热的。那比如某天我家哥哥本来没那么火的(你根本不会想到为我家哥哥的发帖数据进行 redis 缓存)突然一天发了个打篮球的视频就是火了,全网点击量库库库往上涨,每秒点击量 100W 次,你没有为这些数据做缓存,用户疯狂攻击你的 mysql 导致挂了整个系统都瘫痪了(你甚至还在睡梦中,第二天带上假发去上班却发现工位上多了一份离职申请书)
-
你坐下操作将我家哥哥的数据存到 redis ,再重启 mysql ,终于系统又能正常运行了。打了一杯水,然后上个厕所,再从 gitee 上拉了 JDHotKey 的项目顺便把那个什么 喜人申请书撕得稀碎
-
所以 JDHotKey 针对这个例子干了什么可以避免上面这种情况的发生呢?
- 统计访问次数,服务器将接口的访问记录上报给worker,由 worker 来统计这个接口 在 某段内被访问过多少次。(当然不能服务器自己统计啦,因为又不只自己一台服务器提供了这个接口的服务)
- 判断是不是热值,这个是按照我们自己配置的规则来设置的,比如 我觉得 1s 内访问>=1W次那这个就是热点数据了
- 缓存这个这个热点数据,下次在访问的时候就从缓存中那就行了,别老来烦 mysql 这位差点让我丢掉工作的大哥
- 缓存是有存活时间的,时间到了就没有缓存了 又去访问 mysql (mysql才是最新的数据),那么下次访问又过于频繁,又缓存了一次而且还是最新的数据 妙啊。
-
-
架构图(从官网扒的)
-
使用
-
安装 etcd 并启动 (把它当作一个注册中心来看就行,比 zookeeper 轻便 性能高 毕竟是 go 写的)
直接从 github 上下载最新版本的 Etcd 即可,选择对应的操作系统版本:
下载之后解压,双击 etcd.exe 可以看到刷的一下日志就启动了 默认占用的是 2379端口
-
安装并使用 JDHotKey
从 hotkey 官方仓库 下载源码后打开可以看到如下目录结构
首先打开 worker 模块可以看看 配置文件,更改一些配置避免冲突,我这改了一个端口 默认是8080 的我改为 8111,这里也能看到 work是通过 netty 去连接 etcd 并且 10s 上传一次心跳…… etcd 服务的地址 是 本地的 2379端口(所以要先启动 etcd 服务)
我改完的样子:
netty: port: ${nettyPort:11111} heartBeat: ${heartBeat:10} timeOut: ${timeOut:5000} local: address: ${localAddress:} #有些获取到的ip不能用,需要手工配worker的地址 open: timeout: ${openTimeOut:true} monitor: ${openMonitor:false} #开启持续无key发送监控,如果持续1分钟没发来key,就断开和etcd的连接,之后重建和客户端连接 thread: count: ${threadCount:0} caffeine: minutes: ${caffeineMinutes:1} disruptor: bufferSize: ${bufferSize:2} #必须是2的整数倍 #etcd的地址,如有多个用逗号分隔 etcd: server: ${etcdServer:http://127.0.0.1:2379} #etcd的地址,重要!!! workerPath: ${workerPath:default} #该worker放到哪个path下,譬如放/app1下,则该worker只能被app1使用,不会为其他client提供服务 server: port: 8111
-
在打开看一下 dashboard 的配置,可以看到这里有数据库配置,那我们就得按照自己数据库来改了 比如改 username password 自行配置
这是我改完的样子: 改了端口号为 8112 数据库相关配置 连接的库是 hotkey。
既然要连接数据库,那我们肯定要创建一个数据库并准备一些表,在 dashboard 模块的配置文件同级目录下就有一个 db.sql 文件 我们只要创建一个名为 hotkey 的库然后运行这个文件ok了
```java
server :
port : 8112
servlet :
context-path : /
spring :
resources:
static-locations: classpath:/resources,classpath:/static
profiles :
active : dev
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
mvc: #静态文件
static-path-pattern : /static/**
#模板引擎
thymeleaf:
model: HTML5
prefix: classpath:/templates/
suffix: .html
#指定编码
encoding: utf-8
#禁用缓存 默认false
cache: false
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
datasource:
username: ${MYSQL_USER:root}
password: ${MYSQL_PASS:123456}
url: jdbc:mysql://${MYSQL_HOST:localhost}:3306/hotkey?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC&useTimezone=true&serverTimezone=GMT
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
max-lifetime: 120000
idle-timeout: 60000
connection-timeout: 30000
maximum-pool-size: 32
minimum-idle: 10
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
#mybatis:
# configuration:
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
etcd:
server: ${etcdServer:http://127.0.0.1:2379}
```
好了整完这些配置我们就可以试着运行一下了。
先运行一下 dashboard 项目 如果没有报错就可以看到成功连接 etcd 信息,如果数据库、端口连接有问题就会报错

因为刚才我们为 dashboard 项目配置的端口是8112所以我们可以打开 http://localhost:8112/ 用户名是 admin 密码是 123456 (也在配置文件中配置的) 登录可以看到如下画面

我们点一下节点信息菜单会发现列表是空,那是因为我们没有启动 worker 项目,待会我们启动了worker 项目可以再回头看看

再来启动一下 worker 项目 我们可以看到 worker 在定时上传心跳 和 统计 那就说明启动成功了

这时可以回头 去看 节点信息是否多一列数据 为当前你启动的 worker 的信息。因为它俩连的etcd 是同一个服务当然可以互相发现。
两个项目都可以启动就就可以先停掉了,使用 maven 插件打包一下这整个项目

找到 client 项目的 with-dependdencies 这个 jar 包 这可是好东西

-
在 springboot 项目中使用
-
先将刚才我们得到的 jar包 复制到自己项目中,这里我放到 lib 目录下 改了个名其实就是那个 with-dependdencies的 jar 包
-
在pom.xml文件中引入这个包 (system 引入可能会造成一些依赖版本冲突问题,但是maven仓库的hotkey-client 又不是很好用………… )
<dependency> <artifactId>hotkey-client</artifactId> <groupId>com.jd.platform.hotkey</groupId> <version>0.0.4-SNAPSHOT</version> <scope>system</scope> <systemPath>${project.basedir}/lib/hotkey-client-0.0.4-SNAPSHOT.jar</systemPath> </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(); } }
接着我们就可以配置 在项目的配置文件 application.yml 中加入 (可以自行配置 这里假设我们的springboot 应用就叫 gg-video)
# 热 key 探测 hotkey: app-name: gg-video caffeine-size: 10000 # 缓存内存大小 push-period: 1000 # 每 1000 毫秒上报一次要统计的接口调用量 etcd-server: http://localhost:2379 # etcd 服务地址 要从这里知道到底那些接口需要统计上报
-
实际使用
可以在需要自动检测热点数据的接口 编写类似代码 只做演示就做简单点就行
public VideoVO getVideoVOById(Integer id) { String key = "video_" + id; // 如果是热 key if (JdHotKeyStore.isHotKey(key)){ // 从本地缓存中获取缓存值 Object cache = JdHotKeyStore.getValue(key); if (cache!=null){ // 如果缓存中有值,直接返回缓存的值 return (VideoVO) cache; } } // 查询数据库获取值 VideoVO vo = videoService.getById(id); // 设置本地缓存 (不是热key 不会设置成功的放心用) JdHotKeyStore.smartSet(key, vo); return vo; }
所以 JdHotKeyStore 是按照什么规则去判断它是不是 热key 呢? 当然是你指定的啦,你的规矩就是规矩!!!
启动 dashboard 项目并配置一下规则
-
先登录进去给这个项目添加一个负责人,添加负责人的时候顺便可以创建项目,可以看到 所属APP 要和我们自己写的配置文件中的 项目名称一致,并且添加的这个人角色可以是负责人。 账户 昵称 密码之类的可以随意。
-
编写规则,可以看到只有第四步需要我们编写代码,而且页面还很贴心的说明了每个字段的含义,理解起来so easy。
这是配置的格式,一个app内可以为很多接口配置规则,所以是一个数组你可以继续往下写
[ { "duration": 600, "key": "video_", "prefix": true, "interval": 5, "threshold": 10, "desc": "热门题库缓存" } ]
这个规则的意思是,判断
video_
开头的 key,如果 5 秒访问 10 次,就认为它是一个 热点数据 (注意我们并没有缓存的,只是认为这个数据访问量有些大比较热门,至于要不要缓存,或者说关闭访问都还是我们在自己写代码中来控制的,灵活性更高,例如你发现其实就是被某个用户恶意刷流量,爬虫之类的你干嘛要给他做缓存啊应该不给他刷 ) -
-
-
配置好规则 启动 Worker 启动 Dashboard 启动你自己项目
可以看到自己项目 也在想像etcd 上传心跳 ,并且你如果现在再更改或者添加规则,自己的项目的控制台也会输出你配置的规则,说明项目也在监听这个规则,配置规则不仅不会影响到当前项目的运行,而且还很快的被读到。
-
这时你可以模拟一些 5s 内访问数据10次的操作
public static void main(String[] args) { for (int i = 0; i < 10; i++) { getVideoVOById(1); } }
-
执行完之后你就可以到 Dashboard 中的热点数据看到 video_1 在列表中,说明认为这个是个热点数据,并且有存活时间,然后第11次执行getVideoVOById(1);若存活时间未到期则取的是缓存的数据。
-
-
简单疑问
这下你终于放心了,不用你自己设置热点数据,它自己就能按照你的规矩发现热点数据,再按照你的代码逻辑去处理这些热点数据。
不过你在想,我这么用的话缓存数据存在哪了?
JDHotKet框架 使用的缓存是Caffeine为缓存。存到JVM 的内存中了
你也可以不用 JdHotKeyStore.smartSet(key, vo); 来存,继续使用你的 redis 作为缓存。当然取数据也要从 redis 中去取
总之,JDHotKet 最大的帮助就是 帮你发现了那些数据是热点数据,至于你要对热点数据干什么 它可以毫不干涉。
-
热 key 会自动续期么?否则可能出现缓存雪崩的问题?
如果已经是热 key 则不会再 push 上报,离过期还有 2 秒内的时候,会再次 push上报,如果还是符合配置的规则,这样这个 key 可能被继续设置为热 key。 源码可以看到哦
-
哦哦哦 忘了介绍JdHotKeyStore有什么作用了
1)boolean isHotKey(String key)
该方法会返回该 key 是否是热 key,如果是返回 true,如果不是返回 false,并且会将 key 上报到探测集群进行数量计算。该方法通常用于判断只需要判断 key 是否热、不需要缓存 value 的场景,如刷子用户、接口访问频率等。
2)Object get(String key)
该方法返回该 key 本地缓存的 value 值,可用于判断是热 key 后,再去获取本地缓存的 value 值,通常用于 redis 热 key 缓存。
3)void smartSet(String key, Object value)
方法给热 key 赋值 value,如果是热 key,该方法才会赋值,非热 key,什么也不做
4)Object getValue(String key)
该方法是一个整合方法,相当于 isHotKey 和 get 两个方法的整合,该方法直接返回本地缓存的 value。 如果是热 key,则存在两种情况
- 是返回 value
- 是返回 null
返回 null 是因为尚未给它 set 真正的 value,返回非 null 说明已经调用过 set 方法了,本地缓存 value 有值了。 如果不是热 key,则返回 null,并且将 key 上报到探测集群进行数量探测。