hotkey的学习
五、自动缓存热点
1、需求分析
- 能自动的发现并缓存热点,也可管理员设置
- 例如:
- 高频面试题
- 给经常访问的题库加缓存:当一个题库5秒内访问>=10次,加缓存。
- 例如:
- 遇到攻击自动发现并进行封禁
2、方案设计
- 自动缓存热门题库需要以下五个步骤:
1、记录访问:用户每访问一次题库,统计次数+1
2、访问统计:统计一段时间内题库的访问次数。这是最难实现的一部分。
3、阈值判断:访问频率超过一定的阈值,变为热点数据。
4、缓存数据:缓存热点数据
5、获取数据:后续访问时,从缓存中获取数据 - 自己实现,太难了,我们可以使用热点key的框架:hotkey
3、hotkey
京东的热点探测开源框架:https://gitee.com/jd-platform-opensource/hotkey
4、核心组件
Etcd 集群
- Etcd 作为一个高性能的配置中心,可以以极小的资源占用,提供高效的监听订阅服务。主要用于存放规则配置,各worker的 ip 地址,以及探测出的热 key、手工添加的热 key 等。
client端jar包
- 判断那些key是热key,对热key进行caffeine本地缓存
worker
- worker 端是一个独立部署的 Java 程序,启动后会连接 Etcd,并定期上报自己的ip 信息,供 client 端获取地址并进行长连
接。之后,主要就是对各个 client 发来的待测 key 进行 累加计算,当达到 Etcd 里设定的 rule 阈值后,将热 kev 推送到各
个 client.
dashboard
- 控制台是一个带可视化界面的 Java 程序,也是连接到 Etcd,之后在控制台设置各个 APP 的 key 规则,譬如 2 秒出现 20
次算热 key。然后当 worker 探测出来热 key 后,会将 key 发往 etcd,dashboard 也会监听热 key 信息,进行入库保存记
录。同时,dashboard 也可以手工添加、删除热 key,供各个 client 端监听。
Etcd安装
- etcd:etcd 服务本身
- etcdctl:客户端,用于操作 etcd,比如读写数据
- etcdutl:备份恢复工具
- 输入etcd启动
- 2379:提供 HTTP API服务,和 etcdctl 交互
- 2380:集群中节点间通讯
安装配置:hotkey
- 将git源码下载下来:https://gitee.com/jd-platform-opensource/hotkey
- 然后用idea打开
- 先启动worker—>dashboard
启动worker
- yml文件设置端口号为:8111
- 启动
启动dashboard
-
因为dashboard依赖于数据库
- 先导入数据库数据
- 修改数据库密码,端口好
-
启动
- 账号:admin
- 密码:123456
-
添加规则
- 意思是:interval-2秒内出现了threshold-10次就认为它是热key,它就会被推送到jvm内存中,并缓存60秒,prefix-true代表前缀匹配。那么在应用中,就可以把一组key,都用as__开头,用来探测。
引入客户端
-
将打包好的客户端jar包放入主项目的lib里面
-
pom.xml文件
-
<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>
-
-
编写配置信息
-
yml文件
-
# 热 key 探测 hotkey: app-name: lishuati caffeine-size: 10000 push-period: 1000 etcd-server: http://localhost:2379
-
-
配置文件
-
@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(); } }
-
-
最佳实践
-
主要有如下4个方法可供使用
boolean JdHotKeyStore.isHotKey(String key)
Object JdHotKeyStore.get(String key)
void JdHotKeyStore.smartSet(String key, Object value)
Object JdHotKeyStore.getValue(String key)
-
boolean isHotKey(String key) ,
- 该方法会返回该key是否是热key,如果是返回true,如果不是返回false,并且会将key上报到探测集群进行数量计算。该方法通常用于判断只需要判断key是否热、不需要缓存value的场景,如刷子用户、接口访问频率等。
-
Object get(String key),
- 该方法返回该key本地缓存的value值,可用于判断是热key后,再去获取本地缓存的value值,通常用于redis热key缓存
-
void smartSet(String key, Object value),
- 方法给热key赋值value,如果是热key,该方法才会赋值,非热key,什么也不做
-
Object getValue(String key),
- 该方法是一个整合方法,相当于isHotKey和get两个方法的整合,该方法直接返回本地缓存的value。
- 如果是热key,
- 则存在两种情况,1是返回value,2是返回null。返回null是因为尚未给它set真正的value,返回非null说明已经调用过set方法了,本地缓存value有值了。
- 如果不是热key,则返回null,并且将key上报到探测集群进行数量探测。
- 如果是热key,
- 该方法是一个整合方法,相当于isHotKey和get两个方法的整合,该方法直接返回本地缓存的value。
-
-
最佳实践:
- 判断用户是否是刷子
if (JdHotKeyStore.isHotKey(“pin__” + thePin)) { //限流他,do your job }
- 判断商品id是否是热点
Object skuInfo = JdHotKeyStore.getValue("skuId__" + skuId); if(skuInfo == null) { JdHotKeyStore.smartSet("skuId__" + skuId, theSkuInfo); } else { //使用缓存好的value即可 }
- 或者这样:
if (JdHotKeyStore.isHotKey(key)) { //注意是get,不是getValue。getValue会获取并上报,get是纯粹的本地获取 Object skuInfo = JdHotKeyStore.get("skuId__" + skuId); if(skuInfo == null) { JdHotKeyStore.smartSet("skuId__" + skuId, theSkuInfo); } else { //使用缓存好的value即可 } }
改造项目
-
@GetMapping("/get/vo") public BaseResponse<QuestionBankVO> getQuestionBankVOById(QuestionBankQueryRequest questionBankQueryRequest, HttpServletRequest request) { ThrowUtils.throwIf(questionBankQueryRequest == null, ErrorCode.PARAMS_ERROR); Long id = questionBankQueryRequest.getId(); ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR); //生成热key String key = HotKeyConstant.HOT_KEY_PREFIX + id; //判断是否存在缓存 if (JdHotKeyStore.isHotKey(key)) { //注意是get,不是getValue。getValue会获取并上报,get是纯粹的本地获取 Object cacheQuestionBank = JdHotKeyStore.get(key); //如果缓存中存在,则直接返回 if(cacheQuestionBank != null) { return ResultUtils.success((QuestionBankVO) cacheQuestionBank); } } // 查询数据库 // 写入缓存,如果不是热key,则不写入缓存,是热key,则写入缓存 JdHotKeyStore.smartSet(key, questionBankVO); // 获取封装类 return ResultUtils.success(questionBankVO); }
-
改造完后,速度直线上升
-
3、热 key 会自动续期么?否则可能出现缓存雪崩的问题?
- 可以先自己测试一下。比如针对某个热点 key 再多次发送请求查询缓存,发现在热 key 生效(缓存生效)期间,如果该key 仍然被不断访问,并不会刷新缓存时间,直到过期。
- 然后看下源码,就知道为什么了。源码中的逻辑是,如果已经是热 key 则不会再 push,离过期还有2秒内的时候,会再
次 push,这样这个 key 可能被继续设置为热 key。