【敲開BAT的大門】系列:避免緩存擊穿的利器之Bloom Filter

引言

在開發或者面試過程中,時常遇到過海量數據需要查詢,秒殺時緩存擊穿怎麼避免等等這樣的問題呢?掌握好本篇介紹的知識點將有助於你在之後的工作、面試中策馬奔騰。

Bloom Filter概念

Bloom Filter,即傳說中的布隆過濾器。它實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。
在這裏插入圖片描述

Bloom Filter的原理

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

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

在這裏插入圖片描述

緩存擊穿

在這裏插入圖片描述
Bloom Filter在避免緩存擊穿中的應用方法:簡而言之就是先把我們數據庫的數據都加載到我們的過濾器中,比如數據庫的id現在有:1,2,3…,n,以上面的原理圖爲例,將id所有值 經過三次hash之後,將hash得到的結果對應的地方由0修改爲1。這樣做之後,每次請求過來通過id查詢數據,如果緩存沒有命中,再在過濾器中查詢,通過同樣的hash算法將請求的id值進行運算,獲得三個索引值,如果有任何一個對應索引的值爲0,說明MySQL中也不存在該id,則直接報錯返回。
試想想這樣做的好處是什麼?假設這樣的一種場景,如果有1000個參數非法請求同時訪問(所謂參數非法是指數據庫也不存在這類的值,比如id全爲負值),緩存中都沒有命中,此時如果這1000個請求同時打到DB,數據庫層是扛不住的,所以此時Bloom Filter就顯得十分必要。

Bloom Filter的缺點

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

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

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

Bloom Filter 實現

在實現Bloom Filter時,繞不過的兩點就是hash函數的選取以及bit數組的大小。
對於一個確定的場景,我們預估要存的數據量爲n,期望的誤判率爲fpp,然後需要計算我們需要的Bit數組的大小m,以及hash函數的個數k,並選擇hash函數。
1 Bit數組大小選擇
  根據預估數據量n以及誤判率fpp,bit數組大小的m的計算方式:
在這裏插入圖片描述
2 哈希函數選擇
​ 由預估數據量n以及bit數組長度m,可以得到一個hash函數的個數k:
在這裏插入圖片描述
3 應用測試
本篇採用的是Google的Bloom Filter,首先需要引入jar包:

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

測試分兩步:

1、往過濾器中放五千萬個數,然後去驗證這五千萬個數是否能順利通過過濾器;

2、另外找一萬個不在過濾器中的數,檢查Bloom Filter誤判的機率。

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

/**
 * @author Carson Chu
 * @date 2020/3/15 14:48
 * @description 布隆過濾器測試樣例
 */
public class BloomFilterTest {
    private static int capacity = 50000000;
    private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), capacity);
//    private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total, 0.001);

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

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

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

在這裏插入圖片描述
運行結果表示,遍歷這五千萬個在過濾器中的數時,都被識別出來了。一萬個不在過濾器中的數,誤傷了297個,誤判率是2.9%左右。
如果想要降低誤判率該怎麼做呢,不要急,源碼爲我們提供了這一機制:

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

    @CheckReturnValue
    public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
        return create(funnel, expectedInsertions, 0.03D);
    }
 @CheckReturnValue
    public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions, double fpp) {
        return create(funnel, (long)expectedInsertions, fpp);
    }

    @CheckReturnValue
    public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions, double fpp) {
        return create(funnel, expectedInsertions, fpp, BloomFilterStrategies.MURMUR128_MITZ_64);
    }
    
    /* create()方法的最底層實現 */
	@VisibleForTesting
    static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions, double fpp, BloomFilter.Strategy strategy) {
        Preconditions.checkNotNull(funnel);
        Preconditions.checkArgument(expectedInsertions >= 0L, "Expected insertions (%s) must be >= 0", new Object[]{expectedInsertions});
        Preconditions.checkArgument(fpp > 0.0D, "False positive probability (%s) must be > 0.0", new Object[]{fpp});
        Preconditions.checkArgument(fpp < 1.0D, "False positive probability (%s) must be < 1.0", new Object[]{fpp});
        Preconditions.checkNotNull(strategy);
        if (expectedInsertions == 0L) {
            expectedInsertions = 1L;
        }

        long numBits = optimalNumOfBits(expectedInsertions, fpp);
        int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);

        try {
            return new BloomFilter(new BitArray(numBits), numHashFunctions, funnel, strategy);
        } catch (IllegalArgumentException var10) {
            throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", var10);
        }
    }

BloomFilter一共四個create方法,不過最終都是走向第四個。看一下每個參數的含義:
funnel:數據類型(一般是調用Funnels工具類中的)
expectedInsertions:期望插入的值的個數
fpp:錯誤率(默認值爲0.03)
strategy:Bloom Filter的算法策略

錯誤率越大,所需空間和時間越小,錯誤率越小,所需空間和時間約大。

Bloom Filter的應用場景

  • cerberus在收集監控數據的時候, 有的系統的監控項量會很大, 需要檢查一個監控項的名字是否已經被記錄到DB過了,如果沒有的話就需要寫入DB;
  • 爬蟲過濾已抓到的url就不再抓,可用Bloom Filter過濾;
  • 垃圾郵件過濾。如果用哈希表,每存儲一億個email地址,就需要1.6GB的內存(用哈希表實現的具體辦法是將每一個email地址對應成一個八字節的信息指紋,然後將這些信息指紋存入哈希表,由於哈希表的存儲效率一般只有 50%,因此一個email地址需要佔用十六個字節。一億個地址大約要 1.6GB,即十六億字節的內存)。因此存貯幾十億個郵件地址可能需要上百GB的內存。而Bloom Filter只需要哈希表 1/8到 1/4 的大小就能解決同樣的問題。

總結

布隆過濾器主要是在解決緩存擊穿問題的時候引出來的,瞭解他的原理並能實習運用,在開發和麪試中都是大有裨益的。

點點關注,不會迷路

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