Redis在 2.8.9 版本添加了 HyperLogLog结构,Redis HyperLogLog 是用来做基数统计的算法,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数1,这个特性让我想到了布隆过滤器2。关于Bloom为什么能用很小的内存判断元素存不存可以参考《大数据量下的集合过滤3,关于Redis的HyperLogLog为什么可以用12K内存就可以计算接近2^64个基数可以参考《走近源码:神奇的HyperLogLog》4,本文不研究其原理,我们需要知道的是HyperLogLog使用很小的内存就能计算出很多元素不重复的数量,比我们用SET去做可以大大的节省内存空间,利用此原理我们可以用很小的内存来统计UV/DAU/MAU。
HyerLogLog总共就3个命令
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5,我们可以理解为SET集合,只能存不重复的数据。
添加指定基数 PFADD key element [element …]
PFADD runoobkey "redis"
返回给定 HyperLogLog 的基数估算值。PFCOUNT key [key …]
PFCOUNT runoobkey
将多个 HyperLogLog 合并为一个 HyperLogLog PFMERGE destkey sourcekey [sourcekey …]
PFADD hll1 foo bar zap a
我们统计UV/DAU/MAU只需要用到PFADD和PFCOUNT,我们对Jedis做个简单的封装:
public class HyperLogService extends AbstractRedisService implements IHyperlogServcie {
@Override
public Long set(String key, String value) {
try (Jedis jedis = getResource()) {
return jedis.pfadd(key, value);
}
}
@Override
public Long count(List keys) {
if (CollectionUtils.isEmpty(keys)) {
return 0L;
} else {
try (Jedis jedis = getResource()) {
return jedis.pfcount(keys.toArray(new String[keys.size()]));
}
}
}
}
UV是指不同的、通过互联网访问、浏览一个网页的自然人5。这里要限定一段时间,一天内活跃的用户量为DAU(Daily Active User,日活跃用户数,简称日活),一个月内活跃的用户量为MAU:monthly active user(月活跃用户)6,这里关键指标是自然人,比如张三上午访问了你的网站,下午又访问了你的网站,DAU只能增加1,这个月的MAU也只能增加1。假如我们的自然人可以用一个唯一USER_ID表示,我们可以把UV/DAU/MAU统计建立一个简单的访问日志表:
access_at | USER_ID |
---|---|
2023-05-23 17:00:00 | uVNL1cvgsWDzFkUn |
2023-05-23 17:00:01 | uVNL1cvgsWDzFkUn |
2023-05-23 17:00:03 | RGwedXxYpZHf1Jbc |
2023-05-24 17:00:03 | RGwedXxYpZHf1Jbc |
2023-05-25 17:00:03 | RGwedXxYpZHf1Jbc |
统计2023年05月23号的DAU为:
select DISTINCT USER_ID from log where access_at>='2023-05-23 00:00:00' and access_at<'2023-05-24 00:00:00'
统计2023年05月的MAU为:
select DISTINCT USER_ID from log where access_at>='2023-05-01 00:00:00' and access_at<'2023-06-01 00:00:00'
如果采用这样的时间统计,这个日志量将是非常巨大的,查询起来去重也是很慢的。
我们KEY可以定义成UV统计最小时间颗粒度:
比如我需要统计的最小颗粒度为每个小时内的UV则KEY可以定义成 :
SITEID:yyyyMMddHH 如SITEID:2023052301
如果我们要统计的最小颗粒度为每天的UV则KEY可以定义成 :
SITEID:yyyyMMdd 如SITEID:20230523
假设我们的网站或者APP已经做好了埋点,当用户访问时,会调用一次接口,并把用户唯一USER_ID传过来,如果我们是按天来统计,则统计的接口可以定义为:
public ResponseEntity pv(@RequestParam String userId) {
String uvKey = String.format("APP_1:%s", DateUtil.today());
hyperlogServcie.set(uvKey, userId);
renderOk();
}
有了统计数据后,我们查询DAU/MAU就方便了
查询指定天的DAU:
/**
*
* @param day yyyyMMdd
* @return
*/
public ResponseEntity<ApiResult> dau(String day ) {
String uvKey = String.format("APP_1:%s", day);
renderOk(hyperlogServcie.count(Arrays.asList(uvKey)));
}
查询某月的MAU:
/**
*
* @param month yyyyMM
* @return
*/
public ResponseEntity<ApiResult> dau(String month) {
//从当前月的第一天,到最后一天所有KEY,每天按String.format("APP_1:%s", day)格式
List<String> monthKeys=DateUtil.getMonthDayKeys(month);
renderOk(hyperlogServcie.count(Arrays.asList(monthKeys)));
}
本文介绍Redis神奇的数据结构HyperLogLog,它可以用很小的内存,统计很多数据的基数(不重复的元素个数),并利用此特性来统计UV/DAU/MAU,具有高性能,占用内存小特点。
Redis HyperLogLog | 菜鸟教程 (runoob.com) ↩︎
布隆过滤器 | Hutool ↩︎
大数据量下的集合过滤—Bloom Filter - 欠扁的小篮子 - 博客园 (cnblogs.com) ↩︎
走近源码:神奇的HyperLogLog - 知乎 (zhihu.com) ↩︎
独立访客_百度百科 (baidu.com) ↩︎
日活(DAU)的原理、方法论和应用 - 知乎 (zhihu.com) ↩︎