布隆過濾器Bloom filter基本原理

最近有碰到布隆過濾器,發現redis本身沒有實現它。如果使用需要再安裝這個模。
有興趣的可以自己學習下

先看下介紹

The RedisBloom module provides four data structures: a scalable Bloom filter, a cuckoo filter, a count-min sketch, and a top-k. These data structures trade perfect accuracy for extreme memory efficiency, so they're especially useful for big data and streaming applications.

Bloom and cuckoo filters are used to determine, with a high degree of certainty, whether an element is a member of a set.

A count-min sketch is generally used to determine the frequency of events in a stream. You can query the count-min sketch get an estimate of the frequency of any given event.

A top-k maintains a list of k most frequently seen items.

簡單意思就是:

RedisBloom模塊提供了四種數據結構:可伸縮的Bloom filter、cuckoo filter、count-min sketch和top-k。這些數據結構以完美的準確性換取了極高的內存效率,因此它們對於大數據和流媒體應用程序特別有用。

Bloom和cuckoo過濾器用於高度精確地確定一個元素是否屬於一個集合的成員。

count-min sketch 通常用於確定流中事件的頻率。您可以查詢count-min sketch,以獲得對任何給定事件的頻率的估計。

top-k維護一個包含k個最常見項的列表。

簡單來說就是,利用少量內存完成大量數據處理。

下面逐個瞭解下

Bloom filter

使用場景

判斷一個元素在不在一個集合中,我們可以用hashset,複雜度O(1)即可判斷。
但是當數據量很大時,需要的存儲很大。
可以考慮使用bit位來判斷,但是又會有hash衝突的問題。爲了降低衝突率,可以採用多個hash 來實現。

基本原理

Bloom filter 本質是一個bit數組。每個元素都只佔用 1 bit,每個元素只能是 0 或者 1。
對於一個key,利用 k 個hash 函數進行映射得到 n1, n2, n3。然後標記數組下標 n1, n2, n3 的位置爲 1。

因爲存在哈希衝突,即某個bit位已經因爲其他key變成1。因此當一個key 映射的所有位置都已經是1,此時無法判斷當前key是否存在。當然,如果一個key 映射的所有位置不全是1, 則說明當前key 不存在。即所謂的假正

例如,如果 "hello" 的映射爲{1,4,9},此時發現這三個位置都是1,則不一定能準確判斷出 "hello" 存在集合中。

 

公式

當hash函數個數 k = (ln2) * (m/n) 時錯誤率最小。
在錯誤率不大於e的情況下,則 m >= n*log2(1/e)*log2e 
具體可以參考論文

 

使用方法

bf.reserve your_key  0.01 100

bf.reserve 有三個參數,分別是 key, error_rate 和 initial_size 。
從上邊的公式不難看出:error_rate 錯誤率越低,需要的空間越大。
initial_size 參數表示預計放 入的元素數量,當實際數量超出這個數值時,誤判率會上升。
所以需要提前設置一個較大的數值避免超出導致誤判率升高。
默認情況下 error_rate 是 0.01,默認的 initial_size 是 100。

代碼實現

google的guava包中提供了BloomFilter類,直接用的是服務器內存

待續

 

優缺點

優點:存儲數據量小,節省存儲及計算空間
缺點:只能對集合添加元素,無法刪除(也並非完全不能,可以使用 bloom filter 的變種 CounterBloom Filte,它將Bloom Filter每一個Bit改爲一個計數器);存在誤報存在的可能,即false positive rate (假正率)。

cuckoo filter

使用場景

適合大併發的讀, 少量的寫的應用場景。 而且空間利用率可以做到很高。cuckoo filter裏不會存儲原信息,但會存儲指紋信息。因此,相比於Bloom filter,它既可以確保該元素存在的必然性,又可以在不違背此前提下刪除任意元素,僅僅比bitmap犧牲了微量空間效率。

基本原理

一個 key 如果哈希到某個位置,發現該位置上已經有元素 key_1 了,則把 key_1 移到 另一個位置,然後把key 放入騰出的位置上,並記錄碰撞次數。如果另一個位置上還有元素,再進行移動,循環進行。
如果碰撞次數達到某個閾值則認爲過濾器容量不足,需要對其進行 rehash 擴容。

這個過程類似於布穀鳥下蛋的過程,所以稱其爲布穀鳥過濾器。

問題:因爲桶中沒有存原有信息,因此計算備選桶就成了問題,解決方法如下:

即利用當前桶的位置和當前指紋的哈希值,進行異或。

參考文章PPT

公式

在保證內存利用率高的情況下,存儲n個元素,則需要 Ω(logn/b)  bit位存指紋,其中 b 爲桶中的槽位數

實現

這裏引用論文文章裏給出的代碼實現

 

 

count-min sketch

使用場景

主要用於實時統計數據流中元素出現的頻率,能判斷某個元素出現的頻率,近似值。

在數據量小的時候,可以用hashmap,但是當數據量非常大時,就需要很大的存儲了,不可行。而count-min sketch只存儲它們Sketch的計數,因此可行。

基本原理

一個長度爲 x 的數組,用來計數,初始每個元素的計數值爲 0。當某個元素出現時,hash 到某個索引 i, 則對 x[i] 加 1 。

考慮存在哈希衝突,定義 k 個長度爲 x 的數組,和 k 個哈希函數。

 

