文章目录
- 1. 概述
- 2. 热点Key问题分析
- 3. 解决方案
- 3.2 滑动窗口
- 3.2 京东毫秒级热Key探测框架
- 3.2.1 JdHotKey使用教程
- 3.2.2 JdHotKey常用API
1. 概述
如何在海量数据找到热点Key???这时候难免有人回答“这不简单,在同一秒内访问达到一定阈值的Key,这些就是热点Key,然后我们将这些Key对应的值缓存到像Redis这样的缓存中,下次访问的时候如果缓存命中直接从缓存获取不就可以了了么?”
话是这样说,但是这里有一个前提,就是身为程序员的你在编写代码的时候已经预知了哪些Key会是热点Key,所以才会放进缓存。这样的操作适合于双十一这样的高并发大场景,因为双十一的商品肯定会被大量用户访问,所以放进缓存可以减少大量的MySQL压力。而这种热点Key被称为静态热点Key,也就是可以提前预测到的热点数据。
但是如果有这样的一个场景,阁下该如何应对?
打个比如(不严谨),在疫情之前,口罩并不是必需品,所以在淘宝、京东这些网上购物平台口罩的销量并不算高。可是疫情的时候,由于口罩急缺,导致口罩突然称为了一个热门商品,但是程序员在编写代码的时候可能并没有将口罩作为热点商品放进缓存中,这时候因为口罩变成了热门商品,这样势必会给系统资源带来严峻的挑战。这种热点Key被称为动态热点Key,就是不能提前预测到的热点数据。
这时候可能会有人回答**“我手动把他放进缓存不就好了么”**,虽然确实可以,但是使用的是Redis,确实可以直接放进缓存就可以了,但是总不能24小时一直人工监测热点Key,如果某个商品在凌晨称为热点Key,难不成让人24小时值班?成本较大,不太合适。
如果用的缓存时JVM缓存,也就是本地缓存,那么就需要停机再重新上线,这期间会导致服务不可用,不仅仅用户体验感不好,还会导致销售额有所影响。
下面就来分析一下程序运行当中该如何发现并缓存这些动态热点Key
2. 热点Key问题分析
想要解决动态热点Key问题,最最最重要的一个关键点就是让程序运行的时候自己发现某个key在某个时间内是否超过阈值,如果有,则将其缓存,否则则不是热点Key。
当然这只是个简单的思路,热点Key的探测还需要有以下特性:
- 实时性
- 因为Key往往时突发性瞬间就热了,根本不会给人工去添加Key到缓存的机会,其次热的时间不确定,可能一热起来就把集群打爆了。
- 所以热Key,需要在Key刚开始有苗头的时候,它就已经被缓存了。
- 准确性
- 累加数量,做到不误探,精准探测,保证探测出的热key是完全符合用户自己设定的阈值。
- 集群一致性
- 这个比较重要,尤其是某些带删除key的场景,要能做到删key时整个集群内的该key都会删掉,以避免数据的错误。
- 高性能
- 这个是核心之一,高性能带来的就是低成本
- 做热key探测目的就是为了降低数据层的负载,提升应用层的性能,节省服务器资源。
在不影响实时性的情况下,要完成实时热key探测,所消耗的机器资源越少,那么经济价值就越大。
3. 解决方案
下面给出两种解决方案:
- 滑动窗口
- 京东毫秒级热Key探测框架
其中滑动窗口属于一个比较简单的实现方案,可能不太符合上面所说的热点Key的探测特性,但是比较适合一些小系统、并发量不太、大不用太严谨的系统。
3.2 滑动窗口
滑动窗口,其实就是限流算法中的滑动窗口。
在限流算法中,滑动窗口算法是指将固定时间进行划分,并且随着时间的流逝,进行移动,固定数量的可以移动的格子,进行计数并判断阀值,若超过阈值则进行限流操作。
于是,基于滑动窗口算法改动,我们可以将超过阈值进行限流操作改为超过阈值则将该Key认为是热点Key,然后将其缓存。
这就是滑动窗口的具体思路,代码可自行实现。
3.2 京东毫秒级热Key探测框架
京东根据多次被突发海量请求压垮数据层服务的场景,并时刻面临大量的爬虫刷子机器人用户的请求的经验,设计开发了一套通用轻量级热key探测框架——[JdHotkey](hotkey: 京东App后台中间件,毫秒级探测热点数据,毫秒级推送至服务器集群内存,大幅降低热key对数据层查询压力 (gitee.com))。这个框架历经多次高压压测和2020年京东618、双11大促考验。
JdHotKey
可以对任意突发性的无法预先感知的热点数据,包括并不限于热点数据(如突发大量请求同一个商品)、热用户(如恶意爬虫刷子)、热接口(突发海量请求同一个接口)等,进行毫秒级精准探测到。然后对这些热数据、热用户等,推送到所有服务端JVM内存中,以大幅减轻对后端数据存储层的冲击,并可以由使用者决定如何分配、使用这些热key(譬如对热商品做本地缓存、对热用户进行拒绝访问、对热接口进行熔断或返回默认值)。这些热数据在整个服务端集群内保持一致性,并且业务隔离,worker端性能强悍。
它很轻量级,既不改redis
源码也不改redis
的客户端jar包,当然,它与redis
没一点关系,完全不依赖redis
。它是一个独立的系统,部署后,在server代码里引入jar,之后就像使用一个本地的HashMap一样来使用它即可。
框架自身会完成一切,包括对待测key的上报,对热key的推送,本地热key的缓存,过期、淘汰策略等等。框架会告诉你,它是不是个热key,其他的逻辑交给你自己去实现即可。
该框架由四部分组成:
- etcd集群
- etcd作为一个高性能的配置中心,可以以极小的资源占用,提供高效的监听订阅服务。主要用于存放规则配置,各worker的ip地址,以及探测出的热key、手工添加的热key等。
- client端jar包
- 就是在服务中添加的引用jar,引入后,就可以以便捷的方式去判断某key是否热key。
- 该jar完成了key上报、监听etcd里的rule变化、worker信息变化、热key变化,对热key进行本地caffeine缓存等。
- worker端集群
- worker端是一个独立部署的Java程序,启动后会连接etcd,并定期上报自己的ip信息,供client端获取地址并进行长连接。
- 之后,主要就是对各个client发来的待测key进行累加计算,当达到etcd里设定的rule阈值后,将热key推送到各个client。
- dashboard控制台
- 控制台是一个带可视化界面的Java程序,也是连接到etcd,之后在控制台设置各个APP的key规则,譬如2秒出现20次算热key。
- 然后当worker探测出来热key后,会将key发往etcd,dashboard也会监听热key信息,进行入库保存记录。
- 同时,dashboard也可以手工添加、删除热key,供各个client端监听。
网上对于JdHotKey
的使用教程比较少,下面介绍以下如何使用该框架。
3.2.1 JdHotKey使用教程
第一步,安装etcd
在etcd下载页面下载对应操作系统的etcd,下载地址,使用3.4.x以上。
我这里下载的是Window版本。
第二步,将其解压,并启动etcd.exe
第三步,拷贝代码
前往hotkey: 京东App后台中间件,毫秒级探测热点数据,毫秒级推送至服务器集群内存,大幅降低热key对数据层查询压力 (gitee.com)将代码拉取下来。
第四步,打开项目,运行SQL
在这个目录下会有一个SQL,需要创建一个hotkey_db
的数据库
第五步,启动worker
可以将将worker打包为jar,也可以直接在代码中启动(必须先启动etcd,否则报错)
第六步,启动控制台dashboard
记得记得在yml中修改自己的MySQL地址和用户名和密码
第七步,进入dashboard控制台
访问地址:IP:8081
因为我是本地启动,所以是127.0.0.1:8081
登录的账号密码默认admin 123456
在用户管理添加用户(重要的是所属APP)
第八步,配置规则
参数:
- key-(*代表任意以key为前缀),
- prefix-是否前缀,
- interval-间隔时间(秒),
- threshold-阈值,
- duration-缓存时间(秒),默认60
第九步,配置客户端
引入依赖
<dependency>
<artifactId>hotkey-client</artifactId>
<groupId>com.jd.platform.hotkey</groupId>
<version>0.0.4-SNAPSHOT</version>
</dependency>
配置
@PostConstruct
public void initHotkey() {
ClientStarter.Builder builder = new ClientStarter.Builder();
// 注意,setAppName很重要,它和dashboard中相关规则是关联的。
ClientStarter starter = builder.setAppName("sample")
.setEtcdServer("http://127.0.0.1:2379")
.setCaffeineSize(10)
.build();
starter.startPipeline();
}
编写测试接口
@RequestMapping("/get/{key}")
public Object get(@PathVariable String key) {
String cacheKey = "skuId__" + key;
if (JdHotKeyStore.isHotKey(cacheKey)) {
log.info("hotkey:{}", cacheKey);
//注意是get,不是getValue。getValue会获取并上报,get是纯粹的本地获取
Object skuInfo = JdHotKeyStore.get(cacheKey);
if (skuInfo == null) {
Object theSkuInfo = "123" + "[" + key + "]" + key;
JdHotKeyStore.smartSet(cacheKey, theSkuInfo);
return theSkuInfo;
} else {
//使用缓存好的value即可
return skuInfo;
}
} else {
log.info("not hot:{}", cacheKey);
return "123" + "[" + key + "]" + key;
}
}
第十步,测试
在一秒钟内请求几次,因为我设置了2s内的阈值是3次,所以肯定会触发热Key的阈值。
由打印的日志可以看见,一开始key还不是热key,当触发阈值之后就成为了热Key
3.2.2 JdHotKey常用API
boolean JdHotKeyStore.isHotKey(String key)
:该方法会返回该key是否是热key,如果是返回true,如果不是返回false,并且会将key上报到探测集群进行数量计算。该方法通常用于判断只需要判断key是否热、不需要缓存value的场景,如刷子用户、接口访问频率等。Object JdHotKeyStore.get(String key)
:该方法返回该key本地缓存的value值,可用于判断是热key后,再去获取本地缓存的value值,通常用于redis热key缓存void JdHotKeyStore.smartSet(String key, Object value)
:方法给热key赋值value,如果是热key,该方法才会赋值,非热key,什么也不做。Object JdHotKeyStore.getValue(String key)
:该方法是一个整合方法,相当于isHotKey和get两个方法的整合,该方法直接返回本地缓存的value。 如果是热key,则存在两种情况,1是返回value,2是返回null。返回null是因为尚未给它set真正的value,返回非null说明已经调用过set方法了,本地缓存value有值了。 如果不是热key,则返回null,并且将key上报到探测集群进行数量探测。
最佳实践:
1 判断用户是否是刷子
if (JdHotKeyStore.isHotKey(“pin__” + thePin)) {
//限流他,do your job
}
2 判断商品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即可
}
}
参考:
- 京东毫秒级热key探测框架设计与实践,已实战于618大促 (qq.com)
- hotkey: 京东App后台中间件,毫秒级探测热点数据,毫秒级推送至服务器集群内存,大幅降低热key对数据层查询压力 (gitee.com)