如何用redis統計海量UV?

前言

我們先思考一個常見的業務問題:如果你負責開發維護一個大型的網站,有一天老闆找產品經理要網站每個網頁每天的 UV 數據,然後讓你來開發這個統計模塊,你會如何實現?

統計uv的常用方法以及優缺點

其實要是單純的統計pv是比較好辦的,直接用redis的incr就行,但是uv的話,它要去重,同一個用戶一天之內的多次訪問請求只能計數一次。這就要求每一個網頁請求都需要帶上用戶的 ID,無論是登陸用戶還是未登陸用戶都需要一個唯一 ID 來標識。

set

比較容易想到的是為每一個頁面一個獨立的 set 集合來存儲所有當天訪問過此頁面的用戶 ID。當一個請求過來時,我們使用 sadd 將用戶 ID 塞進去就可以了。通過 scard 可以取出這個集合的大小,這個數字就是這個頁面的 UV 數據。沒錯,這是一個非常簡單的方案。

但是,如果你的頁面訪問量非常大,比如一個爆款頁面幾千萬的 UV,你需要一個很大的 set 集合來統計,這就非常浪費空間。如果這樣的頁面很多,那所需要的存儲空間是驚人的。

hash

hash和set在處理uv的問題上其實類似,把用戶id作為hash的key的確可以去重,但是如果訪問量大了之後也會消耗很大的內存空間

bitmap

bitmap同樣是一種可以統計基數的方法,可以理解為用bit數組存儲元素,例如01101001,表示的是[1,2,4,8],bitmap中1的個數就是基數。bitmap也可以輕鬆合併多個集合,只需要將多個數組進行異或操作就可以了。bitmap相比於Set,Hash也大大節省了內存,我們來粗略計算一下,統計1億個數據的基數,需要的內存是:100000000/8/1024/1024 ≈ 12M。

雖然bitmap在節省空間方面已經有了不錯的表現,但是如果需要統計1000個對象,就需要大約12G的內存,顯然這個結果仍然不能令我們滿意。在這種情況下,HyperLogLog將會出來拯救我們。

HyperLogLog

這就是本節要引入的一個解決方案,Redis 提供了 HyperLogLog 數據結構就是用來解決這種統計問題的。HyperLogLog 提供不精確的去重計數方案,雖然不精確但是也不是非常不精確,標準誤差是 0.81%,這樣的精確度已經可以滿足上面的 UV 統計需求了。

HyperLogLog 數據結構是 Redis 的高級數據結構,它非常有用,但是令人感到意外的是,使用過它的人非常少。

使用方法

Redis 的位數組是自動擴展,如果設置了某個偏移位置超出了現有的內容範圍,就會自動將位數組進行零擴充。

命令

HyperLogLog 提供了兩個指令 pfadd 和 pfcount,根據字面意義很好理解,一個是增加計數,一個是獲取計數。

具體實現

pfadd 用法和 set 集合的 sadd 是一樣的,來一個用戶 ID,就將用戶 ID 塞進去就是。pfcount 和 scard 用法是一樣的,直接獲取計數值。關鍵是它非常省空間,載統計海量uv的時候,只佔用了12k的空間

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
127.0.0.1:6379> pfadd codehole user3
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 3
127.0.0.1:6379> pfadd codehole user4
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 4
127.0.0.1:6379> pfadd codehole user5
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 5
127.0.0.1:6379> pfadd codehole user6
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 6
127.0.0.1:6379> pfadd codehole user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount codehole
(integer) 10

簡單試了一下,發現還蠻精確的,一個沒多也一個沒少。接下來我們使用腳本,往裏面灌更多的數據,看看它是否還可以繼續精確下去,如果不能精確,差距有多大。

我們將數據增加到 10w 個,看看總量差距有多大。

public class JedisTest {
  public static void main(String[] args) {
    Jedis jedis = new Jedis();
    for (int i = 0; i < 100000; i++) {
      jedis.pfadd("codehole""user" + i);
    }
    long total = jedis.pfcount("codehole");
    System.out.printf("%d %d\n", 100000, total);
    jedis.close();
  }
}

跑了約半分鐘,我們看輸出:

> python pftest.py
100000 99723

差了 277 個,按百分比是 0.277%,對於上面的 UV 統計需求來說,誤差率也不算高。然後我們把上面的腳本再跑一邊,也就相當於將數據重複加入一邊,查看輸出,可以發現,pfcount 的結果沒有任何改變,還是 99723,說明它確實具備去重功能。

Tags: