Redis_22_Redis中的HyperLogLog数据类型

一、前言

bitmap和HyperLogLog都是redis的特殊类型,都是回答redis的时候的面试加分项,都是节约内存空间的方式,节约内存空间 int < bitmap < hyperloglog,hyperloglog节约内存空间最厉害
bitmap底层很简单,就是按bit位存储,
hyperloglog底层复杂,单独拿出一篇博客来

二、HyperLogLog 概要

HyperLogLog 是一种 估算基数的近似最优算法,在 Redis 中被当做一种 新的数据结构(new datastruct) (算法实现确实需要一种特定的数据结构来实现)。

业务需求:关于基数统计
基数统计(Cardinality Counting) 通常是用来统计一个集合中不重复的元素个数,统计整个网站web页面的UV,UV在PV的基础上根据用户id去重,这就要求了每一个网页请求都需要带上用户的 ID,无论是登录用户还是未登录的用户,都需要一个唯一 ID 来标识。

方案一,Redis使用set数据结构,自带去重:为每一个页面设置一个独立的 set 集合 来存储所有当天访问过此页面的用户 ID。
方案一缺点:
存储空间巨大: 如果网站访问量一大,你需要用来存储的 set 集合就会非常大,如果页面再一多… 为了一个去重功能耗费的资源就可以直接让你 老板打死你;
聚合函数统计复杂: 这么多 set 集合如果要聚合统计一下,又是一个复杂的事情;
方案二,使用 B 树 数据结构
B 树最大的优势就是插入和查找效率很高,如果用 B 树存储要统计的数据,可以快速判断新来的数据是否存在,并快速将元素插入 B 树。要计算基础值,只需要计算 B 树的节点个数就行了。
缺点:存储空间大,将 B 树结构维护到内存中,能够解决统计和计算的问题,但是 并没有节省内存。
方案三,使用 bitmap位图 数据结构
bitmap 可以理解为通过一个 bit 数组来存储特定数据的一种数据结构,每一个 bit 位都能独立包含信息,bit 是数据的最小存储单位,因此能大量节省空间,也可以将整个 bit 数据一次性 load 到内存计算。如果定义一个很大的 bit 数组,基础统计中 每一个元素对应到 bit 数组中的一位,例如:
优点,存储空间小:bitmap 还有一个明显的优势是 可以轻松合并多个统计结果,只需要对多个结果求异或就可以了,也可以大大减少存储内存。可以简单做一个计算,如果要统计 1 亿 个数据的基数值,大约需要的内存:100_000_000/ 8/ 1024/ 1024 ≈ 12 M,如果用 32 bit 的 int 代表 每一个 统计的数据,大约需要内存:32 * 100_000_000/ 8/ 1024/ 1024 ≈ 381 M
金手指: 1 亿个数据,int类型需要内存 381M,bitmap只需要 12M
缺点,存储空间还不够小:可以看到 bitmap 对于内存的节省显而易见,但仍然不够。统计一个对象的基数值就需要 12 M,如果统计 1 万个对象,就需要接近 120 G,对于大数据的场景仍然不适用。

