热点数据发现

实验性质的项目,因为之前被问过两次,当时没有太好的思路,这里试想一种解决方案。


代码:hotkey

实际工作中很多场景会面对热数据问题,当QPS一高就不能完全以来数据库应付了。最容易想到的解决办法就是加缓存,但是如果数据非常多呢?难道全部都加缓存吗?
退一步讲,在所有的数据中:

  • 其实大部分都是不必要加载到缓存的,符合8-2规律;
  • 而且有的数据一会是热的,一会可能会变冷,比如因为流感季节到来,对口罩的搜索量忽然上升,之后随着流感季节过去又降回去;

所以我认为最根本的解决办法还是检测出哪些key是热的并加载到缓存,然后自动将不热的数据从缓存中淘汰掉。

测试

环境搭建

1
2
sudo docker run -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
sudo docker run --name myredis -d -p6379:6379 redis
  1. 对Redis、MySQL连接正常;
    服务启动成功。
  2. SQL执行正常
  3. Redis写入查询正常
    RedisTest#testSetGet

验证 - 上报

  1. 手动调hot-key服务的上报接口
    localhost:8081/hot-key/upload
    1
    2
    3
    4
    5
    6
    7
    8
    {
    "items": [{
    "key": "a",
    "count": 100
    }],
    "collectTime": "2021-01-19 10:50:00",
    "address": "http://127.0.0.1:8080"
    }
  2. 客户端上报热点数据
    HotKeyUploader
    客户端执行一段时间后自动触发上传
  3. 访问热点数据并上报
    通过接口/test/getset访问数据,这些数据会被加载到热点数据列表HotKeyStatistic
    之后等到HotKeyUploader执行时就会把数据上报到hot-key服务,再去表里查就能找到这条数据。

验证 - 热点数据汇总及通知

hot-key服务需要告知客户端哪些key成为了热key。

  1. 手动通知
    localhost:8080/hot-key/notify
    1
    2
    3
    4
    5
    6
    {
    "hotKeys": [
    "a",
    "b"
    ]
    }
  2. 自动通知
    hot-key服务定时任务HotKeyNotifyTask自动统计热点数据并通知相应的服务。
  3. 通知完毕后查看客户端是否能识别到热点数据
    localhost:8080/test/isHot?key=a

验证 - 10W级别数据量

插入10W条数据,其中数据的分布情况:
InitTest
key=1, address=127.0.0.1:8080, count=100000, rate=1000
key=2, address=127.0.0.1:8080, count=99999, rate=999
key=3, address=127.0.0.1:8080, count=99998, rate=998

客户端正常发现其中频率最高的。

热key检测功能的实现

需求

在现实中,数据库可能存在热点数据、分布式缓存也可能存在热点数据。

  • 数据库产生热点数据后,缓存到分布式缓存,且保证分布式缓存和数据库数据的一致性;
  • 分布式缓存产生热点数据后,缓存到本地缓存,且保证本地缓存和分布式缓存的一致性。

其他的非功能性需求,包括:

  • 热点数据的发现需要尽量v地快,及时响应;
  • 对代码侵入低。
  • 能够统计本地缓存、分布式缓存的命中率、热点key等数据,用于后续验证效果。

数据模型

这个场景非常简单,从数据库中查询一个key,我先列出DB和Redis中如何保存数据
DB:

1
2
3
4
5
6
create table data (
id int primary key,
key varchar(20) not null default '',
name varchar(20) not null default '',
uniq key uniq_key(key)
) default engine = InnoDB;

热点数据是有时效的,因此最好key里带上时间,时间的粒度也可以配置,比如将1秒钟拆成4个时段,统计时统计4个时段,这样就不会出现每一秒都重新计数的情况了。
Redis:
<HOT_毫秒_keyname, counter>

架构

热点数据发现

  • 热点统计和上报是异步执行的,因此不会阻塞业务。

热点统计

记录key被访问的频率,统计不能影响服务本身的执行效率,因此不能直接调其他服务。客户端可以每个key使用一个LongAdder来统计其访问频率
问题是服务挂掉的话,计数器会丢失,不过试想这台机器挂掉后,对该key的访问请求其实就都转移到其他机器上了,所以总数是不会丢失的,只是挂掉的那台机器在上次上报之后的数据会丢失,需要从头开始统计。

热点上报

热点数据的上报需要将数据上传到一个汇总服务器上,注意每次上传的是全量的统计数据,然后由hot-key服务来计算单位时间内的访问频。

上传数据的格式?
因此上传的数据格式可以如下:

1
2
3
4
5
{
"key": "Mike",
"count": 100,
"ip": "123.123.123.123"
}

每次要把全量的数据都上报吗?
我们现在线上的Redis平时保持有百万量级的key,如果要把所有这些key的访问数据都上报,每次传输的数据量非常大,而且也没有必要,因为大部分key都不是热key。
因此本地统计数据的时候,用一个PriorityBlockingQueue记录使用频率最高的key。

hot-key服务如何保存数据?

  • 因为统计的频率不能太低,不然热key不能被及时检测出来,比如3秒上报一次,这样一天单机就需要上报28800次,假设我们的网站有10个服务,每个服务又有5个副本,一天的数据量级就在100W左右。
  • 可以保存到数据库中,虽然一天100W挺多的,但是热点统计数据是有时效性的,完全可以每隔1小时将1小时之前的数据清掉。
    判断一个key是否是热key时需要将每台服务器的统计数据都从数据库拿出来然后计算频率。
  • 可以保存到Redis中,比如每个key存一个hash格式的数据来保存每个服务器的统计数据。
    优点:相对数据库来说查得快些。
    缺点:Redis只是缓存,不能持久化数据(有持久化功能,不过那主要用于备份),不便后续的数据分析。

综上所述,我还是选择了数据库来持久化访问频率数据。

1
2
3
4
5
6
7
8
9
10
CREATE TABLE hot_key (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
h_key VARCHAR(50) NOT NULL DEFAULT '' COMMENT 'key',
h_count BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '计数',
collect_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '采集时间',
address VARCHAR(100) NOT NULL DEFAULT '' COMMENT '地址',
rate DECIMAL(10 , 2 ) NOT NULL DEFAULT 0 COMMENT '频率',
UNIQUE KEY idx_key (h_key , address),
KEY idx_time (collect_time)
) ENGINE=INNODB;

热点探测

热点数据上报后由hot-key服务来保存热点数据,之后定时将数据推给其他服务。

应该推给哪些服务?
上报时客户端会告诉hot-key服务自己的IP地址,之后hot-key再根据IP将热key列表推给客户端。

怎么样才算热点数据?

  1. 从数据库中查找最后一次统计频率大于阈值的;
  2. 根据IP分组;
  3. 分IP推送。

客户端如何保存热点数据?
只需要知道哪些key属于热点数据即可,可以用一个Set存key的集合。

本地缓存

通过热点探测知道哪些key属于热点后,客户端在下次访问数据时,会将这些热点数据保存到本地缓存。

关于热key的其他讨论

缓存穿透

缓存穿透指的是一个热key过期后大量请求回源导致数据库被打爆的情况。

参考

  1. 有赞透明多级缓存解决方案(TMC)