優雅快速的統計千萬級別uv

定義

PV是page view的縮寫,即頁面瀏覽量,一般是衡量一個網絡新聞頻道或網站甚至一條網絡新聞的主要指標。網頁瀏覽數是評價網站流量最經常使用的指標之一,簡稱爲PV程序員

UV是unique visitor的簡寫,是指經過互聯網訪問、瀏覽這個網頁的天然人。sql

經過以上的概念,能夠清晰的看出pv是比較好設計的,網站的每一次被訪問,pv都會增長,可是uv就不必定會增長了,uv本質上記錄的是按照某個標準劃分的天然人,這個標準其實咱們能夠本身去定義,好比:能夠定義同一個IP的訪問者爲同一個UV,這也是最多見的uv定義之一,另外還有根據cookie定義等等。不管是pv仍是uv,都須要一個時間段來加以描述,平時咱們所說的pv,uv數量指的都是24小時以內(一個天然日)的數據。數據庫

pv相比較uv來講,技術上比較容易一些,今天我們就來講一說uv的統計,爲何說uv的統計相對來講比較難呢,由於uv涉及到同一個標準下的天然人的去重,尤爲是一個uv千萬級別的網站,設計一個好的uv統計系統也許並不是想象的那麼容易。編程

那咱們就來設計一個以一個天然日爲時間段的uv統計系統,一個天然人(uv)的定義爲同一個來源IP(固然你也能夠自定義其餘標準),數據量級別假設爲每日千萬uv的量級。數組

注意:今天咱們討論的重點是獲取到天然人定義的信息以後如何設計uv統計系統,並不是是如何獲取天然人的定義。uv系統的設計並不是想象的那麼簡單,由於uv可能隨着網站的營銷策略會出現瞬間大流量,好比網站舉辦了一個秒殺活動。服務器

基於DB方案

服務端編程有一句名言曰:沒有一個表解決不了的功能,若是有那就兩個表三個表。一個uv統計系統確實能夠基於數據庫來實現,並且也不復雜,uv統計的記錄表能夠相似以下(不要太糾結如下表設計是否合理):cookie

字段 類型 描述
IP varchar(30) 客戶端來源ip
DayID int 時間的簡寫,例如 20190629
其餘字段 int 其餘字段描述

當一個請求到達服務器,服務端每次須要查詢一次數據庫是否有當前IP和當前時間的訪問記錄,若是有,則說明是同一個uv,若是沒有,則說明是新的uv記錄,插入數據庫。固然以上兩步也能夠寫到一個sql語句中:網絡

if exists( select 1 from table where ip='ip' and dayid=dayid )
  Begin
    return 0
  End
else
  Begin
     insert into table .......
  End

全部基於數據庫的解決方案,在數據量大的狀況下幾乎都更容易出現瓶頸。面對天天千萬級別的uv統計,基於數據庫的這種方案也許並非最優的。數據結構

優化方案

面對每個系統的設計,咱們都應該沉下心來思考具體的業務。至於uv統計這個業務有幾個特色:異步

1. 每次請求都須要判斷是否已經存在相同的uv記錄

2. 持久化uv數據不能影響正常的業務

3. uv數據的準確性能夠忍受必定程度的偏差

哈希表

基於數據庫的方案中,在大數據量的狀況下,性能的瓶頸引起緣由之一就是:判斷是否已經存在相同記錄,因此要優化這個系統,確定首先是要優化這個步驟。根據菜菜之前的文章,是否能夠想到解決這個問題的數據結構,對,就是哈希表。哈希表根據key來查找value的時間複雜度爲O(1)常數級別,能夠完美的解決查找相同記錄的操做瓶頸。

也許在uv數據量比較小的時候,哈希表也許是個不錯的選擇,可是面對千萬級別的uv數據量,哈希表的哈希衝突和擴容,以及哈希表佔用的內存也許並非好的選擇了。假設哈希表的每一個key和value  佔用10字節,1千萬的uv數據大約佔用 100M,對於現代計算機來講,100M其實不算大,可是有沒有更好的方案呢?

優化哈希表

基於哈希表的方案,在千萬級別數據量的狀況下,只能算是勉強應付,若是是10億的數據量呢?那有沒有更好的辦法搞定10億級數據量的uv統計呢?這裏拋開持久化數據,由於持久化設計到數據庫的分表分庫等優化策略了,我們之後再談。有沒有更好的辦法去快速判斷在10億級別的uv中某條記錄是否存在呢?

