欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Redis 的 HyperLoglog

程序员文章站 2022-07-10 20:30:05
...

Redis与HyperLogLog

在开始这一节之前,我们先思考一个常见的业务问题:如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的 UV 数据,然后让你来开发这个统计模块,你会如何实现?

如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。

但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一ID 来标识。

你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用户 ID。当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。没错,这是一个非常简单的方案。

但是,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。如果这样的页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,值得么?其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解决方案呢?

这就是本节要引入的一个解决方案,Redis 提供了 HyperLogLog 数据结构就是用来解决这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。HyperLogLog 数据结构是 Redis 的高级数据结构,它非常有用,但是令人感到意外的是,使用过它的人非常少。

Redis Hyperloglog 操作

要进行 Redis Hyperloglog 的操作,我们可以使用以下三个命令:

  • PFADD
  • PFCOUNT
  • PFMERGE

我们用一个实际的例子来解释这些命令。比如,有这么个场景,用户登录到系统,我们需要在一小时内统计不同的用户。 因此,我们需要一个 key,例如 USER:LOGIN:2019092818。 换句话说,我们要统计在 2019 年 09 月 28 日下午 18 点至 19 点之间发生用户登录操作的非重复用户数。对于将来的时间,我们也需要使用对应的 key 进行表示,比如 2019111100、2019111101、2019111102 等。

我们假设,用户 A、B、C、D、E 和 F 在下午 18 点至 19 点之间登录了系统。

127.0.0.1:6379> pfadd USER:LOGIN:2019092818 A
(integer) 1
127.0.0.1:6379> pfadd USER:LOGIN:2019092818 B C D E F
(integer) 1
127.0.0.1:6379>

当进行计数时,你会得到预期的 6。

127.0.0.1:6379> pfcount USER:LOGIN:2019092818
(integer) 6

如果 A 和 B 在这个时间内多次登录系统,你也将得到相同的结果,因为我们仅保留不同的用户。

127.0.0.1:6379> pfadd USER:LOGIN:2019092818 A B
(integer) 0
127.0.0.1:6379> pfcount USER:LOGIN:2019092818
(integer) 6

如果用户 A~F 和另外一个其他用户 G 在下午 19 点至下午 20 点之间登录系统:

127.0.0.1:6379> pfadd USER:LOGIN:2019092819 A B C D E F G
(integer) 1
127.0.0.1:6379> pfcount USER:LOGIN:2019092819
(integer) 7

现在,我们有两个键 USER:LOGIN:2019092818 和 USER:LOGIN:2019092819,如果我们想知道在 18 点到 20 点(2 小时)之间有多少不同的用户登录到系统中,我们可以直接使用pfcount命令对两个键进行合并计数:

127.0.0.1:6379> pfcount USER:LOGIN:2019092818 USER:LOGIN:2019092819
(integer) 7

如果我们需要保留键值而避免一遍又一遍地计数,那么我们可以将键合并为一个键 USER:LOGIN:2019092818-19,然后直接对该键进行pfcount操作,如下所示。

127.0.0.1:6379> pfmerge USER:LOGIN:2019092818-19 USER:LOGIN:2019092818 USER:LOGIN:2019092819
OK
127.0.0.1:6379> pfcount USER:LOGIN:2019092818-19
(integer) 7

HyperLogLog

HyperLogLog,它是 LogLog 算法的升级版,作用是能够提供不精确的去重计数。存在以下的特点:

  • 代码实现较难。
  • 能够使用极少的内存来统计巨量的数据,在 Redis 中实现的 HyperLogLog,只需要12K内存就能统计2^64个数据。
  • 计数存在一定的误差,误差率整体较低。标准误差为 0.81% 。
  • 误差可以被设置辅助计算因子进行降低。

稍微对编程中的基础数据类型内存占用有了解的同学,应该会对其只需要12K内存就能统计2^64个数据而感到惊讶。为什么这样说呢,下面我们举下例子:
取 Java 语言来说,一般long占用8字节,而一字节有8位,即:1 byte = 8 bit,即long数据类型最大可以表示的数是:263-1。对应上面的264个数,假设此时有2^63-1这么多个数,从 0 ~ 2^63-1,按照long以及1k = 1024字节的规则来计算内存总数,就是:((2^63-1) * 8/1024)K,这是很庞大的一个数,存储空间远远超过12K。而 HyperLogLog 却可以用 12K 就能统计完。

注意事项

HyperLogLog 这个数据结构不是免费的,不是说使用这个数据结构要花钱,它需要占据一定 12k 的存储空间,所以它不适合统计单个用户相关的数据。如果你的用户上亿,可以算算,这个空间成本是非常惊人的。但是相比 set 存储方案,HyperLogLog 所使用的空间那真是可以使用千斤对比四两来形容了。

不过你也不必过于当心,因为 Redis 对 HyperLogLog 的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间。

pf 的内存占用为什么是 12k?

在 Redis 的 HyperLogLog 实现中用到的是 16384 个桶,也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,最大可以表示 maxbits=63,于是总共占用内存就是 2^14 * 6 / 8 = 12k 字节。

相关标签: Redis