Redis HyperLogLog

366 阅读4分钟

今天是周五,你正开心的摸鱼,产品经理通过邮件给你发了一个需求文档。需求大概是:公司要统计网站每天的访客 IP,而且这个统计是一个长期的行为,短则数月、长则几年。

你看完需求就觉得这 so easy 啊,使用 Redis 的集合类型可以轻松实现这个功能:每天生成一个集合类型的键,使用 SADD 存储每天的访客 IP,使用 SCARD 命令就可以轻松得到每天访客 IP 的数量。

你很快就敲完了代码并通过测试,这个功能就上线了。上线后运行一段时间发现 Redis 所在服务器开始告警,原因是某些键的内存占用过大,你看了一下发现这些键都是存储访客 IP 的集合键。你这才拍了一下脑袋,知道自己给自己挖了一个大坑。

假设存储一个 IPv4 格式的 IP 地址最多需要 15 个字节,网站每天最多有 100 万个访客访问网站。这些集合键一个月就将使用 0.45 GB 的内存,一年将占用 5.4 GB 的内存,这还只是估算了 IPv4 格式的情况下,若是 IPv6 格式将占用更多的内存。SADD 和 SCARD 的时间复杂度虽然都是 O(1),但是它们对内存的消耗是无法接受的。

你在 Redis 的官方网站翻了翻,发现 Redis 还提供了一种数据类型 HyperLogLog,它既可以实现产品的需求还占用更少的内存。

HyperLogLog 算法

HyperLogLog 是一个专门为了计算集合的基数而创建的概率算法,它可以计算出一个给定集合的近似基数。

近似基数并非集合的实际基数,它可能会比实际的基数小一点或者大一点,但是估算基数和实际基数之间的误差会处于一个合理的范围之内,对于那些不要求十分精确的统计就可以使用 HyperLogLog 算法。

HyperLogLog 的优点在于它计算近似基数所需的内存并不会因为集合的大小而改变,无论集合包含的元素有多少个,HyperLogLog 进行计算所需的内存总是固定的,并且是非常少的。

Redis 的每个 HyperLogLog 类型只需要使用 12KB 内存空间,就可以对接近:264 个元素进行计数,而算法的标准误差仅为 0.81%。

如果使用 HyperLogLog 类型实现上述功能,每天有 100 万个访客的情况下,1 个月也仅仅占用 360KB 的内存。

PFADD

通过 PFADD 命令可以对给定的一个或多个集合元素进行计数。

PFADD key element [element...]

根据给定的元素是否已经进行过计数,PFADD 命令可能返回 0,也可能返回 1:

  • 如果给定的所有元素都已经进行过计数,那么 PFADD 命令将返回 0,表示 HyperLogLog 计算出的近似基数没有发生变化。
  • 如果给定的元素中出现了至少一个之前没有进行过计数的元素,导致 HyperLogLog 计算出的近似基数发生了变化,那么 PFADD 命令将返回 1。

例如:

redis> PFADD letters a b c -- 第一次添加
(integer) 1
redis> PFADD letters a     -- 第二次添加
(integer) 0

如果在调用该命令时仅指定 key 而不指定元素也是可以的,如果 key 存在,则不会有任何操作,如果不存在,则会创建一个数据结构(返回 1)。

PFCOUNT

通过 PFCOUNT 命令可以获取 HyperLogLog 为集合计算出的近似基数。若给定的 key 不存在将返回 0。

PFCOUNT key [key...]

例如:

redis> PFCOUNT letters
(integer) 3

当向 PFCOUNT 传入多个 HyperLogLog 时,PFCOUNT 命令将先对所有的 HyperLogLog 求并集,然后返回近似基数。

redis> PFADD letters1 a b c
(integer) 1
redis> PFADD letters2 c d e
(integer) 1
redis> PFCOUNT letters1 letters2
(integer) 5

PFMERGE

PFMERGE 命令可以对多个 HyperLogLog 执行并集计算,然后把计算得出的并集 HyperLogLog 保存到指定的键中。

PFMERGE destKey sourceKey [sourceKey...]

如果指定的键已经存在,PFMERGE 命令将覆盖已有的键。

redis> PFADD letters1 a b c
(integer) 1
redis> PFADD letters2 c d e
(integer) 1
redis> PFMERGE res letters1 letters2
OK
redis> PFCOUNT res
(integer) 5

可以看到 PFMERGE 和 PFCOUNT 命令十分相似,实际上 PFCOUNT 命令在计算多个 HyperLogLog 的近似基数时会执行以下操作:

  1. 在内部调用 PFMERGE 命令,计算所有给定 HyperLogLog 的并集,并将这个并集存储到一个临时的 HyperLogLog 中。
  2. 对临时 HyperLogLog 执行 PFCOUNT 命令,得到它的近似基数。
  3. 删除临时 HyperLogLog。
  4. 返回得到的近似基数。

当程序需要对多个 HyperLogLog 调用 PFCOUNT 命令,并且这个调用可能会重复执行多次时,可以考虑把这一调用替换成相应的 PFMERGE 命令调用:通过把并集的计算结果存储到指定的 HyperLogLog 中而不是每次都重新计算并集,程序可以最大程度地减少不必要的并集计算。

业务场景

HyperLogLog 的特性十分适合:计数(月度、年度统计)、去重(垃圾短信检测)等场景。