[轉]:利用bloom filter算法處理大規模數據過濾

Bloom Filter是由Bloom在1970年提出的一種快速查找算法,經過多個hash算法來共同判斷某個元素是否在某個集合內。能夠用於網絡爬蟲的url重複過濾、垃圾郵件的過濾等等。git

它相比hash容器的一個優點就是,不須要存儲元素的實際數據到容器中去來一個個的比較是否存在。
只須要對應的位段來標記是否存在就好了,因此想當節省內存,特別適合海量的數據處理。而且因爲省去了存儲元素和比較操做,因此性能也比基於hash容器的高了不少。github

可是因爲bloom filter沒有去比較元素,只經過多個hash來判斷惟一性,因此存在必定的hash衝突致使誤判。誤判率的大小由hash函數的個數、hash函數優劣、以及存儲的位空間大小共同決定。算法

而且刪除也比較困難,解決辦法是使用其變種,帶計數的bloom filter,這個這裏就很少說了。網絡

對於bloom filter算法的實現,至關簡單:
首先分配一塊固定的連續空間,大小是m個比特位(m/8+1個字節),而後再提供k個不一樣hash函數,同時對每一個元素進行計算位索引。若是每一個位索引對應的位都爲1,則存在該元素,不然就是不存在。dom

能夠看出,若是判斷爲不存在,那麼確定是不存在的,只有在判斷爲存在的時候,纔會存在誤判。函數

bloom filter主要的難點其實在於估算:
保證指定誤判率的狀況下,到底須要多少個hash函數,多少的存儲空間。性能

首先來看下bloom filter的誤判率計算公式:測試

假定有k個hash函數,m個比特位的存儲空間,n個集合元素,則有誤判率p:url

p = (1 - ((1 - 1/ m) ^ kn))^k ~= (1 - e^(-kn/m))^k

根據這個,官方給出了一個計算k的最優解公式,使其知足給定p的狀況下,存儲空間達到最小:spa

k = (m / n) * ln2

把它帶入機率公式獲得:

p = (1 - e ^-((m/nln2)n/m))^(m/nln2)

簡化爲:

lnp = -m/n * (ln2)^2

所以,若是指定p,只須要知足若是公式,就能夠獲得最優解:

s = m/n = -lnp / (ln2 * ln2) = -log2(p) / ln2
k = s * ln2 = -log2(p)

理論值:

p < 0.1: k = 3.321928, m/n = 4.79
p < 0.01: k = 6.643856, m/n = 9.58
p < 0.001: k = 9.965784, m/n = 14.37
p < 0.0001: k = 13.287712, m/n = 19.170117

能夠看出,這個確實可以在保證誤判率的前提下,使其存儲空間達到最小,可是使用的hash函數個數k
相對較多,至少也得4個,要知足p < 0.001,須要10個才行,這個對於字符串hash的計算來說,性能損耗至關大的,實際使用中根本無法接受。

所以咱們須要另一種推到公式,能夠認爲指定p和k的狀況下,來計算空間使用s=m/n的大小,這樣咱們在實際使用的時候,靈活性就大大提升了。

下面來看下,我本身推到出來的公式,首先仍是依據誤判率公式:

p = (1 - e^(-kn/m))^k

假定s=m/n,則有

p = (1 - e^(-k/s))^k

兩邊取導,獲得:

lnp = k * ln(1 - e^(-k/s))

交換k:

(lnp) / k = ln(1 - e^(-k/s))

從新上e:

e^((lnp) / k) = 1 - e^(-k/s)

化簡:

e^(-k/s) = 1 - e^((lnp) / k) = 1 - (e^lnp)^(1/k) = 1 - p^(1/k)

再求導:

-k/s = ln(1 - p^(1/k))

得出:

s = -k / ln(1 - p^(1/k))

假定c = p^(1/k)

s = -k / ln(1 - c)

利用泰勒展開式:ln(1 + x) ~= x - 0.5x^2 while x < 1 化簡獲得:

s ~= -k / (-c-0.5c^2) = 2k / (2c + c * c)

最後得出公式:

c = p^(1/k)
s = m / n = 2k / (2c + c * c)

假定有n=10000000的數據量,則有理論值:

p < 0.1 and k = 1: s = m/n = 9.523810
p < 0.1 and k = 2: s = m/n = 5.461082
p < 0.1 and k = 3: s = m/n = 5.245850, space ~= 6.3MB
p < 0.1 and k = 4: s = m/n = 5.552045, space ~= 6.6MB

p < 0.01 and k = 1: s = m/n = 99.502488
p < 0.01 and k = 2: s = m/n = 19.047619
p < 0.01 and k = 3: s = m/n = 12.570636, space ~= 15MB
p < 0.01 and k = 4: s = m/n = 10.922165, space ~= 13MB

