希音面试:亿级用户 日活 月活,如何统计?(史上最强 HyperLogLog 解读)

本文原文链接

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:

  • 如何 统计一个 网站 的日活、月活数? 亿级用户的日活、月活,如何统计
  • Bitmaps可用于统计日活吗?
  • 亿级用户的日活、月活,Bitmaps可以统计吗?
  • 如何 统计一个页面的每天被多少个不同账户访问量(Unique Visitor,UV))?
  • 如何 统计用户每天搜索不同词条的个数?
  • 如何 统计注册 IP 数?

最近有小伙伴在面试 希音,又遇到了相关的面试题。小伙伴懵了,因为没有遇到过,所以支支吾吾的说了几句,面试官不满意,面试挂了。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书

首先回顾一下:redis 四大统计(基数统计,二值统计,排序统计、聚合统计)的原理 和应用场景

尼恩这边的文章都有一个 基本的规则,从最为基础的讲起。

先看看 常见的四大统计。

第1大统计:基数统计

基数(Cardinality)是指一个集合中不同元素的数量, 或者说统计一个集合中不重复元素的个数。

简单来说: 基数(Cardinality)就是去除重复后的数的个数

例如,对于一个包含重复元素的集合{1, 2, 2, 3, 4, 4, 4},其基数为4,即不同元素的个数。

在Redis中,HashSet /bitmap/ HyperLogLog数据结构都能提供 高效的基数统计,而HyperLogLog算法可以在不保存原始数据的情况下快速计算出一个集合的基数。

第2大统计:二值统计

二值统计通常涉及到将数据分为两个类别或状态,比如成功与失败、是与非等,并对这些类别进行计数和分析。

这种统计方法在处理二分类问题时非常常见,比如在质量控制、用户行为分析等领域。

亿级海量数据查重,如何实现 ? 涉及的是四大统计其中的 二值统计

亿级海量数据黑名单 ,如何存储?涉及的也是四大统计其中的 二值统计

京东面试:亿级 数据查重,亿级 数据黑名单 ,如何实现?(此文介绍了布隆过滤器、布谷鸟过滤器)

第3大统计:排序统计

排序统计涉及将数据按照一定的顺序(如升序或降序)进行排列,以便于分析和比较。

排序统计的例子,比如,可以使用ZSET对排序统计

  1. 可以 使用ZSET对文章的点赞数排序并分页展示
  2. 对评论根据时间进行排序

排序算法如快速排序、归并排序、堆排序等,都是排序统计中常用的方法。

第4大统计:聚合统计

聚合统计是一种数据处理技术,它将多个数据记录组合成一个集合,并计算该集合的统计信息,如总和、平均值、最大值和最小值等。

聚合操作通常用于数据仓库和数据分析中,以简化数据并提取有用的信息。

聚合统计的核心在于对数据进行分组(grouping),然后对每个组应用聚合函数(如sum, avg, max, min等)来计算统计值。

日活月活,属于 基数统计的类型

在数学上,基数或势,即集合中包含的元素的“个数”(参见势的比较),是日常交流中基数的概念在数学上的精确化(并使之不再受限于有限情形)。