方案四,三种概率算法(Linear counting LogLog HyperLogLog)
实际上目前还没有发现更好的在 大数据场景 中 准确计算 基数的高效算法,因此在不追求绝对精确的情况下,使用概率算法算是一个不错的解决方案。
记住,概率算法 不直接存储 数据集合本身,通过一定的 概率统计方法预估基数值,这种方法可以大大节省内存,同时保证误差控制在一定范围内。
目前用于基数计数的概率算法包括:
Linear Counting(LC):早期的基数估计算法,LC 在空间复杂度方面并不算优秀,实际上 LC 的空间复杂度与上文中简单 bitmap 方法是一样的(但是有个常数项级别的降低),都是 O(Nmax)
LogLog Counting(LLC):LogLog Counting 相比于 LC 更加节省内存,空间复杂度只有 O(log2(log2(Nmax)))
HyperLogLog Counting(HLL):HyperLogLog Counting 是基于 LLC 的优化和改进,在同样空间复杂度情况下,能够比 LLC 的基数估计误差更小(金手指:HyperLogLog相对于LogLog,不是缩小空间,而是提供准确率
其中,HyperLogLog 的表现是惊人的,用 bitmap 存储 1 个亿 统计数据大概需要 12 M 内存,而在 HyperLoglog 中,只需要不到 1 K 内存就能够做到!在 Redis 中实现的 HyperLoglog 也只需要 12 K 内存,在 标准误差 0.81% 的前提下,能够统计 2^64 个数据!

三、HyperLogLog 原理

3.1 从游戏到代码

3.1.1 游戏

我们来思考一个抛硬币的游戏:你连续掷 n 次硬币,然后说出其中连续掷为正面的最大次数,我来猜你一共抛了多少次。

这很容易理解吧,例如:你说你这一次 最多连续出现了 2 次 正面,那么我就可以知道你这一次投掷的次数并不多,所以 我可能会猜是 5 或者是其他小一些的数字,但如果你说你这一次 最多连续出现了 20 次 正面,虽然我觉得不可能,但我仍然知道你花了特别多的时间。

只要你重复实验,我得到了更多的数据之后就会估计得更准。我们来把刚才的游戏换一种说法:

金手指:抛掷硬币有正面和反面两种情况,任何一个bit位有 0|1 两种情况。

在这里插入图片描述

这张图的意思是,我们给定一系列的随机整数,记录从最低位开始,连续零位的最大长度 K,即为图中的 maxbit,通过这个 K 值我们就可以估算出随机数的数量 N。

问题,当从最低位开始,连续零位的最大长度为 16 bit,那么N为多少?

3.1.2 代码实验

我们可以简单编写代码做一个实验,来探究一下 K 和 N 之间的关系:

public class Test0 {
    static class BitKeeper {  // 静态内部类,bit位持有者

        private int maxbit;  // 从最低位开始,连续零位的最大长度K

        public void random() {   // 从 0 - 2L<<32 中,随机出来一个数字
            long value = ThreadLocalRandom.current().nextLong(2L << 32);
            int bit = lowZeros(value);    // 参数值为随机数作为N,返回值为从最低位开始,连续零位长度K
            if (bit > this.maxbit) {    // 计算出来的最低零位长度的K大于当前的,就更新当前的,不大于,就保持当前的
                this.maxbit = bit;
            }
        }

        private int lowZeros(long value) {   // 计算最低零位长度的K
            int i = 0;    // i=0;
            for (; i < 32; i++) {    // 为什么到32退出?
                if (value >> i << i != value) {    // 先右边移动i位,再左移i位,还是==value
                 break;
                }
                // 对于5 返回0      返回值表示 最低位连续0的个数为0个
                // 对于6   返回1    返回值表示 最低位连续0的个数为1个
                // 对于7 i为1就不行了,跳出i-1,返回0   返回值表示   最低位连续0的个数为0个
                // 对于8 i为 0 1 2 3 都可以,4不行,跳出i-1,返回为3  返回值表示  最低位连续0的个数为3个
                // 对于 9 i为1,就!=value  跳出并将i-1; 返回为0       返回值表示    最低位连续0的个数为0个
                // 对于 10 i为1,可以==value,i=2,就!=value,所以,跳出并将i-1; 返回为1  返回值表示   最低位连续0的个数为1个
            }
            return i - 1;
        }
    }

    static class Experiment {

        private int n;
        private BitKeeper keeper;

        public Experiment(int n) {
            this.n = n;
            this.keeper = new BitKeeper();
        }

        public void work() {
            for (int i = 0; i < n; i++) {
                this.keeper.random();    // 对于同一个keeper对象,调用n次random,在n次中,得到最大的maxbit
            }
        }

        public void debug() {

            //  返回参数的自然数底数e 2.7 的对数值

            System.out.printf("%d %.2f %.2f %.2f %d\n",
                    this.n, Math.log(this.n) , Math.log(2),Math.log(this.n) / Math.log(2), this.keeper.maxbit);   // 输出三个数字,第一个是构造函数中传递过来的n
            // 第二个是 
            // 第三个是  从最低位开始,连续零位的最大长度K,就是一个keeper对象得到的maxbit
        }
    }

    public static void main(String[] args) {
        for (int i = 1000; i < 100000; i += 100) {
            Experiment exp = new Experiment(i);
            exp.work();   // 随机n次,这里的n就是i, 从1000 到 100000,每次间隔100
            exp.debug();   // 打印每一次随机
        }
        
    }
}

部分输出查看:

//n   n/log2 maxbit
34000  15.05  13
35000  15.10  13
36000  15.14  16
37000  15.18  17
38000  15.21  14
39000  15.25  16
40000  15.29  14
41000  15.32  16
42000  15.36  18

会发现 K(即maxbit) 和 N 的对数之间存在显著的线性相关性:N 约等于 2^k

3.2 更近一步:分桶平均

如果 N 介于 2 ^k 和 2 ^(k+1) 之间,用这种方式估计的值都等于 2 ^k,这明显是不合理的,所以我们可以使用多个 BitKeeper 进行加权估计,就可以得到一个比较准确的值了:

public class Test {
    static class BitKeeper {
        private int maxbit;  // 从最低位开始,连续零位的最大长度K

        public void random() {   // 从 0 - 2L<<32 中,随机出来一个数字
            long value = ThreadLocalRandom.current().nextLong(2L << 32);
            int bit = lowZeros(value);    // 随机数作为N,去计算最低零位长度K
            if (bit > this.maxbit) {    // 计算出来的最低零位长度的K大于当前的,就更新当前的,不大于,就保持当前的
                this.maxbit = bit;
            }
        }

        private int lowZeros(long value) {   // 计算最低零位长度的K
            int i = 0;    // i=0;
            for (; i < 32; i++) {    // 为什么到32退出?
                if (value >> i << i != value) {
                    break;
                }
            }
            return i - 1;
        }
    }

    static class Experiment {

        private int n;
        private int k;
        private BitKeeper[] keepers;

        public Experiment(int n) {
            this(n, 1024);
        }

        public Experiment(int n, int k) {
            this.n = n;   // 同时记录n和k,比之前多了一个k,然后一个keeper对象变成了一个keeper数组
            this.k = k;
            this.keepers = new BitKeeper[k];
            for (int i = 0; i < k; i++) {
                this.keepers[i] = new BitKeeper();
            }
        }

        public void work() {
            for (int i = 0; i < this.n; i++) {   // 循环n次
                long m = ThreadLocalRandom.current().nextLong(1L << 32);   // 生成一个随机数字
                BitKeeper keeper = keepers[(int) (((m & 0xfff0000) >> 16) % keepers.length)];  // 保留高12位,然后向右移动16位,然后长度取模
                keeper.random();    // 对于每个keeper对象,调用random()函数
            }
        }

        public double estimate() {   // 估算函数   之前没有这个函数
            double sumbitsInverse = 0.0;
            for (BitKeeper keeper : keepers) {   // 遍历keepers数组中每一个元素
                sumbitsInverse += 1.0 / (float) keeper.maxbit;   // 不断累加所有keeper对象的从最低位开始的连续零位的长度,
            }
            double avgBits = (float) keepers.length / sumbitsInverse;   // 求平均值,keepers元素个数除以
            return Math.pow(2, avgBits) * this.k;  // 估算值N,就是用2的平均数为幂, * 1024 (k  只使用一个参数的构造函数)
            // 为什么要使用 *1024 因为这个k=1024 是keeper元素个数,计算了他们的倒数平均数,当然要 * 1024个数组元素,才能得到总值
        }
    }

    public static void main(String[] args) {
        for (int i = 100000; i < 1000000; i += 100000) {   // 初始10万,每次间隔10万,所以一共10次就可以到100万了
            Experiment exp = new Experiment(i);   // 这个i放进去就是循环的次数n
            exp.work();
            double est = exp.estimate();
            System.out.printf("%d %.2f %.2f\n", i, est, Math.abs(est - i) / i);  // 第一,真实值;第二,估算值,第三,差值
        }
    }
}

上述代码就有 1024 个 “评委”,并且在计算平均值的时候,采用了 调和平均数,也就是倒数的平均值,它能有效地平滑离群值的影响:

avg = (3 + 4 + 5 + 104) / 4 = 29
avg = 4 / (1/3 + 1/4 + 1/5 + 1/104) = 5.044

观察脚本的输出,误差率百分比控制在个位数:

100000 95265.04 0.05
200000 192633.26 0.04
300000 300796.30 0.00
400000 412490.59 0.03
500000 479796.79 0.04
600000 614286.27 0.02
700000 681325.70 0.03
800000 793219.74 0.01
900000 897108.97 0.00

真实的 HyperLogLog 要比上面的示例代码更加复杂一些,也更加精确一些。上面这个算法在随机次数很少的情况下会出现除零错误,因为 maxbit = 0 是不可以求倒数的。

3.3 真实的 HyperLogLog

有一个神奇的网站,可以动态地让你观察到 HyperLogLog 的算法到底是怎么执行的:http://content.research.neustar.biz/blog/hll.html

在这里插入图片描述

其中的一些概念这里稍微解释一下,您就可以自行去点击 step 来观察了:
(1)m 表示分桶个数: 从图中可以看到,这里分成了 64 个桶,就是中间那个表格,4 *16 = 64小格,每个小格子的数字表示Register Values 注册值;
(2)蓝色的 bit 表示在桶中的位置: 例如图中的 101110 实则表示二进制的 46(2 + 4 + 8 + 32 =46),所以该元素被统计在中间大表格 Register Values 中标红的序号为 46 的桶中(第一行0-15 第二行 16-31 第三行 32-47 第四行48-63);
(3)绿色的 bit 表示第一个 1 出现的位置:从图中可以看到标绿的 bit 中,从右往左数,第一位就是 1,所以在 Register Values 第 46 个桶中写入 1;
(4)红色 bit 表示绿色 bit 的值的累加: 下一个出现在第 46 个桶的元素值会被累加;

注意: 字节位序是左边低位右边高位,所以从右到左;而通常我们使用的字节都是左边高位右边低位,所以从左到右。

Actual Cardinality:20,autual 表示真实的,cardinality表示基数,所以,整个表示基数值是20
当使用LogLog算法,估算estimated 基数为35 ,错误率为75
当使用HyperLogLog算法,估算estimated 基数为23 ,错误率为15

问题1:为什么要统计 Hash 值中第一个 1 出现的位置?
回答1:因为第一个 1 出现的位置可以同我们抛硬币的游戏中第一次抛到正面的抛掷次数对应起来,根据上面掷硬币实验的结论,记录每个数据的第一个出现的位置 K,就可以通过其中最大值 Kmax 来推导出数据集合中的基数:N = 2^Kmax

问题2:PF 的内存占用为什么是 12 KB?
回答2:我们上面的算法中使用了 1024 个桶,网站演示也只有 64 个桶,不过在 Redis 的 HyperLogLog 实现中,用的是 16384 个桶,即:2^ 14 ,也就是说,就像上面网站中间那个 Register Values 表格中有 16384 个小格(2^14个小格子 1024*16 =16384)。
而Redis 最大能够统计的数据量是 2 ^64,即每个桶(即每一个小格)的 maxbit 需要 6 个 bit 来存储(所以,最大可以表示 maxbit = 63(2 ^6 =64,数值范围表示 0-63)),所以,HyperLogLog 总共占用内存就是:(2 ^14) x 6 / 8 (*每个桶 6 bit,而这么多桶本身要占用 16384 6 bit,再除以 8 转换成 KB),算出来的结果就是 12 KB。 good

四、Redis 中的 HyperLogLog 内部存储(HYLL 对象头 + 16384 个桶)/ 为什么HyperLogLog只需要12KB就可以存放2^64 数据,初始化情况下,只需要2B就可以统计2 ^64 数据?

从上面我们算是对 HyperLogLog 的算法和思想有了一定的了解,并且知道了一个 HyperLogLog 实际占用的空间大约是 12 KB,但 Redis 对于内存的优化非常变态,当 计数比较小 的时候,大多数桶的计数值都是 零,这个时候 Redis 就会适当节约空间,转换成另外一种 稀疏存储方式,与之相对的,正常的存储模式叫做 密集存储,这种方式会恒定地占用 12 KB。

4.1 密集型存储结构

4.1.1 引入:密集型存储结构

密集型的存储结构非常简单,就是Redis 的 HyperLogLog 16384 个桶, Register Values 表格中有 16384 个小格,即每一个桶/小格 6 bit 连续串成 的字符串位图,如下:

在这里插入图片描述

默认情况下,一个字节是由 8 个 bit 组成的,这样 6 bit 排列的结构就会导致,有一些桶会 跨越字节边界(比如bucket1 就是第一个字节的后两位 + 第二个字节的前四位),我们需要 对这一个或者两个字节进行适当的移位拼接 才可以得到具体的计数值

假设桶的编号为 index,这个 6 bit 计数值的起始字节偏移用 offset_bytes 表示,它在这个字节的起始比特位置偏移用 offset_bits 表示,于是我们有:

offset_bytes = (index * 6) / 8    商     **起始字节偏移用 offset_bytes 表示**
offset_bits = (index * 6) % 8   余数    **起始比特位置偏移用 offset_bits 表示**

前者是商,后者是余数。比如,
bucket 0 的 字节偏移 = 0 ,也就是第1个字节,比特位置偏移为 0,也就是第 1 个bit位,综合,也就是 第1个字节的第1个bit位;
bucket 1 的 字节偏移 = 0 ,也就是第1个字节,比特位置偏移为 6,也就是第 7 个bit位,综合,也就是 第1个字节的第7个bit位;
bucket 2 的 字节偏移是 1,也就是第 2 个字节;它的 比特位置 偏移是 4,也就是第 5 个bit位,综合也就是第 2 个字节的第 5 个位开始是 bucket 2 的计数值。

4.1.2 Redis处理密集型存储结构

金手指:分别为两种情况,
第一种情况:如果 起始比特位置偏移offset_bits 小于等于 2,说明这 6 bit 在一个字节的内部,可以直接使用下面的表达式得到计数值 val:

val = buffer[offset_bytes] >> offset_bits  # 向右移位

第二种情况:如果 offset_bits 大于 2,那么就会涉及到 跨越字节边界,我们需要拼接两个字节的位片段:

low_val = buffer[offset_bytes] >> offset_bits   # 低位值
low_bits = 8 - offset_bits  # 低位个数
val = (high_val << low_bits | low_val) & 0x111111   # 拼接,保留低6位

不过下面 Redis 的源码要晦涩一点,看形式它似乎只考虑了跨越字节边界的情况。这是因为如果 6 bit 在单个字节内,上面代码中的 high_val 的值是零,所以这一份代码可以同时照顾单字节和双字节,good 如下:

// 获取指定桶的计数值
#define HLL_DENSE_GET_REGISTER(target,p,regnum) do { \
    uint8_t *_p = (uint8_t*) p; \
    unsigned long _byte = regnum*HLL_BITS/8; \
    unsignedlong _fb = regnum*HLL_BITS&7; \  # %8 = &7
    unsignedlong _fb8 = 8 - _fb; \
    unsignedlong b0 = _p[_byte]; \
    unsignedlong b1 = _p[_byte+1]; \
    target = ((b0 >> _fb) | (b1 << _fb8)) & HLL_REGISTER_MAX; \
} while(0)

