利剑,HyperLogLog之诞生

1、前言

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

2、HyperLogLog 概要

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

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

方案一,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,对于大数据的场景仍然不适用。golang

方案四,三种几率算法(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 个数据!web

3、HyperLogLog 原理

3.1 从游戏到代码

3.1.1 游戏

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

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

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

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

在这里插入图片描述

这张图的意思是,咱们给定一系列的随机整数,记录从最低位开始,连续零位的最大长度 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

4、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"
5、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
6、面试金手指

之后补充。

7、小结

【Redis 第六篇】HyperLogLog底层解析,好了。

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

工程代码:工程代码连接

相关文章
相关标签/搜索