有限集合的基数,其意义与日常用语中的“基数”相同,例如{ {a,b,c}的基数是3。

无限集合的基数,其意义在于比较两个集的大小,例如整数集和有理数集的基数相同;整数集的基数比实数集的小。

日活 、月活名词解释

  • DAU日活:(Daily Active User)日活跃用户数量
    常用于反映网站、互联网应用或网络游戏的运营情况。DAU通常统计一日(统计日)之内,登录或使用了某个产品的用户数(去除重复登录的用户);
  • MAU 月活: 月活跃用户数量(Monthly Active User,MAU)
    月活跃用户数量通常统计一个月(统计月)之内,登录或使用了某个产品的用户数(去除重复登录的用户);
  • PV(Page View):页面浏览量 指网站页面被访问的总次数。用户每打开一个页面就会被记录一次 PV,多次打开同一页面则累计计数。例如,一个用户在一天内访问了网站的首页 5 次,那么这个首页在这一天的 PV 就是 5。
  • UV(Unique Visitor) - 独立访客: 指在一定时间范围内访问网站的不同用户的数量。同一用户无论访问网站多少次,在统计周期内只计算一次 UV。例如,在一天内,用户 A 访问了网站 10 次,用户 B 访问了网站 3 次,那么这一天的 UV 就是 2。注意,DAU 是 UV 的一个子集。因为 DAU 强调的是在一天内有活跃行为的用户,而 UV 只是统计了不同的访问者,这些访问者不一定在当天有实际的活跃行为。

Note:日活、月活反映用户的活跃度,但是无法反映用户的粘性。

日活、PV、UV数据统计的特点

  • 数据需要去重;
  • 数据允许有一定的偏差,101W和102W差距不大;
  • 占用空间尽可能小;

HyperLogLog是Redis的高级数据结构,是统计基数(如日活、PV、UV)的利器。

在介绍HyperLogLog的原理之前,请你先来思考一下,如果让你来统计基数,你会用什么方法。

使用 redis Set 进行基数统计

使用 redis Set 进行基数统计的方法是:只需要把数据都存入Set,然后用scard命令就可以得到结果,这是一种思路,但是存在一定的问题。

import redis.clients.jedis.Jedis;

public class RedisSetCardinalityDemo {

    public static void main(String[] args) {
        // 连接到Redis服务器
        Jedis jedis = new Jedis("localhost", 6379);

        // 假设要统计用户的访问次数,这里模拟添加用户ID到Set中
        String setKey = "visited_users";

        // 添加一些用户ID到Set中
        jedis.sadd(setKey, "user1", "user2", "user2", "user3", "user4", "user4", "user4");

        // 获取Set的基数,即不同元素的数量
        long cardinality = jedis.scard(setKey);

        System.out.println("基数统计结果:不同元素的数量为 " + cardinality);

        // 关闭Redis连接
        jedis.close();
    }
}

在上述代码中:

  1. 定义了一个 Set 的键名visited_users,用于模拟统计访问过的用户情况。
  2. 使用sadd方法向 Set 中添加了多个用户 ID,注意其中有重复的 ID,这正是要通过基数统计来确定不同用户数量的场景。
  3. 最后通过scard方法获取 Set 的基数,也就是不同元素(这里即不同用户)的数量,并将结果输出。

通过这样的方式,就可以利用 Redis Set 数据结构简单地实现基数统计功能。

使用 redis Set 进行基数统计的问题:

如果数据量非常大,那么将会耗费很大的内存空间,尤其是如果日活一个亿,那么这个是一个超级大的set(100M * 4), 是一个超级大的bigkey。

如果这些数据仅仅是用来统计基数,那么无疑是造成了巨大的浪费,并且也是非常的低性能,因此是,我们需要找到一种占用内存较小的方法。

使用 redis bitmap 进行基数统计

bitmap同样是一种可以统计基数的方法,可以理解为用bit数组存储元素,例如01101001,表示的是[1,2,4,8],bitmap中1的个数就是基数。

以下是一个使用 Redis 的 Bitmap 进行日活统计的 Java 示例代码:

import redis.clients.jedis.Jedis;

public class RedisBitmapDailyActiveUsersDemo {

    public static void main(String[] args) {
        // 连接到Redis服务器
        Jedis jedis = new Jedis("localhost", 6379);

        // 定义存储日活用户的Bitmap键名,格式可以是 "daily_active_users:日期",这里假设日期为20241112
        String bitmapKey = "daily_active_users:20241112";

        // 模拟用户ID,这里简单用整数表示,实际应用中可能是更复杂的用户唯一标识
        int[] userIds = {1001, 1002, 1003, 1004, 1005};

        // 将每个用户ID对应的位设置为1,表示该用户当天活跃
        for (int userId : userIds) {
            // 使用SETBIT命令,将用户ID对应的位设置为1
            jedis.setbit(bitmapKey, userId, true);
        }

        // 统计日活用户数量,使用BITCOUNT命令
        long dailyActiveUsersCount = jedis.bitcount(bitmapKey);

        System.out.println("日活用户数量为: " + dailyActiveUsersCount);

        // 关闭Redis连接
        jedis.close();
    }
}

在上述代码中:

  1. 定义了一个用于存储日活用户的 Bitmap 键名,这里按照daily_active_users:日期的格式进行定义,示例中假设日期为20241112
  2. 模拟了一些用户 ID,这里简单用整数表示,实际情况中应该是能唯一标识用户的信息。
  3. 然后通过循环,使用SETBIT命令,将每个用户 ID 对应的位在 Bitmap 中设置为 1,表示该用户当天活跃。
  4. 最后使用BITCOUNT命令,统计 Bitmap 中值为 1 的位的数量,也就是日活用户的数量,并将结果输出。

通过这样的方式,就可以利用 Redis 的 Bitmap 数据结构方便地实现日活统计功能。

在实际应用中,可以根据具体需求进一步扩展和优化代码,比如从真实的用户访问日志中提取用户 ID,或者结合其他数据进行更深入的分析等。

bitmap也可以轻松合并多个集合,只需要将多个数组进行异或操作就可以了。

Bitmaps可用于统计日活吗?

Bitmaps 在大数据下的应用,那么Bitmaps可以用于统计日活数据吗?我们来做个计算分析(以一亿用户为例):

统计方式 占用计算 1亿用户占用空间(M)
Redis 32bit的string数据类型 1个string大概所需存储空间为4字节,可存储32 bit位 10^8 / (1024 * 1024 * 8 / 32) ≈ 381 M
Redis Bitmaps Bitmaps单个支持512M,不像int单个仅存储32位 10^8 / (1024 * 1024 * 8) ≈ 12M

bitmap相比于Set也大大节省了内存,我们来粗略计算一下,统计1亿个数据的基数,需要的内存是:100000000/8/1024/1024 ≈ 12M。

虽然bitmap在节省空间方面已经有了不错的表现,和set(100M * 4)相比,空间的消耗 缩小了 400倍。

当然,使用Bitmaps计算日活月活,还有很多优势的地方:

  • 计算日活:bitcount key获取key为1的数量;
  • 计算月活:可把30天的所有bitmap做or计算,再进行bitcount计算;
  • 计算留存率:昨日留存=昨天今天连续登录的人数/昨天登录的人数,即昨天的bitmap与今天的bitmap进行and计算,再除以昨天bitcount的数量。

通过以上计算,统计一个小型(10W用户)网站的日活已不在话下,我们发现Bitmaps已经很节省空间了。

但是大型互联网公司的日活、UV、PV,一个set如果上100M,这个就太浪费,而且也性能太低了。

怎么才能性能高点呢?

使用 HyperLogLog进行基数统计

以下是一个使用 Redis HyperLogLog 进行日活统计的 Java 示例代码:

<
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值