// 设置指定桶的计数值
#define HLL_DENSE_SET_REGISTER(p,regnum,val) do { \
    uint8_t *_p = (uint8_t*) p; \
    unsigned long _byte = regnum*HLL_BITS/8; \
    unsigned long _fb = regnum*HLL_BITS&7; \
    unsigned long _fb8 = 8 - _fb; \
    unsigned long _v = val; \
    _p[_byte] &= ~(HLL_REGISTER_MAX << _fb); \
    _p[_byte] |= _v << _fb; \
    _p[_byte+1] &= ~(HLL_REGISTER_MAX >> _fb8); \
    _p[_byte+1] |= _v >> _fb8; \
} while(0)

4.2 稀疏存储结构

稀疏存储适用于很多计数值都是零的情况。下图表示了一般稀疏存储计数值的状态:
在这里插入图片描述

当 多个连续桶的计数值都是零 时,Redis 提供了几种不同的表达形式:

(1)(小于等于64个数量的)连续的零值计数器可以使用稀疏存储,使用00xxxxxx:

对于00xxxxxx这种稀疏存储的解释:对于一个8位字节来说,如果前缀两个零,则表示接下来的 6bit 整数值加 1 表示零值计数器的数量,注意这里要加 1 是因为数量如果为零是没有意义的。比如 00 01 01 01 表示连续 22(1 +4 +16=21,连续22个是指 从0到21这22个) 个零值计数器。