更新時,對 k 個哈希函數算的位置上的值加 1。 即圖中行的Ct 都加 1 

查詢頻率時,對k個哈希函數算出的位置上的值,取最小。即 min( Ct1, Ct2, ... Ctk)

公式

理論及 w 和 k 的取值,參考論文

前面提到的 redisBloom 代碼裏,實現如下:

CMSketch *NewCMSketch(size_t width, size_t depth) {
    assert(width > 0);
    assert(depth > 0);

    CMSketch *cms = CMS_CALLOC(1, sizeof(CMSketch));

    cms->width = width;
    cms->depth = depth;
    cms->counter = 0;
    cms->array = CMS_CALLOC(width * depth, sizeof(uint32_t));

    return cms;
}

根據參數 width, depth 申請一個長度爲  width * depth 的數組,來代替上邊說的二維哈希結構。

插入元素時,循環 hash 函數個數,即depth 次

size_t CMS_IncrBy(CMSketch *cms, const char *item, size_t itemlen, size_t value) {
    assert(cms);
    assert(item);

    size_t maxCount = 0;

    for (size_t i = 0; i < cms->depth; ++i) {
        uint32_t hash = CMS_HASH(item, itemlen, i);
        cms->array[(hash % cms->width) + (i * cms->width)] += value;
        maxCount = max(maxCount, cms->array[(hash % cms->width) + (i * cms->width)]);
    }
    cms->counter += value;
    return maxCount;
}

查詢時,則循環 depth 次,最後取 最小值;

size_t CMS_Query(CMSketch *cms, const char *item, size_t itemlen) {
    assert(cms);
    assert(item);

    size_t temp = 0, res = (size_t)-1;

    for (size_t i = 0; i < cms->depth; ++i) {
        uint32_t hash = CMS_HASH(item, itemlen, i);
        temp = cms->array[(hash % cms->width) + (i * cms->width)];
        if (temp < res) {
            res = temp;
        }
    }

    return res;
}

 

優點:只需要固定大小的內存和計算時間,和需要統計的元素多少無關;
缺點:只會估算偏大,永遠不會偏小;對於低頻的元素,估算值相對的錯誤可能會很大。

個人感覺它是 HyperLogLog 的一個補充,HyperLogLog 只能 統計去重的數據量,但是不能計算頻率。

對此,文章《New estimation algorithms for streaming data: Count-min can do more》提出了 Count-Mean-Min Sketch。在查詢時利用降噪來提高準確率。

具體:
插入:和Count-min Sketch一樣;
查詢:假設查詢到 d 個hash函數在 d * w 的數組中,查到的數量分別時 Ct1,Ct2,.. Ctd,對每行,除去Ctd, 剩下的元素均勻分佈,則噪音爲該行剩餘元素的平均值,因此Ctd 降噪後應該是 Ctd - mean(該行剩餘元素)。
然後取 d 個 降噪後的中位數,即頻次爲 median(Ctd - mean(該行剩餘元素))

當然還有其他變種的Sketch,可以參考文章

 

top-k

使用場景

top-k 有基本的top-k 和 top-k 頻繁項之分

基本的top-k顧名思義,即海量數據取最大或最小的K個值。

可能想到的方案:

1.直接排序後取前 k 個。複雜度 O(n logn) ,當一億條數據時,就不可行了。

2.構建 k 個元素的大頂堆(小頂堆),遍歷所有元素,最後堆裏的元素排序輸出。O(k * logk) + O(n * logk)。如果不需要排序即O(n * logk)

3.使用快排中的partition,即BFPRT。利用隨機某個元素key 對n個元素分組,大於等於 key 的集合爲SL,小於 key 的爲SM。若SL 大於k個元素,則在 SL中遞歸找,否則在 SM 中找 k - len(SL) 個元素。同過遞歸將問題分解。複雜度O(n)

這就是平常所說的Top-K問題

而標題的Top-K 頻繁項,則是尋找數據流中出現最頻繁的k個元素,即 find top k frequent items in a data stream。

方案
1.用一個容量爲k的小頂堆,維護目前最頻繁的前 k 個元素,同時用一個HashMap 維護元素與出現次數的映射。
2.元素出現,則把 HashMap 的計數加 1(不存在則初始爲 1)
3.同時在堆裏查找該元素:a)找到則把堆裏的計數加 1,並調整堆。b)找不到,則將當前元素出現次數與堆頂比較,如果比堆頂大,則替換,並調整堆。

空間複雜度:O(n)
時間複雜度:每次查找堆 O(k),調整堆 O(logk)。n個元素下來,複雜度 O(n*(k+logk))。k比較小,即 O(n)

海量數據時,就不可行了,除非利用多臺機器分解。

基本原理

Top-K 則是對上邊方案的改進,將 HashMap 維護計數 改成 Count-Min Sketch 維護計數。

空間複雜度:只需要一個非常小的數組,加 O(k) 的堆
時間複雜度:同上
具體理論可以參考論文《HeavyKeeper: An Accurate Algorithm for Finding Top-k Elephant Flows》

 

優缺點

 

參考:

《流數據庫 概率計算概念 - PipelineDB-Probabilistic Data Structures & Algorithms》

《Probabilistic Data Structures for Web Analytics and Data Mining》

《Approximating Data with the Count-Min Data Structure》

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章