使用Redis神奇的HyperLogLog做UV统计

前言

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。

HyperLogLog基本命令

HyerLogLog总共就3个命令

基数定义

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5,我们可以理解为SET集合,只能存不重复的数据。

PFADD

添加指定基数 PFADD key element [element …]

   PFADD runoobkey "redis"

PFCOUNT

返回给定 HyperLogLog 的基数估算值。PFCOUNT key [key …]

 PFCOUNT runoobkey

PFMERGE

将多个 HyperLogLog 合并为一个 HyperLogLog PFMERGE destkey sourcekey [sourcekey …]

   PFADD hll1 foo bar zap a
   

JAVA操作

我们统计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(独立访客)介绍

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'

如果采用这样的时间统计,这个日志量将是非常巨大的,查询起来去重也是很慢的。

使用HyperLogLog设计

KEY的定义

我们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/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,具有高性能,占用内存小特点。

缺点及展望

  1. 用户每次访问都会调用Redis,但是有效UV只有一个,其它调用其实都是浪费的,可以在调用前再添加个Bloom过滤器,同一用户每天内调用过了就不要再去调用Redis添加基数了
  2. 无法做到任务时间颗粒度的UV统计,比如我们设计最小统计时间颗粒度为一天,就无法统计每小时的UV了,如果我们缩小时间颗粒度,比如每10分钟一个KEY,则随着时间变化Redis中KEY会很多。

  1. Redis HyperLogLog | 菜鸟教程 (runoob.com) ↩︎

  2. 布隆过滤器 | Hutool ↩︎

  3. 大数据量下的集合过滤—Bloom Filter - 欠扁的小篮子 - 博客园 (cnblogs.com) ↩︎

  4. 走近源码:神奇的HyperLogLog - 知乎 (zhihu.com) ↩︎

  5. 独立访客_百度百科 (baidu.com) ↩︎

  6. 日活(DAU)的原理、方法论和应用 - 知乎 (zhihu.com) ↩︎

你可能感兴趣的:(redis,uv,java)