一文讀懂BloomFilter

# 前言 你在開發或者面試過程中,有沒有遇到過**海量數據需要查重**,**緩存穿透**怎麼避免等等這樣的問題呢?下面這個東西超屌,好好了解下,面試過關斬將,凸顯你的不一樣。

Bloom Filter 概念

布隆過濾器(英語:Bloom Filter)是1970年由一個叫布隆的小夥子提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。

Bloom Filter 原理

布隆過濾器的原理是,當一個元素被加入集合時,通過K個散列函數將這個元素映射成一個位數組中的K個點,把它們置爲1。檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:如果這些點有任何一個0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。

Bloom Filter跟單哈希函數Bit-Map不同之處在於:Bloom Filter使用了k個哈希函數,每個字符串跟k個bit對應。從而降低了衝突的概率。

img

緩存穿透

每次查詢都會直接打到DB

簡而言之,言而簡之就是我們先把我們數據庫的數據都加載到我們的過濾器中,比如數據庫的id現在有:1、2、3

那就用id:1 爲例子他在上圖中經過三次hash之後,把三次原本值0的地方改爲1

下次數據進來查詢的時候如果id的值是1,那麼我就把1拿去三次hash 發現三次hash的值,跟上面的三個位置完全一樣,那就能證明過濾器中有1的

反之如果不一樣就說明不存在了

那應用的場景在哪裏呢?一般我們都會用來防止緩存擊穿

簡單來說就是你數據庫的id都是1開始然後自增的,那我知道你接口是通過id查詢的,我就拿負數去查詢,這個時候,會發現緩存裏面沒這個數據,我又去數據庫查也沒有,一個請求這樣,100個,1000個,10000個呢?你的DB基本上就扛不住了,如果在緩存裏面加上這個,是不是就不存在了,你判斷沒這個數據就不去查了,直接return一個數據爲空不就好了嘛。

這玩意這麼好使那有啥缺點麼?有的,我們接着往下看

Bloom Filter的缺點

bloom filter之所以能做到在時間和空間上的效率比較高,是因爲犧牲了判斷的準確率、刪除的便利性

  • 存在誤判,可能要查到的元素並沒有在容器中,但是hash之後得到的k個位置上值都是1。如果bloom filter中存儲的是黑名單,那麼可以通過建立一個白名單來存儲可能會誤判的元素。

  • 刪除困難。一個放入容器的元素映射到bit數組的k個位置上是1,刪除的時候不能簡單的直接置爲0,可能會影響其他元素的判斷。可以採用Counting Bloom Filter

Bloom Filter 實現

布隆過濾器有許多實現與優化,Guava中就提供了一種Bloom Filter的實現。

在使用bloom filter時,繞不過的兩點是預估數據量n以及期望的誤判率fpp,

在實現bloom filter時,繞不過的兩點就是hash函數的選取以及bit數組的大小。

對於一個確定的場景,我們預估要存的數據量爲n,期望的誤判率爲fpp,然後需要計算我們需要的Bit數組的大小m,以及hash函數的個數k,並選擇hash函數

(1)Bit數組大小選擇

  根據預估數據量n以及誤判率fpp,bit數組大小的m的計算方式:

img

(2)哈希函數選擇

​ 由預估數據量n以及bit數組長度m,可以得到一個hash函數的個數k:

img

​ 哈希函數的選擇對性能的影響應該是很大的,一個好的哈希函數要能近似等概率的將字符串映射到各個Bit。選擇k個不同的哈希函數比較麻煩,一種簡單的方法是選擇一個哈希函數,然後送入k個不同的參數。

哈希函數個數k、位數組大小m、加入的字符串數量n的關係可以參考Bloom Filters - the mathBloom_filter-wikipedia

要使用BloomFilter,需要引入guava包:

 <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
 </dependency>    

測試分兩步:

1、往過濾器中放一百萬個數,然後去驗證這一百萬個數是否能通過過濾器

2、另外找一萬個數,去檢驗漏網之魚的數量

public class TestBloomFilter {
    private static int total = 1000000;
    private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total);
//    private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total, 0.001);

    public static void main(String[] args) {
        // 初始化1000000條數據到過濾器中
        for (int i = 0; i < total; i++) {
            bf.put(i);
        }

        // 匹配已在過濾器中的值,是否有匹配不上的
        for (int i = 0; i < total; i++) {
            if (!bf.mightContain(i)) {
                System.out.println("有壞人逃脫了~~~");
            }
        }

        // 匹配不在過濾器中的10000個值,有多少匹配出來
        int count = 0;
        for (int i = total; i < total + 10000; i++) {
            if (bf.mightContain(i)) {
                count++;
            }
        }
        System.out.println("誤傷的數量:" + count);
    }
}

運行結果:

img

運行結果表示,遍歷這一百萬個在過濾器中的數時,都被識別出來了。一萬個不在過濾器中的數,誤傷了320個,錯誤率是0.03左右。

看下BloomFilter的源碼:

 public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions) {
        return create(funnel, (long) expectedInsertions);
    }

    public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
        return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
    }

    public static <T> BloomFilter<T> create(
            Funnel<? super T> funnel, long expectedInsertions, double fpp) {
        return create(funnel, expectedInsertions, fpp, BloomFilterStrategies.MURMUR128_MITZ_64);
    }

    static <T> BloomFilter<T> create(
            Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
     ......
    }

BloomFilter一共四個create方法,不過最終都是走向第四個。看一下每個參數的含義:

funnel:數據類型(一般是調用Funnels工具類中的)

expectedInsertions:期望插入的值的個數

fpp 錯誤率(默認值爲0.03)

strategy 哈希算法(我也不懂啥意思)Bloom Filter的應用

在最後一個create方法中,設置一個斷點:

img

img

上面的numBits,表示存一百萬個int類型數字,需要的位數爲7298440,700多萬位。理論上存一百萬個數,一個int是4字節32位,需要481000000=3200萬位。如果使用HashMap去存,按HashMap50%的存儲效率,需要6400萬位。可以看出BloomFilter的存儲空間很小,只有HashMap的1/10左右

上面的numHashFunctions,表示需要5個函數去存這些數字

使用第三個create方法,我們設置下錯誤率:

private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total, 0.0003);

再運行看看:

此時誤傷的數量爲4,錯誤率爲0.04%左右。

img

當錯誤率設爲0.0003時,所需要的位數爲16883499,1600萬位,需要12個函數

和上面對比可以看出,錯誤率越大,所需空間和時間越小,錯誤率越小,所需空間和時間約大

常見的幾個應用場景:

  • cerberus在收集監控數據的時候, 有的系統的監控項量會很大, 需要檢查一個監控項的名字是否已經被記錄到db過了, 如果沒有的話就需要寫入db.

  • 爬蟲過濾已抓到的url就不再抓,可用bloom filter過濾

  • 垃圾郵件過濾。如果用哈希表,每存儲一億個 email地址,就需要 1.6GB的內存(用哈希表實現的具體辦法是將每一個 email地址對應成一個八字節的信息指紋,然後將這些信息指紋存入哈希表,由於哈希表的存儲效率一般只有 50%,因此一個 email地址需要佔用十六個字節。一億個地址大約要 1.6GB,即十六億字節的內存)。因此存貯幾十億個郵件地址可能需要上百 GB的內存。而Bloom Filter只需要哈希表 1/8到 1/4 的大小就能解決同樣的問題。

總結

布隆過濾器主要是在回答道緩存穿透的時候引出來的,文章裏面還是寫的比較複雜了,很多都是網上我看到就複製下來了,大家只要知道他的原理,還有就是知道他的場景能在面試中回答出他的作用就好了。

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