Redis實戰篇:基於HyperLogLog實現UV統計功能

若是如今要開發一個功能:html

統計APP或網頁的一個頁面,天天有多少用戶點擊進入的次數。同一個用戶的反覆點擊進入記爲 1 次,也就是統計 UV 數據。redis

讓你來開發這個統計模塊,你會如何實現?算法

 

若是統計 PV 數據,只要給網頁一個獨立的 Redis 計數器就能夠了,這個計數器的 key 的格式爲 puv:{pid}:{yyyyMMdd}。每來一個請求就 incrby 一次,就能夠統計出全部的 PV 數據。數據結構

 

可是 UV 不同,它要去重,同一個用戶一天以內的屢次訪問請求只能計數一次。這就要求每個網頁請求都須要帶上用戶的 ID,不管是登錄用戶仍是未登錄用戶都須要一個惟一 ID 來標識。ide

 

你可能會立刻想到,用 Hash 數據類型就能知足去重。這確實是一種解決方法,可是當這個頁面的日活達到百萬或千萬以上級別的話,Hash 的內存開銷就會很是大。post

 

咱們來估算一下采用 Hash 的內存空間是多大。假設 key 是 int 類型,對應的是用戶ID,value 是 bool 類型,表示已訪問,當有百萬級不一樣用戶訪問時,內存空間爲:100萬 * (32+8)bit = 40MB優化

 

那有更好的方法嗎?有的,下面來介紹基於 HyperLogLog 的解決方案。首先咱們先來了解一下 HyperLogLogui

 

HyperLogLog

HyperLogLog 的做用是提供不精確的去重計數方案。雖然不精確,但也不是很是不精確,標準偏差是 0.81%,這樣的精確度已經能夠知足上面的 UV 統計需求了。url

 

它的優勢是使用極少的內存就能統計大量的數據,Redis 實現的 HyperLogLog,只須要 12K 內存就能統計 $2^64$ 個數據。遠比 Hash 的內存開銷要少。spa

 

HyperLogLog(HLL) 是一種用於基數計數的機率算法,是基於 LogLog(LLC) 算法的優化和改進,在一樣空間複雜度下,可以比 LLC 的基數估計偏差更小。

 

HyperLogLog 算法的通俗說明:假設咱們爲一個數據集合生成一個8位的哈希串,那麼咱們獲得00000111的機率是很低的,也就是說,咱們生成大量連續的0的機率是很低的。生成連續5個0的機率是1/32,那麼咱們獲得這個串時,能夠估算,這個數據集的基數是32。

 

再深刻的那就是數學公式,可參考本文最後的參考連接前往研究。

 

Redis 中 HLL 的使用

命令 說明 可用版本 時間複雜度
PFADD 添加 >= 2.8.9 O(1)
PFCOUNT 得到基數值 >= 2.8.9 O(1)
PFMERGE 合併多個key >= 2.8.9 O(N)

 

 示例代碼

複製代碼

using StackExchange.Redis;using System;public class PageUVDemo
{    private static IDatabase db;    static void Main(string[] args)
    {
        ConnectionMultiplexer connection = ConnectionMultiplexer.Connect("192.168.0.104:7001,password=123456");

        db = connection.GetDatabase();

        Console.WriteLine("hll:");
        HLLVisit(1000, 1000);
        HLLVisit(10000, 10000);
        HLLVisit(100000, 100000);

        Console.WriteLine("hash:");
        HashVisit(1000, 1000);
        HashVisit(10000, 10000);
        HashVisit(100000, 100000);
        connection.Close();
    }    static void HLLVisit(int times, int pid)
    {        string key = $"puv:hll:{pid}";
        DateTime start = DateTime.Now;        for (int i = 0; i < times; i++)
        {
            db.HyperLogLogAdd(key, i);
        }        long total = db.HyperLogLogLength(key);

        DateTime end = DateTime.Now;

        Console.WriteLine("插入{0}次:", times);
        Console.WriteLine("  total:{0}", total);
        Console.WriteLine("  duration:{0:F2}s", (end - start).TotalSeconds);
        Console.WriteLine();
    }    static void HashVisit(int times, int pid)
    {        string key = $"puv:hash:{pid}";
        DateTime start = DateTime.Now;        for (int i = 0; i < times; i++)
        {
            db.HashSet(key, i, true);
        }        long total = db.HashLength(key);

        DateTime end = DateTime.Now;

        Console.WriteLine("插入{0}次:", times);
        Console.WriteLine("  total:{0}", total);
        Console.WriteLine("  duration:{0:F2}s", (end - start).TotalSeconds);
        Console.WriteLine();
    }
}

複製代碼

 

運行結果

image

 

結果對比

image

 

數據經過 redis-rdb-tools 導出,更多請查看

數據類型 插入次數 內存開銷 時間開銷 偏差率
hash 1000 35KB 3.45s 0%
10000 426KB 34.65s 0%
100000 3880KB 342.36s 0%
hll 1000 2KB 3.57s 0.1%
10000 14KB 33.25s 0.13%
100000 14KB 307.80s 0.44%

 

從上面的結果能夠看出,10萬次級別下,HyperLogLog 的偏差率很低,0.44%,但內存開銷是 Hash 的0.3%,隨着數量級的提高,內存開銷差距也越大。

 

應用場景

  • 統計註冊 IP 數
  • 統計每日訪問 IP 數
  • 統計頁面實時 UV 數
  • 統計在線用戶數
  • 統計用戶天天搜索不一樣詞條的個數

 

總結

不追求百分百的準確度時,使用 HyperLogLog 數據結構能減小內存開銷。

 

參考資料

相關文章
相關標籤/搜索