00 000000 表示连续1个0,因为连续0个0没有意义;
00 000001 表示连续2个0,
00 111111 表示连续64个0

金手指:对于二进制数字来说,最低位为0表示偶数,最低位为1表示奇数。

(2)(大于64个数量的)连续的零值计数器可以使用稀疏存储,使用01xxxxxx yyyyyyyy:

01xxxxxx yyyyyyyy:6bit 最多只能表示连续 64 个零值计数器,这样扩展出的 14bit 可以表示最多连续 16384 个零值计数器(2 ^6 =64个数字,2 ^14 =1024*16=16384个数字)。这意味着 HyperLogLog 数据结构中 16384 个桶的初始状态,所有的计数器都是零值,可以直接使用 2 个字节来表示,初始状态仅用两个字节就可以了,空间成本太小了

01 000000 00000000 表示连续1个0,因为连续0个0没有意义;
01 000000 00000000 表示连续2个0,
01 111111 11111111 表示连续16384个0,所有的桶都是0,存满了,只要16bit,两个字节就好

所以,上面说HyperLogLog只需要12KB(是指使用密集型存储情况下),如果特殊情况下压缩,例如初始状态,只需要两个字节就可以了(是指初始状态使用稀疏存储)。

(3)(小于等于32计数值的)连续相同的计数值可以使用稀疏存储,使用1vvvvvxx:

对于1vvvvvxx这种稀疏存储的解释:
中间 5bit 表示计数值,尾部 2bit 表示连续的桶数。如果出现 1vvvvvxx,它表示的意思是:连续 (xx +1) 个计数值都是 (vvvvv + 1)。比如 1 01010 11 表示连续 4 个计数值都是 11。

计数值 00000 表示 1 ,因为存储0没有意义。
计数值 11111 表示 32

数量 00 表示连续1次,因为连续0次没有意义;
数量 01 表示连续2次,
数量 10 表示连续3次,
数量 11 表示连续4次

金手指:HyperLogLog可以灵活切换稀疏存储和密集存储,在存储的数据的时候仅占用最小的空间
HyperLogLog可以灵活切换稀疏存储和密集存储,例如,对于连续相同的计数值可以使用稀疏存储 1vvvvvxx,这种存储方式的计数值最大只能表示到 32(5bit用来存储计数值,2bit用来存储连续的桶数,所以,最大存储的计数值为 (16+8+4+2+1)+1 =32,最大的连续次数为(2+1)+1=4次),一旦超过,就必须使用密集存储了,HyperLogLog 的密集存储单个计数值用 6bit 表示,最大可以表示到 63。
所以,当稀疏存储的某个计数值需要调整到大于 32 时,Redis 就会立即转换 HyperLogLog 的存储结构,将稀疏存储转换成密集存储。

