布隆過濾器解決快取穿透問題

我會努力變強的發表於2020-12-01
背景

如果我們使用快取,那樣會帶來快取三大問題,快取穿透、快取雪崩、快取擊穿。這裡針對快取穿透並使用布隆過濾器解決。

快取穿透就是有心使用者利用快取和資料庫都必不存在的資料來傳送惡意請求,從而繞過快取,直接訪問資料庫,最終導致資料庫崩潰的問題。

這是一個通用的問題,關鍵就在於我們怎麼知道請求的 key 在我們的資料庫裡面是否存在,如果資料量特別大的話,我們怎麼去快速判斷。

這個問題是如何在海量元素中(例如 10 億無序、不定長、不重複)快速判斷一個元素是否存在。

這個問題涉及兩個關鍵點:海量資料、快速判斷。

如果我們直接把這些元素的值放到基本的資料結構Set裡面,比如一個元素 1 位元組的欄位,10 億的資料大概需要 900G 的記憶體空間,這個對於普通的伺服器來說是承受不了的。

所以,我們儲存這幾十億個元素,不能直接存值,我們應該找到一種最簡單的最節省空間的資料結構,用來標記這個元素有沒有出現。這個東西我們就把它叫做點陣圖,他是一個有序的陣列,只有兩個值,0 和 1。0 代表不存在,1 代表存在。

要讓這個陣列標記這些元素是否存在,必須有一個對映方法。
這個對映方法需要符合以下基本要求:
1)因為值長度是不固定的,所以希望不同長度的輸入,可以得到固定長度的輸出。
2)轉換成下標的時候,希望在這個有序陣列裡面是分佈均勻的,不然的話全部擠到一對去了,我也沒法判斷到底哪個元素存了,哪個元素沒存。

結合上面兩個要求,使用分佈性優良雜湊函式加上相應取模方法,可以得到相應下標。

在這裡插入圖片描述
具體如上圖,資料經過雜湊計算並取模得到相應的陣列下標,並把該下標值置1,表示存在。
然後到時判斷資料是否存在時,只需要把資料用相應的函式計算出下標,再檢視對應資料元素是否為1,1則存在,0則不存在。

由於會出現雜湊碰撞,此時YaoMing和Kobe Bryant計算出了相同的下標。所以此時使用該方法判斷資料是否存在就會出現誤差,比如假如Kobe Bryant實際上是不存在,但是YaoMing資料已經把下標6的元素置1了,然後Kobe Bryant經過運算得到下標為6,此時他去檢視6元素是否為1,因為YaoMing已經把他置1了,所以會判斷Kobe Bryant是存在,但是實際上它是不存在的。

因為雜湊衝突會導致判斷處弱,所以要儘量減少雜湊衝突的概率。方法有:

  1. 增大點陣圖陣列的容量,因為我們的函式是分佈均勻的,所以,點陣圖容量越大,在同一個位置發生雜湊碰撞的概率就越小,但是點陣圖陣列容量增大意味著會增大記憶體的消耗,所以不能不講道理地擴大點陣圖容量,應該是在錯誤率和點陣圖容量中平衡取值。
  2. 如果資料經過一次雜湊計算,得到的相同下標的概率比較高,所以可以計算多次呢? 原來只用一個雜湊函式,現在對於每一個要儲存的元素都用多個雜湊函式計算,這樣每次計算出來的下標都相同的概率就小得多了。但是 我們也不講道理地使用很多次的雜湊計算函式,因為很多次的雜湊計算會消耗掉cpu的效能,和延長判斷速度。

所以總的來說,我們既要節省空間,又要很高的計算效率,就必須在點陣圖容量和函式個數之間找到一個最佳的平衡。

對於如何取得平衡,這個事情早就有人研究過了,在 1970 年的時候,有一個叫做布隆的前輩對於判斷海量元素中元素是否存在的問題進行了研究,也就是到底需要多大的點陣圖容量和多少個雜湊函式,它發表了一篇論文,提出的這個容器就叫做布隆過濾器。

但是無論如果也不可能達到100%正確率,除非使用絕對均勻的下標演算法和絕對大於元素個數且隨時擴容的位陣列。

所以,這個是布隆過濾器的一個很重要的特性,因為雜湊碰撞不可避免,所以它會存在一定的誤判率。這種把本來不存在布隆過濾器中的元素誤判為存在的情況,我們把它叫做假陽性(False Positive Probability,FPP)。

布隆過濾器的原理就是跟上面講到的原理是一樣的。

布隆過濾器的特點:

