布隆過濾器(Bloom Filter)詳解

今天在看Redis緩存穿透問題時裏面講到了布隆過濾器,研究了一番,總結一下。

一、什麼是布隆過濾器

布隆過濾器,Bloom Filter是1970年由Bloom提出的,它是由一組哈希(Hash)函數和一個位陣列組成。布隆過濾器可以用於查詢一個元素是否存在於一個集合當中,查詢結果爲以下二者之一:

  • 這個元素可能存在於這個集合當中。
  • 這個元素一定不存在於這個集合當中。

布隆過濾器的優點是空間效率和查詢時間都比一般的算法要好的多,缺點是有一定的誤識別率和刪除困難。

布隆過濾器在實際中主要用來解決網頁URL去重複,垃圾郵件檢測,大集合中重複元素判斷和緩存擊穿等問題。

二、設計原理

如果想判斷一個元素是不是在一個集合裏,一般想到的是將所有元素保存起來,然後通過比較確定。鏈表,樹等等數據結構都是這種思路. 但是隨着集合中元素的增加,我們需要的存儲空間越來越大,檢索速度也越來越慢(O(n),O(logn))。

這時候我們可以利用哈希表這種數據結構,基於哈希函數的特性,它在理想情況下(不發生哈希衝突),檢索速度可以達到O(1)。一張哈希表的示意圖如下所示:

哈希函數是這樣的:

hashcode = H(key)

這裏的hashcode就是哈希桶的索引,如何哈希函數H足夠完美,那麼每個key就會對應一個唯一的hashcode,但實際上往往會出現哈希衝突,即兩個不同的key對應同一個hashcode如下圖所示,“John Smith” 和 “Sandra Dee” 經過哈希之後得到了相同的哈希值“02”。

布隆過濾器使用了上面的思路,即利用哈希表這個數據結構,通過一個Hash函數將一個元素映射成一個位陣列(Bit array)中的一個點(每個點只能表示0或者1),這樣一來,我們只要看看這個點是不是1就知道在集合中有沒有它了。這就是Bloom Filter的基本思想。

但是在哈希衝突的情況下,我們無法使用一個哈希函數來判斷一個元素是否存在於集合之中,解決方法也簡單,就是使用多個哈希函數,如果其中有一個哈希函數判斷該元素不在集合中(元素經過Hash之後映射在位陣列中的點爲0),那肯定就不在。如果它們都判斷存在,那也有一定可能性它們都在說謊,不過這要比只用一個哈希函數來判斷“一個元素存在於集合之中”的可靠性要高很多。這種多個Hash組成的數據結構就叫Bloom Filter。

一個Bloom Filter是基於一個m位的位陣列(b1,…bm),這些位陣列的初始值爲0。另外,還有一系列的hash函數(h1,…hk),這些hash函數的值域屬於1~m。下圖是一個bloom filter插入x,y,z並判斷某個值w是否在該數據集的示意圖:

上圖中,m=18,k=3;

插入x時,三個hash函數分別得到藍線對應的三個值,並將對應的位向量改爲1,插入y,z時,類似的,分別將紅線,紫線對應的位向量改爲1。

查找時,當查找x時,三個hash值對應的位向量都爲1,因此判斷x在此數據集中。y,z也是如此。但是當查找w時,w有個hash值對應的位向量爲0,因此可以判斷不在此集合中。但是,假如w的最後那個hash值比上圖中的大1,這是就會認爲w在此集合中,而事實上,w可能不在此集合中,因此可能出現誤報。顯然的,插入數據越多,1的位數越多,誤報的概率越大。

產生誤報的原因是由於哈希碰撞導致的巧合而將不同的元素存儲在相同的比特位上。幸運的是,布隆過濾器有一個可預測的誤判率(FPP):

 preview

n 是已經添加元素的數量;
k 哈希的次數;
m 布隆過濾器的長度(如比特數組的大小);

極端情況下,當布隆過濾器沒有空閒空間時(滿),每一次查詢都會返回 true 。這也就意味着 m 的選擇取決於期望預計添加元素的數量 n ,並且 m 需要遠遠大於 n 。

實際情況中,布隆過濾器的長度 m 可以根據給定的誤判率(FFP)的和期望添加的元素個數 n 的通過如下公式計算:

preview

對於 m/n 比率表示每一個元素需要分配的比特位的數量,也就是哈希函數 k 的數量可以調整誤判率。通過如下公式來選擇最佳的 k 可以減少誤判率(FPP):

preview

瞭解完上述的內容之後,我們可以得出一個結論,當我們搜索一個值的時候,若該值經過 K 個哈希函數運算後的任何一個索引位爲 ”0“,那麼該值肯定不在集合中。但如果所有哈希索引值均爲 ”1“,則只能說該搜索的值可能存在集合中。