4.3 对象头

HyperLogLog 除了需要存储 16384 个桶的计数值之外,它还有一些附加的字段需要存储,比如总计数缓存、存储类型。所以它使用了一个额外的对象头来表示:

struct hllhdr {
    char magic[4];      /* 魔术字符串"HYLL" */
    uint8_t encoding;   /* 存储类型 HLL_DENSE or HLL_SPARSE. */
    uint8_t notused[3]; /* 保留三个字节未来可能会使用 */
    uint8_t card[8];    /* 总计数缓存 */
    uint8_t registers[]; /* 所有桶的计数器 */
};

金手指1:HyperLogLog 整体的内部结构就是 HYLL 对象头 加上 16384 个桶的计数值位图
金手指2:HyperLogLog 在 Redis 的内部结构表现就是一个字符串位图,你可以把 HyperLogLog 对象当成普通的字符串来进行处理。

HyperLogLog 在 Redis 的内部结构表现就是一个字符串位图,你可以把 HyperLogLog 对象当成普通的字符串来进行处理。

> PFADD codehole python java golang
(integer) 1
> GET codehole
"HYLL\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80C\x03\x84MK\x80P\xb8\x80^\xf3"
但是 不可以 使用 HyperLogLog 指令来 操纵普通的字符串,因为它需要检查对象头魔术字符串是否是 "HYLL"