容器角度:

  1. 如果布隆過濾器判斷結果為元素存在,那麼該元素實際上元素不一定會存在,由於雜湊碰撞,所以會存在一定誤判率,上面已經說明了。
  2. 如果布隆過濾器判斷結果為元素不存在,那麼他就一定不存在,因為無論雜湊碰撞啥的,只要該元素計算出下標值對應陣列元素值為0,那麼該元素就必定不存在,自己想想就好,只可意會不可言傳。

元素角度:

  1. 如果元素實際不存在,布隆過濾器可能判斷存在。
  2. 如果元素實際存在,布隆過濾器一定判斷存在。

利用第二個特性,我們就能解決持續從資料庫查詢不存在的值的問題,把要查詢的值先過布隆過濾器,判斷是否存在,存在就走redis快取,不存在就直接返回,並且配合快取空值,可以有效解決快取穿透問題,雖然存在一定誤差,但是在業務範圍內允許接受。

在這裡插入圖片描述

  • 第一步先查詢資料庫資料並加入到布隆過濾器中。
  • 請求傳送過來布隆過濾器判斷是否命中,命中就走快取,之後接著看是否走資料庫還是直接從快取獲取返回。
  • 如果布隆過濾器miss,就直接返回,不走cache了。
布隆過濾器實戰:

谷歌的 Guava 裡面就提供了一個現成的布隆過濾器。

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.1-jre</version>
</dependency>

/**
 * @author YeHaocong
 * @decription
 * 測試布隆過濾器的正確判斷和誤判
 *
 * 往布隆過濾器裡面存放100萬個元素
 * 測試100個存在的元素和9900個不存在的元素
 *
 */

public class BloomFilterDemo {

    //元素個數 100萬
    private static final int insertions = 1000000;

    public static void main(String[] args) {
        //建立一個布隆過濾器,第二個值是元素的個數
        // 初始化一個儲存string資料的布隆過濾器,初始化大小為100W
        // 預設誤判率是0.03
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),insertions);

        // 用於存放所有實際存在的key,判斷key是否存在,這個可快速判斷key是否存在
        Set<String> set = new HashSet<>(insertions);

        // 用於存放所有實際存在的key,可以取出使用,這個可供使用下標取出
        List<String> list = new ArrayList<>(insertions);

        //插入資料
        for (int i = 0;i<insertions;i++){
            String uuid = UUID.randomUUID().toString();
            bloomFilter.put(uuid);
            set.add(uuid);
            list.add(uuid);
        }

        int right = 0; // 正確判斷的次數
        int wrong = 0; // 錯誤判斷的次數

        for (int i = 0; i < 10000; i++) {
            // 可以被100整除的時候,取一個存在的數。否則隨機生成一個UUID
            // 0-10000之間,可以被100整除的數有100個(100的倍數)
            //這裡就是實現100個存在key,9900個不存在key。
            String data = i % 100 == 0 ? list.get(i / 100) : UUID.randomUUID().toString();

            //bloomFilter.mightContain(data)   布隆過濾器提供的方法用於判斷資料是否命中
            if (bloomFilter.mightContain(data)) {
                if (set.contains(data)) {
                    // 判斷存在實際存在的時候,命中
                    right++;
                    continue;
                }
                // 判斷存在卻不存在的時候,錯誤
                wrong++;
            }
        }

        //計算命中率和誤判率
        NumberFormat percentFormat =NumberFormat.getPercentInstance();
        percentFormat.setMaximumFractionDigits(2); //最大小數位數
        float percent = (float) wrong / 9900;
        float bingo = (float) (9900 - wrong) / 9900;

        System.out.println("在100W個元素中,判斷100個實際存在的元素,布隆過濾器認為存在的:"+right);
        System.out.println("在100W個元素中,判斷9900個實際不存在的元素,誤認為存在的:"+wrong+"" +
                ",命中率:" + percentFormat.format(bingo) + ",誤判率:" + percentFormat.format(percent) );

    }
}

連續三次的執行結果,誤判率都在3%作用,因為預設的誤判率為3%。
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

可以指定誤判率:

//最後一個引數就是誤判率,這裡設定的是0.1  10%。
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),insertions,0.1D);

在這裡插入圖片描述

布隆過濾器會根據元素個數和誤判率來自動跳轉雜湊函式個數和位陣列的容量。

總結

實際上海量元素快速判斷是否存在、布隆過濾器是一個通用技術,而解決快取穿透只是他適合使用的其中一個場景。

還有其他場景:

比如爬資料的爬蟲,爬過的 url 我們不需要重複爬,那麼在幾十億的 url 裡面,怎麼判斷一個 url 是不是已經爬過了?

還有我們的郵箱伺服器,傳送垃圾郵件的賬號我們把它們叫做 spamer,在這麼多的郵箱賬號裡面,怎麼判斷一個賬號是不是 spamer 等等一些場景,我們都可以用到布隆過濾器。

海量資料的白名單定位等等。

相關文章