思考一個常見的業務問題:如果你負責開發維護一個大型的網站,有一天老板找產品經理要網站每個網頁每天的 UV 數據,然后讓你來開發這個統計模塊,你會如何實現?
如果統計 PV 那非常好辦,給每個網頁一個獨立的 Redis 計數器就可以了,這個計數器的 key 后綴加上當天的日期。這樣來一個請求,incrby 一次,最終就可以統計出所有的 PV
數據。
但是 UV 不一樣,它要去重,同一個用戶一天之內的多次訪問請求只能計數一次。這就要求每一個網頁請求都需要帶上用戶的 ID,無論是登陸用戶還是未登陸用戶都需要一個唯一ID 來標識。
你也許已經想到了一個簡單的方案,那就是為每一個頁面一個獨立的 set 集合來存儲所有當天訪問過此頁面的用戶 ID。當一個請求過來時,我們使用 sadd 將用戶 ID 塞進去就可
以了。通過 scard 可以取出這個集合的大小,這個數字就是這個頁面的 UV 數據。沒錯,這是一個非常簡單的方案。
但是,如果你的頁面訪問量非常大,比如一個爆款頁面幾千萬的 UV,你需要一個很大的 set 集合來統計,這就非常浪費空間。如果這樣的頁面很多,那所需要的存儲空間是驚人
的。為這樣一個去重功能就耗費這樣多的存儲空間,值得么?其實老板需要的數據又不需要太精確,105w 和 106w 這兩個數字對於老板們來說並沒有多大區別,So,有沒有更好的解決方案呢?
Redis 提供了 HyperLogLog 數據結構就是用來解決這種統計問題的。HyperLogLog 提供不精確的去重計數方案,雖然不精確但是也不是非常不
精確,標准誤差是 0.81%,這樣的精確度已經可以滿足上面的 UV 統計需求了。HyperLogLog 數據結構是 Redis 的高級數據結構,它非常有用,但是令人感到意外的是,使用過它的人非常少。
使用方法
HyperLogLog 提供了兩個指令 pfadd 和 pfcount,根據字面意義很好理解,一個是增加計數,一個是獲取計數。pfadd 用法和 set 集合的 sadd 是一樣的,來一個用戶 ID,就將用
戶 ID 塞進去就是。pfcount 和 scard 用法是一樣的,直接獲取計數值。
127.0.0.1:6379> FLUSHDB OK 127.0.0.1:6379> pfadd hll 001 (integer) 1 127.0.0.1:6379> pfadd hll 001 (integer) 0 127.0.0.1:6379> pfadd hll 002 (integer) 1 127.0.0.1:6379> pfadd hll 003 (integer) 1 127.0.0.1:6379> pfadd hll 002 (integer) 0 127.0.0.1:6379> PFCOUNT hll (integer) 3 127.0.0.1:6379>
HyperLogLog類型的基本操作
添加數據
pfadd key element [element ...]
統計數據
pfcount key [key ...]
合並數據
pfmerge destkey sourcekey [sourcekey...]
root@d4cad7fb69c2:/data# redis-cli 127.0.0.1:6379> PFADD hll1 foo bar zap a (integer) 1 127.0.0.1:6379> PFADD hll2 a b c foo (integer) 1 127.0.0.1:6379> PFMERGE hll3 hll1 hll2 OK 127.0.0.1:6379> PFCOUNT hll3 (integer) 6 127.0.0.1:6379>
說明:
- 用於進行基數統計,不是集合,不保存數據,只記錄數量而不是具體數據
- 核心是基數估算算法,最終數值存在一定誤差
- 誤差范圍:基數估計的結果是一個帶有 0.81% 標准錯誤的近似值
- 耗空間極小,每個hyperloglog key占用了12K的內存用於標記基數
- pfadd命令不是一次性分配12K內存使用,會隨着基數的增加內存逐漸增大
- Pfmerge命令合並后占用的存儲空間為12K,無論合並之前數據量多少
pfmerge 適合什么場合用 ?
HyperLogLog 除了上面的 pfadd 和 pfcount 之外,還提供了第三個指令 pfmerge,用於將多個 pf 計數值累加在一起形成一個新的 pf 值。
比如在網站中我們有兩個內容差不多的頁面,運營說需要這兩個頁面的數據進行合並。其中頁面的 UV 訪問量也需要合並,那這個時候 pfmerge 就可以派上用場了。
注意事項
HyperLogLog 這個數據結構不是免費的,不是說使用這個數據結構要花錢,它需要占據一定 12k 的存儲空間,所以它不適合統計單個用戶相關的數據。如果你的用戶上億,可以算
算,這個空間成本是非常驚人的。但是相比 set 存儲方案,HyperLogLog 所使用的空間那真是可以使用千斤對比四兩來形容了。
不過你也不必過於當心,因為 Redis 對 HyperLogLog 的存儲進行了優化,在計數比較小時,它的存儲空間采用稀疏矩陣存儲,空間占用很小,僅僅在計數慢慢變大,稀疏矩陣占
用空間漸漸超過了閾值時才會一次性轉變成稠密矩陣,才會占用 12k 的空間。
HyperLogLog 實現原理
pf 的內存占用為什么是 12k ?
算法中使用了 1024 個桶進行獨立計數,不過在 Redis 的 HyperLogLog實現中用到的是 16384 個桶,也就是 2^14,每個桶的 maxbits 需要 6 個 bits 來存儲,最
大可以表示 maxbits=63,於是總共占用內存就是 2^14 * 6 / 8 = 12k 字節。