如何在海量数据找到热点Key???这时候难免有人回答“这不简单,在同一秒内访问达到一定阈值的Key,这些就是热点Key,然后我们将这些Key对应的值缓存到像Redis这样的缓存中,下次访问的时候如果缓存命中直接从缓存获取不就可以了了么?”
话是这样说,但是这里有一个前提,就是身为程序员的你在编写代码的时候已经预知了哪些Key会是热点Key,所以才会放进缓存。这样的操作适合于双十一这样的高并发大场景,因为双十一的商品肯定会被大量用户访问,所以放进缓存可以减少大量的MySQL压力。而这种热点Key被称为静态热点Key,也就是可以提前预测到的热点数据。
但是如果有这样的一个场景,阁下该如何应对?
打个比如(不严谨),在疫情之前,口罩并不是必需品,所以在淘宝、京东这些网上购物平台口罩的销量并不算高。可是疫情的时候,由于口罩急缺,导致口罩突然称为了一个热门商品,但是程序员在编写代码的时候可能并没有将口罩作为热点商品放进缓存中,这时候因为口罩变成了热门商品,这样势必会给系统资源带来严峻的挑战。这种热点Key被称为动态热点Key,就是不能提前预测到的热点数据。
这时候可能会有人回答**“我手动把他放进缓存不就好了么”**,虽然确实可以,但是使用的是Redis,确实可以直接放进缓存就可以了,但是总不能24小时一直人工监测热点Key,如果某个商品在凌晨称为热点Key,难不成让人24小时值班?成本较大,不太合适。
如果用的缓存时JVM缓存,也就是本地缓存,那么就需要停机再重新上线,这期间会导致服务不可用,不仅仅用户体验感不好,还会导致销售额有所影响。
下面就来分析一下程序运行当中该如何发现并缓存这些动态热点Key
想要解决动态热点Key问题,最最最重要的一个关键点就是让程序运行的时候自己发现某个key在某个时间内是否超过阈值,如果有,则将其缓存,否则则不是热点Key。
当然这只是个简单的思路,热点Key的探测还需要有以下特性:
在不影响实时性的情况下,要完成实时热key探测,所消耗的机器资源越少,那么经济价值就越大。
下面给出两种解决方案:
其中滑动窗口属于一个比较简单的实现方案,可能不太符合上面所说的热点Key的探测特性,但是比较适合一些小系统、并发量不太、大不用太严谨的系统。
滑动窗口,其实就是限流算法中的滑动窗口。
在限流算法中,滑动窗口算法是指将固定时间进行划分,并且随着时间的流逝,进行移动,固定数量的可以移动的格子,进行计数并判断阀值,若超过阈值则进行限流操作。
于是,基于滑动窗口算法改动,我们可以将超过阈值进行限流操作改为超过阈值则将该Key认为是热点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,其他的逻辑交给你自己去实现即可。
该框架由四部分组成:
网上对于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)
第八步,配置规则
参数:
第九步,配置客户端
引入依赖
<dependency>
<artifactId>hotkey-clientartifactId>
<groupId>com.jd.platform.hotkeygroupId>
<version>0.0.4-SNAPSHOTversion>
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
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)