【搞定面試官】系列:避免緩存穿透的利器之Bloom Filter

引言

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

Bloom Filter概念

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

Bloom Filter的原理

布隆過濾器的原理是,<font color="red">當一個元素被加入集合時,經過K個散列函數將這個元素映射成一個位數組中的K個點,把它們置爲1。檢索時,咱們只要看看這些點是否是都是1就(大約)知道集合中有沒有它了:若是這些點有任何一個0,則被檢元素必定不在;若是都是1,則被檢元素極可能在。</font>這就是布隆過濾器的基本思想。算法

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,則直接報錯返回。
<font color="#E96900">試想一想這樣作的好處是什麼?假設這樣的一種場景,若是有1000個參數非法請求同時訪問(所謂參數非法是指數據庫也不存在這類的值,好比id全爲負值),緩存中都沒有命中,此時若是這1000個請求同時打到DB,數據庫層是扛不住的,因此此時Bloom Filter就顯得十分必要。</font>數據庫

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>

測試分兩步:函數

一、往過濾器中放五千萬個數,而後去驗證這五千萬個數是否能順利經過過濾器;工具

二、另外找一萬個不在過濾器中的數,檢查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%左右。
若是想要下降誤判率該怎麼作呢,不要急,源碼爲咱們提供了這一機制:google

@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 的大小就能解決一樣的問題。

總結

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

點點關注,不會迷路

相關文章
相關標籤/搜索