后端与中间件 · 教程

Redis数据结构之 `HyperLogLogs`

本文写于 2021 年,涉及的组件版本、配置与数据可能已过时,请按经验材料阅读并核对当前版本。原文链接与内容保持不变。

书接上回

上一篇我们学习的 bitmap 这一“数据类型”。其内部是由sds这种种数据结构编码的。 如果不记得了,那就来坐穿梭机回去看看吧。 开始穿梭

接下来,我们继续学习一个新的"数据类型", 位图, HyperLogLogs.(注意啦,数据类型,我又加了引号!!)

HyperLogLogs简介

HyperLogLog是一种概率数据结构,用于对唯一事物进行计数(从技术上讲,这是指估计集合的基数)。

注意哦, HyperlogLog其实是一种基数计数算法,并非Redis独有的。

通常,对唯一项目进行计数需要使用与要计数的项目数量成比例的内存量,因为您需要记住过去已经看到的元素,以避免多次对其进行计数。但是,有一组算法会以内存换取精度:以Redis实施为例,您得出的带有标准误差的估计度量最终会小于1%。

该算法的神奇之处在于,您不再需要使用与所计数项目数量成比例的内存量,而可以使用恒定数量的内存!在最坏的情况下为12k字节,如果您的HyperLogLogs看到的元素很少,而且少得多。

Redis中的HyperLogLog尽管在技术上是不同的数据结构,但被编码为Redis字符串(sds),因此您可以调用GET来序列化HyperLogLogs,然后调用SET来将其反序列化回服务器。这里我们在文末会大体翻一下源码。

接下来我们先看一下 HyperLogLog 有什么应用场景。

HyperLogLog 的应用场景

根据HyperLogLog的特性来看,使用了一种概率性计数的功能,这样的功能有一个特点就是当数据特别大的时候,其统计的值是不准确的。什么意思呢?

就是比如统计一个网站的PV,PV数值是 123456789 或者 123456000 这两者的值对于管理者来讲是一样的。对于一些对精度要求不准确而且数据量很大的场景是非常适合的。

比如以下场景:计算网站的PV,UV,统计日活,月活,统计用户每天搜索的词条数等等。

HyperLogLog 的常用命令

HyperLogLog 应该是Redis所有结构中命令最少的了,只有三个命令。

  • PFADD key element [element ...]

添加元素。 如果一个HyperLogLog的估计的近似基数在执行命令过程中发了变化, PFADD 返回1,否则返回0,如果指定的key不存在,这个命令会自动创建一个空的HyperLogLog结构(指定长度和编码的字符串).

127.0.0.1:6379> pfadd k96 v1 v2 v3
(integer) 1
127.0.0.1:6379> pfadd k96 v4
(integer) 1
127.0.0.1:6379> pfadd k96 v1
(integer) 0
127.0.0.1:6379> pfadd k96 v2 v3 v4
(integer) 0
127.0.0.1:6379> 
  • PFCOUNT key [key ...]

当参数为一个key时,返回存储在HyperLogLog结构体的该变量的近似基数,如果该变量不存在,则返回0.

当参数为多个key时,返回这些HyperLogLog并集的近似基数,这个值是将所给定的所有key的HyperLoglog结构合并到一个临时的HyperLogLog结构中计算而得到的.

127.0.0.1:6379> pfadd k97 v1 v2 v3 v4
(integer) 1
127.0.0.1:6379> PFCOUNT k97
(integer) 4
# 测试多个key
127.0.0.1:6379> PFCOUNT k97 k96
(integer) 4
127.0.0.1:6379> PFADD k97 v5 v6
(integer) 1
127.0.0.1:6379> PFCOUNT k97 k96
(integer) 6

刚才说了,HyperLogLog只是存储总数的一种结构,而且其值也会和实际值有偏差。我们一起来验证一下这个结果

我们往Redis中插入10000条数据,查看其 pfcount的值是多少。

使用下面这个脚本去插入(java实现)。公众号回复 RedisClient 可以获取完整源码。

String host = "10.1.14.159";
int port = 6379;

SocketRedisClient client = new SocketRedisClient(host, port);
String authCmd = client.exec("auth moyang");
System.out.println("登录验证结果:" + authCmd);
System.out.println("删除已经存在的key :" + client.exec("del k97"));
for (int i = 1; i <= 10000; i++) {
    String cmd = "pfadd k97 " + i;
    client.exec(cmd);

    String cmd2 = "pfcount k97 ";
    String exec = client.exec(cmd2);
    System.out.println("执行命令:" + cmd + " 返回结果:" + exec); // 返回的结果是未解析Redis协议的。
}
client.releases();

插入完成之后,如下图,我们可以看到 pfCount 的结果是 9988.

如果不相信我们可以 在shell里看看。

127.0.0.1:6379> pfcount k97
(integer) 9988

我这次测试正好相等,其实由于脚本的问题呢,从程序里获取的值和shell里获取的值,可能不一样,这种怎么解决呢? 使用脚本单独再执行一次,就会一样了。具体原因,不赘述了。

好了,我们插入了10000次,但是得出的值却是 9988, 这也就验证了其不精确性。

  • PFMERGE destkey sourcekey [sourcekey ...]

将多个 HyperLogLog 合并(merge)为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合(observed set)的并集.

合并得出的 HyperLogLog 会被储存在目标变量(第一个参数)里面, 如果该键并不存在, 那么命令在执行之前, 会先为该键创建一个空的 HyperLogLog.

127.0.0.1:6379> pfadd k98 v1 v2 v3
(integer) 1
127.0.0.1:6379> pfadd k98_1 v2 v3 v4
(integer) 1
127.0.0.1:6379> pfadd k98_2 v3 v4 v5
(integer) 1
127.0.0.1:6379> pfmerge k98 k98_1 k98_2
OK
127.0.0.1:6379> pfcount k98
(integer) 5
127.0.0.1:6379> pfcount k98_1
(integer) 3
127.0.0.1:6379> pfcount k98_2
(integer) 3

以上就是 HyperLogLog 的命令,老规矩我们下一步来看看 HyperLogLog在Redis中是怎么实现的。

HyperLogLog的结构

在 Clion 中直接查找hyperloglog ,就是 hyperloglog 的实现了。

hyperloglog-查找目录.png

我们可以看到有一个 struct

struct hllhdr {
    char magic[4];      /* 魔数: "HYLL" */
    uint8_t encoding;   /* HLL_DENSE或HLL_SPARSE */
    uint8_t notused[3]; /* 保留以供将来使用,必须为零。 */
    uint8_t card[8];    /* 缓存基数,小端。 */
    uint8_t registers[]; /* 数据字节。 */
};

这个就是 HyperLogLog 的存储结构了。这里大家大体有个印象就可以了。HyperLogLog是一种基数估算算法的实现。后面我们会介绍这种基数估算法。

预告

最后一个 数据类型 geohash (多维空间点索引算法)!!!

最后

希望与你一起遇见更好的自己

微信二维码

配套资料 · 免费领取
本文属于「BACKEND」资料包范围:中间件源码阅读路线图。按 RocketMQ / Redis / MySQL 分章节的完整路线。
在公众号「方家小白」回复关键词 BACKEND 即可领取。全文无需关注即可阅读。
BACKEND