利劍,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底層解析,好了。

每天打碼,每天進步!!!

工程代碼:工程代碼連接

相關文章
相關標籤/搜索