統計功能是一類極爲常見的需求,好比下面這個場景:redis
要完成這個統計任務,最直觀的方式是使用一個SET
保存頁面在某天的訪問用戶 ID,而後經過對集合求差SDIFF
和求交SINTER
完成統計:數據結構
# 2020-01-01 當日的 UV SADD page:uv:20200101 "Alice" "Bob" "Tom" "Jerry" # 2020-01-02 當日的 UV SADD page:uv:20200102 "Alice" "Bob" "Jerry" "Nancy" # 2020-01-02 新增用戶 SDIFFSTORE page:new:20200102 page:uv:20200102 page:uv:20200101 # 2020-01-02 新增用戶數量 SCARD page:new:20200102 # 2020-01-02 留存用戶 SINTERSTORE page:rem:20200102 page:uv:20200102 page:uv:20200101 # 2020-01-02 留存用戶數量 SCARD page:rem:20200102
優勢:函數
缺點:大數據
SUNION
、SINTER
、SDIFF
計算複雜度高,大數據量狀況下會致使 Redis 實例阻塞,可選的優化方式有:
當用戶 ID 是連續的整數時,可使用BITMAP
實現二值統計:優化
# 2020-01-01 當日的 UV SETBIT page:uv:20200101 0 1 # "Alice" SETBIT page:uv:20200101 1 1 # "Bob" SETBIT page:uv:20200101 2 1 # "Tom" SETBIT page:uv:20200101 3 1 # "Jerry" # 2020-01-02 當日的 UV SETBIT page:uv:20200102 0 1 # "Alice" SETBIT page:uv:20200102 1 1 # "Bob" SETBIT page:uv:20200102 3 1 # "Jerry" SETBIT page:uv:20200102 4 1 # "Nancy" # 2020-01-02 新增用戶 BITOP NOT page:not:20200101 page:uv:20200101 BITOP AND page:new:20200102 page:uv:20200102 page:not:20200101 # 2020-01-02 新增用戶數量 BITCOUNT page:new:20200102 # 2020-01-02 留存用戶 BITOP AND page:rem:20200102 page:uv:20200102 page:uv:20200101 # 2020-01-02 留存用戶數量 BITCOUNT page:new:20200102
優勢:編碼
缺點:spa
前面兩種方式都能提供準確的統計結果,可是也存在如下問題:code
考慮下面這一場景:blog
cardinality counting
針對這一特定的統計場景,Redis 提供了HyperLogLog
類型支持基數統計:內存
# 2020-01-01 當日的 UV PFADD page:uv:20200101 "Alice" "Bob" "Tom" "Jerry" PFCOUNT page:uv:20200101 # 2020-01-02 當日的 UV PFADD page:uv:20200102 "Alice" "Bob" "Tom" "Jerry" "Nancy" PFCOUNT page:uv:20200102 # 2020-01-01 與 2020-01-02 的 UV 總和 PFMERGE page:uv:union page:uv:20200101 page:uv:20200102 PFCOUNT page:uv:union
優勢:
HyperLogLog
計算基數所需的空間是固定的。只須要 12KB 內存就能夠計算接近 \(2^{64}\) 個元素的基數。
缺點:
HyperLogLog
的統計是基於機率完成的,其統計結果是有必定偏差。不適用於精確統計的場景。
HyperLogLog
是一種基於機率的統計方式,該如何理解?
咱們來作一個實驗:不停地拋一個均勻的雙面硬幣,直到結果是正面爲止。
用 0 和 1 分別表示正面與反面,則實驗結果能夠表示爲以下二進制串:
+-+ 第 1 次拋到正面 |1| +-+ +--+ 第 2 次拋到正面 |01| +--+ +---+ 第 3 次拋到正面 |001| +---+ +---------+ 第 k 次拋到正面 |000...001| (總共 k-1 個 0) +---------+
進行 n 實驗後,將每次實驗拋硬幣的次數記爲 \(k_1, k_3,\cdots,k_n\),其中的最大值記爲 \(k_{max}\)。
理想狀況下有 \(k_{max} = log_2(n)\),反過來也能夠經過 \(k_{max}\) 來估計總的實驗次數 \(n = 2^{k_{max}}\)。
實際進行實驗時,極端狀況總會出現,好比在第 1 次實驗時就連續拋出了 10 次反面。
若是按照前面的公式進行估計,會認爲已經進行了 1000 次實驗,這顯然與事實不符。
爲了提升估計的準確性,能夠同時使用 m 枚硬幣進行 分組實驗。
而後計算這 m 組實驗的平均值 \(\hat{k}_{max} = \frac{\sum_{i=0}^{m}{k_{max}}}{m}\),此時能更準確的估計實際的實驗次數 \(\hat{n}=2^{\hat{k}_{max}}\)。
經過前面的分析,咱們能夠總結出如下經驗:
能夠經過二進制串中首個 1 出現的位置 \(k_{max}\) 來估計實際實驗發生的次數 \(n\)
HyperLogLog
借鑑上述思想來統計集合中不重複元素的個數:
bucket
中分別統計
HyperLogLog
實現,使用
8bit 輸出的 hash 函數並以
4 個桶進行分組統計
映射爲二進制串 分組 計算k | | | V V V +---------+ hash("Alice") => |01|101000| => bucket=1, k=1 +---------+ 分組統計 k_max +---------+ hash("Bob") => |11|010010| => bucket=3, k=2 +----------+----------+----------+----------+ +---------+ | bucket_0 | bucket_1 | bucket_2 | bucket_3 | +---------+ ==> +----------+----------+----------+----------+ hash("Tom") => |10|001000| => bucket=2, k=3 | k_max= 1 | k_max= 2 | k_max= 3 | k_max= 2 | +---------+ +----------+----------+----------+----------+ +---------+ hash("Jerry") => |00|111010| => bucket=0, k=1 +---------+ +---------+ hash("Nancy") => |01|010001| => bucket=1, k=2 +---------+
分組計數完成後,用以前的公式估計集合基數爲 \(2^{\hat{k}_{max}}= 2^{(\frac{1+2+3+2}{4})} = 4\)。
在 Redis 的實現中,對於一個輸入的字符串,首先獲得 64 位的 hash 值:
因爲使用了 64 位輸出的 hash 函數,所以能夠計數的集合的基數沒有實際限制。
HyperLogLog
的標準偏差計算公式爲 \(\frac{1.04}{\sqrt{m}}\)(\(m\) 爲分組數量),據此計算 Redis 實現的標準偏差爲 \(0.81\%\)。
下面這幅圖展現了統計偏差與基數大小的關係:
分析該圖能夠得出如下結論: