首頁(yè)技術(shù)文章正文

Java培訓(xùn):防緩存穿透利器-布隆濾器(BloomFilter)

更新時(shí)間:2022-10-17 來(lái)源:黑馬程序員 瀏覽量:

IT培訓(xùn)班

  一、布隆過(guò)濾器原理

   如果想要判斷一個(gè)元素是不是在一個(gè)集合中存在,一般的想法是將所有元素保存起來(lái),然后再拿著這個(gè)元素在集合中一個(gè)一個(gè)進(jìn)行比對(duì)。但是隨著集合中元素的增加,我們需要的存儲(chǔ)空間越來(lái)越大,檢索速度也越來(lái)越慢。 針對(duì)這種需要在大量數(shù)據(jù)中去判斷某一個(gè)值是事否存在的情況,1970年由布隆提出了布隆過(guò)濾器的概念。布隆過(guò)濾器本質(zhì)是一個(gè)位數(shù)組,位數(shù)組就是數(shù)組的每個(gè)元素都只占用 1 bit 。每個(gè)元素只能是 0 或者 1。這樣申請(qǐng)一個(gè) 10000 個(gè)元素的位數(shù)組只占用 10000 / 8 = 1250 字節(jié) 的空間。布隆過(guò)濾器除了一個(gè)位數(shù)組,還有 n個(gè)哈希函數(shù)。

   它具體的實(shí)現(xiàn)思想是并不將具體的數(shù)據(jù)存儲(chǔ)在數(shù)組中,而是通過(guò)hash函數(shù)對(duì)要存儲(chǔ)的數(shù)據(jù)進(jìn)行三次hash運(yùn)算,并將hash運(yùn)算的結(jié)果做為位數(shù)組的下標(biāo),將對(duì)應(yīng)的數(shù)組元素修改為1。

   例如:我們要將"oracle"、"database"、"filter"存儲(chǔ)到布隆過(guò)濾器中可以這樣做。如下圖所示

<img src=" 防緩存穿透利器-隆過(guò)濾器(BloomFilter).assets/image-20220716172346986.png" 
alt="image-20220716172346986" style="zoom:67%;" />

   從上圖中可以看到,我們有三個(gè)hash函數(shù)(h1()、h2()、h3())和一個(gè)位數(shù)組,oracle經(jīng)過(guò)三個(gè)hash函數(shù),得到第1、4、5位為1,database同理得到2、5、10位1,這樣如果我們需要判斷oracle是否在此位數(shù)組中,則通過(guò)hash函數(shù)判斷位數(shù)組的1、4、5位是否均為1,如果均為1,則判斷oracle在此位數(shù)組中,database同理。這就是布隆過(guò)濾器判斷元素是否在集合中的原理。

   但同學(xué)們也會(huì)發(fā)現(xiàn),如果我們現(xiàn)在要判斷"mysql"是否存在,例如它通過(guò)三次hash運(yùn)算得到的值分別是4,5,10?,F(xiàn)在即使你的位數(shù)中沒(méi)有存儲(chǔ)“mysql”,布隆過(guò)濾器也會(huì)判斷它存在。這是因?yàn)?quot;oracle"、"database"、"filter"算出的hash值已經(jīng)導(dǎo)致上面的三個(gè)位置的值被改為了1,這樣就會(huì)導(dǎo)致誤判。但是可以保證的是,如果布隆過(guò)濾器判斷一個(gè)元素不在一個(gè)集合中,那這個(gè)元素一定不會(huì)再集合中。

   產(chǎn)生上面誤判的主要原因是hash碰撞導(dǎo)致的。但如果我們將位數(shù)組設(shè)置的足夠大,并且讓hash運(yùn)算執(zhí)行的次數(shù)多一些,這樣就會(huì)降低誤判率。

   布隆過(guò)率器還有另一個(gè)問(wèn)題就是不能刪除。這是因?yàn)樵谖粩?shù)組上的同一個(gè)點(diǎn)有可能有多個(gè)輸入值的映射,如果刪除了會(huì)影響布隆過(guò)濾器里其他元素的判斷結(jié)果。

   所以我們可以總結(jié)出布隆過(guò)濾器的優(yōu)缺點(diǎn)如下:

  - 優(yōu)點(diǎn):

  - 所占空間小(并不存儲(chǔ)真正的數(shù)據(jù)),空間效率高

  - 查詢時(shí)間短

  - 缺點(diǎn):

  - 元素添加到數(shù)組中后,不能被刪除

  - 有一定的誤判率

  二、布隆過(guò)濾器使用場(chǎng)景

  - 解決緩存穿透問(wèn)題,緩存穿透是指查詢一個(gè)一定不存在的數(shù)據(jù),由于緩存是不命中就到數(shù)據(jù)庫(kù)中去查,并且出于容錯(cuò)考慮,如果從數(shù)據(jù)庫(kù)中查不到數(shù)據(jù)則不寫(xiě)入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到數(shù)據(jù)庫(kù)中去查詢,失去了緩存的意義。在流量大時(shí),可能數(shù)據(jù)庫(kù)就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應(yīng)用,這就是漏洞。這時(shí)候可以用布隆過(guò)濾器當(dāng)緩存的索引,只有在布隆過(guò)濾器中,才去查詢緩存,如果沒(méi)查詢到,則再到數(shù)據(jù)庫(kù)中查。如果不在布隆器中,則直接返回。

  - 在業(yè)務(wù)場(chǎng)景中可以用來(lái)判斷用戶是否閱讀過(guò)某些文章或視頻,比如抖音或頭條,當(dāng)然會(huì)導(dǎo)致一定的誤判,但不會(huì)讓用戶看到重復(fù)的內(nèi)容。

  - 黑名單:比如在郵件系統(tǒng)中可以使用布隆器設(shè)置黑名單,判斷郵件地址是否在黑名單中。也可以設(shè)IP黑名單防止網(wǎng)格爬蟲(chóng)等。

  - 垃圾郵件過(guò)濾,從數(shù)十億個(gè)垃圾郵件列表中判斷某郵箱是否垃圾郵箱。

  三、實(shí)踐

  3.1 通過(guò) Java 編程手動(dòng)實(shí)現(xiàn)布隆過(guò)濾器