五、HyperLogLog 的使用

HyperLogLog 提供了两个指令 PFADD 和 PFCOUNT,字面意思就是一个是增加,另一个是获取计数。

(1)PFADD 和 set 集合的 SADD 的用法是一样的,来一个用户 ID,就将用户 ID 塞进去就是;
(2)PFCOUNT 和 SCARD 的用法是一致的,直接获取计数值:

> PFADD codehole user1   // pfadd 添加user1
(interger) 1
> PFCOUNT codehole    // pfcount计数
(integer) 1
> PFADD codehole user2     // pfadd 添加user2
(integer) 1
> PFCOUNT codehole    // pfcount计数
(integer) 2
> PFADD codehole user3    // pfadd 添加user3
(integer) 1
> PFCOUNT codehole    // pfcount计数
(integer) 3
> PFADD codehole user4 user 5     // pfadd 添加user4 user5
(integer) 1
> PFCOUNT codehole     // pfcount计数
(integer) 5

我们可以用 Java 编写一个脚本来试试 HyperLogLog 的准确性到底有多少:

publicclass JedisTest {
  public static void main(String[] args) {
    for (int i = 0; i < 100000; i++) {
      jedis.pfadd("codehole", "user" + i);    // Java程序使用Jedis客户端来操作连接的redis,jedis就是服务器上redis的抽象,这里进行10万次pdadd()操作
    }
    long total = jedis.pfcount("codehole");   // 计数
    System.out.printf("%d %d\n", 100000, total);   // 打印total,计数值
    jedis.close();   // 关闭连接
  }
}

结果输出如下:

10000099723

由输出结果: 10 万条数据只差了 277,按照百分比误差率是 0.277%,对于巨量的 UV 需求来说,这个误差率真的不算高。

当然,除了上面的 PFADD 和 PFCOUNT 之外,还提供了第三个 PFMEGER 指令,用于将多个计数值累加在一起形成一个新的 pf 值:

> PFADD  nosql  "Redis"  "MongoDB"  "Memcached"    // pfadd,将三个字符串添加到变量nosql中,返回1添加陈公
(integer) 1

> PFADD  RDBMS  "MySQL" "MSSQL" "PostgreSQL"
(integer) 1

> PFMERGE  databases  nosql  RDBMS
OK

> PFCOUNT  databases
(integer) 6

六、尾声

《利剑,HyperLogLog之诞生》,完成了。

天天打码,天天进步!!!

工程代码:工程代码链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

祖母绿宝石

打赏一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值