三、布隆過濾器應用

在實際工作中,布隆過濾器常見的應用場景如下:

  • 網頁爬蟲對 URL 去重,避免爬取相同的 URL 地址;
  • 反垃圾郵件,從數十億個垃圾郵件列表中判斷某郵箱是否垃圾郵箱;
  • Google Chrome 使用布隆過濾器識別惡意 URL;
  • Medium 使用布隆過濾器避免推薦給用戶已經讀過的文章;
  • Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆過濾器減少對不存在的行和列的查找。

除了上述的應用場景之外,布隆過濾器還有一個應用場景就是解決緩存穿透的問題。所謂的緩存穿透就是服務調用方每次都是查詢不在緩存中的數據,這樣每次服務調用都會到數據庫中進行查詢,如果這類請求比較多的話,就會導致數據庫壓力增大,這樣緩存就失去了意義。

利用布隆過濾器我們可以預先把數據查詢的主鍵,比如用戶 ID 或文章 ID 緩存到過濾器中。當根據 ID 進行數據查詢的時候,我們先判斷該 ID 是否存在,若存在的話,則進行下一步處理。若不存在的話,直接返回,這樣就不會觸發後續的數據庫查詢。需要注意的是緩存穿透不能完全解決,我們只能將其控制在一個可以容忍的範圍內。

四、代碼實踐

布隆過濾器有很多實現和優化,由 Google 開發著名的 Guava 庫就提供了布隆過濾器(Bloom Filter)的實現。在基於 Maven 的 Java 項目中要使用 Guava 提供的布隆過濾器,只需要引入以下座標:

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>28.0-jre</version>
</dependency>

在導入 Guava 庫後,我們新建一個 BloomFilterDemo 類,在 main 方法中我們通過 BloomFilter.create 方法來創建一個布隆過濾器,接着我們初始化 1 百萬條數據到過濾器中,然後在原有的基礎上增加 10000 條數據並判斷這些數據是否存在布隆過濾器中:

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterDemo {
    public static void main(String[] args) {
        int total = 1000000; // 總數量
        BloomFilter<CharSequence> bf = 
          BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total);
        // 初始化 1000000 條數據到過濾器中
        for (int i = 0; i < total; i++) {
            bf.put("" + i);
        }
        // 判斷值是否存在過濾器中
        int count = 0;
        for (int i = 0; i < total + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配數量 " + count);
    }
}

當以上代碼運行後,控制檯會輸出以下結果:

已匹配數量 1000309

很明顯以上的輸出結果已經出現了誤報,因爲相比預期的結果多了 309 個元素,誤判率爲:

309/(1000000 + 10000) * 100 ≈ 0.030594059405940593

如果要提高匹配精度的話,我們可以在創建布隆過濾器的時候設置誤判率 fpp:

BloomFilter<CharSequence> bf = BloomFilter.create(
  Funnels.stringFunnel(Charsets.UTF_8), total, 0.0002
);

在 BloomFilter 內部,誤判率 fpp 的默認值是 0.03:

// com/google/common/hash/BloomFilter.class
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
  return create(funnel, expectedInsertions, 0.03D);
}

在重新設置誤判率爲 0.0002 之後,我們重新運行程序,這時控制檯會輸出以下結果:

已匹配數量 1000003

通過觀察以上的結果,可知誤判率 fpp 的值越小,匹配的精度越高。當減少誤判率 fpp 的值,需要的存儲空間也越大,所以在實際使用過程中需要在誤判率和存儲空間之間做個權衡。

五、總結

Bloom Filter有以下幾個特點:

  • 不存在漏報(False Negative),即某個元素在某個集合中,肯定能報出來。
  • 可能存在誤報(False Positive),即某個元素不在某個集合中,可能也被爆出來。
  • 確定某個元素是否在某個集合中的代價和總的元素數目無關。

優點:

相比於其它的數據結構,Bloom Filter在空間和時間方面都有巨大的優勢。Bloom Filter存儲空間和插入/查詢時間都是常數。另外, Hash函數相互之間沒有關係,方便由硬件並行實現。Bloom Filter不需要存儲元素本身,在某些對保密要求非常嚴格的場合有優勢。

缺點:

一般情況下不能從Bloom Filter中刪除元素. 我們很容易想到把位列陣變成整數數組,每插入一個元素相應的計數器加1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全的刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在Bloom Filter裏面. 這一點單憑這個過濾器是無法保證的。另外計數器迴繞也會造成問題。

參考資料

https://en.wikipedia.org/wiki/Bloom_filter

https://zh.wikipedia.org/wiki/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8

http://www.sigma.me/2011/09/13/hash-and-bloom-filter.html

https://segmentfault.com/a/1190000021136424

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