package com.heima.rbac.controller;

import java.util.BitSet;

public class MyBloomFilter {

    /**
     * 位數(shù)組的大小
     */
    private static final int DEFAULT_SIZE = 2 << 24;
    /**
     * 通過(guò)這個(gè)數(shù)組可以創(chuàng)建 6 個(gè)不同的哈希函數(shù)
     */
    private static final int[] SEEDS = new int[]{5, 17, 48, 73, 93, 132};

    /**
     * 位數(shù)組。數(shù)組中的元素只能是 0 或者 1
     */
    private BitSet bits = new BitSet(DEFAULT_SIZE);

    /**
     * 存放包含 hash 函數(shù)的類的數(shù)組
     */
    private SimpleHash[] func = new SimpleHash[SEEDS.length];

    /**
     * 初始化多個(gè)包含 hash 函數(shù)的類的數(shù)組,每個(gè)類中的 hash 函數(shù)都不一樣
     */
    public MyBloomFilter() {
        // 初始化多個(gè)不同的 Hash 函數(shù)
        for (int i = 0; i < SEEDS.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
        }
    }

    /**
     * 添加元素到位數(shù)組
     */
    public void add(Object value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    /**
     * 判斷指定元素是否存在于位數(shù)組
     */
    public boolean contains(Object value) {
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    /**
     * 靜態(tài)內(nèi)部類。用于 hash 操作!
     */
    public static class SimpleHash {

        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        /**
         * 計(jì)算 hash 值
         */
        public int hash(Object value) {
            int h;
            return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
        }

    }
}

  3.2 Redis 中的布隆過(guò)濾器

  redis 在 4.0 的版本中加入了 module 功能,布隆過(guò)濾器可以通過(guò) module 的形式添加到 redis 中,所以使用 redis 4.0 以上的版本可以通過(guò)加載 [module](https://github.com/RedisLabsModules/rebloom) 來(lái)使用 redis 中的布隆過(guò)濾器。但是這不是最簡(jiǎn)單的方式,使用 docker 可以直接在 redis 中體驗(yàn)布隆過(guò)濾器。

  ```

  > docker run -d -p 6379:6379 --name bloomfilter redislabs/rebloom

  > docker exec -it bloomfilter redis-cli

  ```

  redis 布隆過(guò)濾器主要就兩個(gè)命令:

  - `bf.add` 添加元素到布隆過(guò)濾器中:`bf.add urls http://www.itcast.com`。

  - `bf.exists` 判斷某個(gè)元素是否在過(guò)濾器中:`bf.exists urls http://www.itcast.com`。

  上面說(shuō)過(guò)布隆過(guò)濾器存在誤判的情況,在 redis 中有兩個(gè)值決定布隆過(guò)濾器的準(zhǔn)確率:

  - `error_rate`:允許布隆過(guò)濾器的錯(cuò)誤率,這個(gè)值越低過(guò)濾器的位數(shù)組的大小越大,占用空間也就越大。

  - `initial_size`:布隆過(guò)濾器可以儲(chǔ)存的元素個(gè)數(shù),當(dāng)實(shí)際存儲(chǔ)的元素個(gè)數(shù)超過(guò)這個(gè)值之后,過(guò)濾器的準(zhǔn)確率會(huì)下降。

  redis 中有一個(gè)命令可以來(lái)設(shè)置這兩個(gè)值:

  ```

  bf.reserve urls 0.01 100

  ```

  三個(gè)參數(shù)的含義:

  - 第一個(gè)值是過(guò)濾器的名字。

  - 第二個(gè)值為 `error_rate` 的值。

  - 第三個(gè)值為 `initial_size` 的值。

  使用這個(gè)命令要注意一點(diǎn):執(zhí)行這個(gè)命令之前過(guò)濾器的名字應(yīng)該不存在,如果執(zhí)行之前就存在會(huì)報(bào)錯(cuò):`(error) ERR item exists`。

分享到:
在線咨詢 我要報(bào)名
和我們?cè)诰€交談!