p < 0.001 and k = 1: s = m/n = 999.500250
p < 0.001 and k = 2: s = m/n = 62.261118
p < 0.001 and k = 3: s = m/n = 28.571429, space ~= 34MB
p < 0.001 and k = 4: s = m/n = 20.656961, space ~= 24.6MB

p < 0.0001 and k = 1: s = m/n = 9999.500025
p < 0.0001 and k = 2: s = m/n = 199.004975
p < 0.0001 and k = 3: s = m/n = 63.167063, space ~= 75.3MB
p < 0.0001 and k = 4: s = m/n = 38.095238, space ~= 45.4MB
p < 0.0001 and k = 5: s = m/n = 29.231432, space ~= 24.8MB

能夠看到,在k=3的狀況下,其實已經能夠達到咱們日常使用所能的接受範圍內了,不必非得
使用最優解,除非在空間使用極爲苛刻的狀況下,並且這個公式,針對程序空間使用的調整,更加的靈活智能。

特別提下,通過實測,若是每一個hash的實現很是優質,分佈很均勻的狀況下,其實際的誤判率比理論值低不少:

就拿TBOX的bloom filter的實現作測試,n=10000000:

p < 0.01 and k = 3的狀況下,其實際誤判率爲:0.004965
p < 0.001 and k = 3的狀況下,其實際誤判率爲:0.000967

因此好的hash函數算法也是尤其的重要。

下面來看下TBOX提供的bloom filter的使用,用起來也是至關的方便:

// 總的元素個數
tb_size_t count = 10000000;

/* 初始化bloom filter
 *
 * TB_BLOOM_FILTER_PROBABILITY_0_01: 預約義的誤判率,接近0.01
 * 注:因爲內部使用位移數來表示:1 / 2^6 = 0.015625 ~= 0.01
 * 因此實際傳入的誤判率,有可能稍微大一點,可是仍是至關接近的
 *
 * 3:爲k值,hash函數的個數,最大不超過15個
 *
 * count:指定的元素規模數
 *
 * tb_item_func_long():容器的元素類型,主要是用其內定的hash函數,若是要自定義hash函數,能夠替換:
 *
 * tb_size_t tb_xxxxxx_hash(tb_item_func_t* func, tb_cpointer_t data, tb_size_t mask, tb_size_t index)
 * {
 *      // mask爲hash掩碼,index爲第index個hash算法的索引
 *      return compute_hash(data, index) & mask;
 * }
 *
 * tb_item_func_t func = tb_item_func_long();
 * func.hash = tb_xxxxxx_hash;
 *
 * 來進行
 */
tb_bloom_filter_ref_t filter = tb_bloom_filter_init(TB_BLOOM_FILTER_PROBABILITY_0_01, 3, count, tb_item_func_long());

if (filter)
{
    tb_size_t i = 0;
    for (i = 0; i < count; i++)
    {
        // 產生隨機數
        tb_long_t value = tb_random();
        
        // 設置值到filter內,若是不存在,則返回tb_true表示設置成功
        if (tb_bloom_filter_set(filter, (tb_cpointer_t)value))
        {
             // 添加元素成功,以前元素不存在
             // 不會存在誤判
        }
        else
        {
             // 添加失敗,添加的元素已經存在
             // 這裏可能會存在誤判
        }
        
        // 僅僅判斷元素是否存在
        if (tb_bloom_filter_get(filter, (tb_cpointer_t)data)
        {
             // 元素已經存在
             // 這裏可能會存在誤判
        }
        else
        {
             // 元素不存在
             // 不會存在誤判
        }
    }
    
    // 退出filter
    tb_bloom_filter_exit(filter);
}

// 經常使用預約義的誤判率,也能夠指定其餘值,注:必須是位移數,而不是實際值
typedef enum __tb_bloom_filter_probability_e
{
    TB_BLOOM_FILTER_PROBABILITY_0_1         = 3 ///!< 1 / 2^3 = 0.125 ~= 0.1
,   TB_BLOOM_FILTER_PROBABILITY_0_01        = 6 ///!< 1 / 2^6 = 0.015625 ~= 0.01
,   TB_BLOOM_FILTER_PROBABILITY_0_001       = 10 ///!< 1 / 2^10 = 0.0009765625 ~= 0.001
,   TB_BLOOM_FILTER_PROBABILITY_0_0001      = 13 ///!< 1 / 2^13 = 0.0001220703125 ~= 0.0001
,   TB_BLOOM_FILTER_PROBABILITY_0_00001     = 16 ///!< 1 / 2^16 = 0.0000152587890625 ~= 0.00001
,   TB_BLOOM_FILTER_PROBABILITY_0_000001    = 20 ///!< 1 / 2^20 = 0.00000095367431640625 ~= 0.000001
        
}tb_bloom_filter_probability_e;

相關文章
相關標籤/搜索