1. 程式人生 > 實用技巧 >Redis-避免快取穿透的利器之BloomFilter(轉)

Redis-避免快取穿透的利器之BloomFilter(轉)

你知道的越多,你不知道的也越多

點贊再看,養成習慣

GitHub github.com/java… 上已經開源,有面試點思維導圖,歡迎【Star】【完善】

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

Bloom Filter 概念

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

Bloom Filter 原理

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

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

快取穿透

每次查詢都會直接打到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的計算方式:

(2)雜湊函式選擇

​ 由預估資料量n以及bit陣列長度m,可以得到一個hash函式的個數k:

​ 雜湊函式的選擇對效能的影響應該是很大的,一個好的雜湊函式要能近似等概率的將字串對映到各個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、另外找一萬個數,去檢驗漏網之魚的數量

/**
 * 測試布隆過濾器(可用於redis快取穿透)
 * 
 * @author 敖丙
 */
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);
    }

}
複製程式碼

執行結果:

執行結果表示,遍歷這一百萬個在過濾器中的數時,都被識別出來了。一萬個不在過濾器中的數,誤傷了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方法中,設定一個斷點:

上面的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%左右。

當錯誤率設為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 的大小就能解決同樣的問題。

總結

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


作者:敖丙
連結:https://juejin.im/post/5db69365518825645656c0de
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。