如果你負責(zé)開發(fā)維護一個大型的網(wǎng)站,有一天老板找產(chǎn)品經(jīng)理要網(wǎng)站每個網(wǎng)頁每天的 UV 數(shù)據(jù),然后讓你來開發(fā)這個統(tǒng)計模塊,你會如何實現(xiàn)?
如果統(tǒng)計 PV (page view)那非常好辦,給每個網(wǎng)頁一個獨立的 Redis 計數(shù)器就可以了,這個計數(shù)器的 key 后綴加上當(dāng)天的日期。這樣來一個請求,incrby 一次,最終就可以統(tǒng)計出所有的 PV數(shù)據(jù)。
但是 UV(unique view) 不一樣,它要去重,同一個用戶一天之內(nèi)的多次訪問請求只能計數(shù)一次。這就要求每一個網(wǎng)頁請求都需要帶上用戶的 ID,無論是登陸用戶還是未登陸用戶都需要一個唯一ID 來標識。
你也許已經(jīng)想到了一個簡單的方案,那就是為每一個頁面一個獨立的 set 集合來存儲所有當(dāng)天訪問過此頁面的用戶 ID。當(dāng)一個請求過來時,我們使用 sadd 將用戶 ID 塞進去就可以了。通過scard 可以取出這個集合的大小,這個數(shù)字就是這個頁面的 UV 數(shù)據(jù)。沒錯,這是一個非常簡單的方案。
但是,如果你的頁面訪問量非常大,比如一個爆款頁面幾千萬的 UV,你需要一個很大的 set 集合來統(tǒng)計,這就非常浪費空間。如果這樣的頁面很多,那所需要的存儲空間是驚人的。為這樣一個去重功能就耗費這樣多的存儲空間,值得么?其實老板需要的數(shù)據(jù)又不需要太精確,105w 和 106w 這兩個數(shù)字對于老板們來說并沒有多大區(qū)別,So,有沒有更好的解決方案呢?
這就是本節(jié)要引入的一個解決方案,Redis 提供了 HyperLogLog 數(shù)據(jù)結(jié)構(gòu)就是用來解決這種統(tǒng)計問題的。HyperLogLog 提供不精確的去重計數(shù)方案,雖然不精確但是也不是非常不精確,標準誤差是 0.81%,這樣的精確度已經(jīng)可以滿足上面的 UV 統(tǒng)計需求了。
HyperLogLog 數(shù)據(jù)結(jié)構(gòu)是 Redis 的高級數(shù)據(jù)結(jié)構(gòu),它非常有用,但是令人感到意外的是,使用過它的人非常少。
使用方法
HyperLogLog 提供了兩個指令 pfadd 和 pfcount,根據(jù)字面意義很好理解,一個是增加計數(shù),一個是獲取計數(shù)。pfadd 用法和 set 集合的 sadd 是一樣的,來一個用戶 ID,就將用戶 ID 塞進去就是。pfcount 和 scard 用法是一樣的,直接獲取計數(shù)值。
127.0.0.1:6379> pfadd codehole user1
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 1
127.0.0.1:6379> pfadd codehole user2
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 2
pfmerge 適合什么場合用 ?
HyperLogLog 除了上面的 pfadd 和 pfcount 之外,還提供了第三個指令 pfmerge,用于將多個 pf 計數(shù)值累加在一起形成一個新的 pf 值。
比如在網(wǎng)站中我們有兩個內(nèi)容差不多的頁面,運營說需要這兩個頁面的數(shù)據(jù)進行合并。其中頁面的 UV 訪問量也需要合并,那這個時候 pfmerge 就可以派上用場了。
注意事項
HyperLogLog 這個數(shù)據(jù)結(jié)構(gòu)不是免費的,不是說使用這個數(shù)據(jù)結(jié)構(gòu)要花錢,它需要占據(jù)一定 12k 的存儲空間,所以它不適合統(tǒng)計單個用戶相關(guān)的數(shù)據(jù)。如果你的用戶上億,可以算算,這個空間成本是非常驚人的。但是相比 set 存儲方案,HyperLogLog 所使用的空間那真是可以使用千斤對比四兩來形容了
不過你也不必過于當(dāng)心,因為 Redis 對 HyperLogLog 的存儲進行了優(yōu)化,在計數(shù)比較小時,它的存儲空間采用稀疏矩陣存儲,空間占用很小,僅僅在計數(shù)慢慢變大,稀疏矩陣占用空間漸漸超過了閾值時才會一次性轉(zhuǎn)變成稠密矩陣,才會占用 12k 的空間。
pf 的內(nèi)存占用為什么是 12k ?
我們在上面的算法中使用了 1024 個桶進行獨立計數(shù),不過在 Redis 的 HyperLogLog實現(xiàn)中用到的是 16384 個桶,也就是 2^14,每個桶的 maxbits 需要 6 個 bits 來存儲,最大可以表示maxbits=63,于是總共占用內(nèi)存就是 2^14 * 6 / 8 = 12k 字節(jié)。