爲了儘可能縮小使用的內存,咱們能夠這樣設計,能夠預先分配bit類型的數組,數組的大小是統計的最大數據量的一個倍數,這個倍數能夠自定義調整。如今假設系統的uv最大數據量爲1千萬,系統能夠預先分配一個長度爲5千萬的bit數組,bit佔用的內存最小,只佔用一位。按照一個哈希衝突比較小的哈希函數計算每個數據的哈希值,並設置bit數組相應哈希值位置的值爲1。因爲哈希函數都有衝突,有可能不一樣的數據會出現相同的哈希值,出現誤判,可是咱們能夠用多個不一樣的哈希函數來計算同一個數據,來產生不一樣的哈希值,同時把這多個哈希值的數組位置都設置爲1,從而大大減小了誤判率,剛纔新建的數組爲最大數據量的一個倍數也是爲了減少衝突的一種方式(容量越大,衝突越小)。當一個1千萬的uv數據量級,5千萬的bit數組佔用內存才幾十M而已,比哈希表要小不少,在10億級別下內存佔用差距將會更大。

如下爲代碼示例:

class BloomFilter
    {
        BitArray container = null;
      public BloomFilter(int length)
        {
            container = new BitArray(length);
        }

        public void Set(string key)
        {
            var h1 = Hash1(key);
            var h2 = Hash2(key);
            var h3 = Hash3(key);
            var h4 = Hash4(key);
            container[h1] = true;
            container[h2] = true;
            container[h3] = true;
            container[h4] = true;

        }
        public bool Get(string key)
        {
            var h1 = Hash1(key);
            var h2 = Hash2(key);
            var h3 = Hash3(key);
            var h4 = Hash4(key);

            return container[h1] && container[h2] && container[h3] && container[h4];
        }

        //模擬哈希函數1
         int Hash1(string key)
        {
            int hash = 5381;
            int i;
            int count;
            char[] bitarray = key.ToCharArray();
            count = bitarray.Length;
            while (count > 0)
            {
                hash += (hash << 5) + (bitarray[bitarray.Length - count]);
                count--;
            }
            return (hash & 0x7FFFFFFF) % container.Length;

        }
         int Hash2(string key)
        {
            int seed = 131; // 31 131 1313 13131 131313 etc..
            int hash = 0;
            int count;
            char[] bitarray = (key+"key2").ToCharArray();
            count = bitarray.Length;
            while (count > 0)
            {
                hash = hash * seed + (bitarray[bitarray.Length - count]);
                count--;
            }

            return (hash & 0x7FFFFFFF)% container.Length;
        }
         int Hash3(string key)
        {
            int hash = 0;
            int i;
            int count;
            char[] bitarray = (key + "keykey3").ToCharArray();
            count = bitarray.Length;
            for (i = 0; i < count; i++)
            {
                if ((i & 1) == 0)
                {
                    hash ^= ((hash << 7) ^ (bitarray[i]) ^ (hash >> 3));
                }
                else
                {
                    hash ^= (~((hash << 11) ^ (bitarray[i]) ^ (hash >> 5)));
                }
                count--;
            }

            return (hash & 0x7FFFFFFF) % container.Length;

        }
        int Hash4(string key)
        {
            int hash = 5381;
            int i;
            int count;
            char[] bitarray = (key + "keykeyke4").ToCharArray();
            count = bitarray.Length;
            while (count > 0)
            {
                hash += (hash << 5) + (bitarray[bitarray.Length - count]);
                count--;
            }
            return (hash & 0x7FFFFFFF) % container.Length;
        }
    }

測試程序爲:

BloomFilter bf = new BloomFilter(200000000);
            int exsitNumber = 0;
            int noExsitNumber = 0;

            for (int i=0;i < 10000000; i++)
            {
                string key = $"ip_{i}";
                var isExsit= bf.Get(key);
                if (isExsit)
                {
                    exsitNumber += 1;
                }
                else
                {
                    bf.Set(key);
                    noExsitNumber += 1;
                }
            }
            Console.WriteLine($"判斷存在的數據量:{exsitNumber}");
            Console.WriteLine($"判斷不存在的數據量:{noExsitNumber}");

 測試結果:

判斷存在的數據量:7017
判斷不存在的數據量:9992983

佔用內存40M,誤判率不到 千分之1,在這個業務場景下在可接受範圍以內。在真正的業務當中,系統並不會在啓動之初就分配這麼大的bit數組,而是隨着衝突增多慢慢擴容到必定容量的。

異步優化

當判斷一個數據是否已經存在這個過程解決以後,下一個步驟就是把數據持久化到DB,若是數據量較大或者瞬間數據量較大,能夠考慮使用mq或者讀寫IO比較大的NOSql來代替直接插入關係型數據庫。

思路一轉,整個的uv流程其實也均可以異步化,並且也推薦這麼作。

點關注,不迷路,這是一個程序員都想要關注的公衆號

相關文章
相關標籤/搜索