布隆過濾器,這一篇給你講的明明白白

什麼是 BloomFilter

布隆過濾器(英語:Bloom Filter)是 1970 年由布隆提出的。它其實是一個很長的二進制向量和一系列隨機映射函數。主要用於判斷一個元素是否在一個集合中。html

一般咱們會遇到不少要判斷一個元素是否在某個集合中的業務場景,通常想到的是將集合中全部元素保存起來,而後經過比較肯定。鏈表、樹、散列表(又叫哈希表,Hash table)等等數據結構都是這種思路。可是隨着集合中元素的增長,咱們須要的存儲空間也會呈現線性增加,最終達到瓶頸。同時檢索速度也愈來愈慢,上述三種結構的檢索時間複雜度分別爲$O(n)$,$O(logn)$,$O(1)$。java

這個時候,布隆過濾器(Bloom Filter)就應運而生。git

布隆過濾器原理

瞭解布隆過濾器原理以前,先回顧下 Hash 函數原理。程序員

哈希函數

哈希函數的概念是:將任意大小的輸入數據轉換成特定大小的輸出數據的函數,轉換後的數據稱爲哈希值或哈希編碼,也叫散列值。下面是一幅示意圖:github

全部散列函數都有以下基本特性:面試

  • 若是兩個散列值是不相同的(根據同一函數),那麼這兩個散列值的原始輸入也是不相同的。這個特性是散列函數具備肯定性的結果,具備這種性質的散列函數稱爲單向散列函數redis

  • 散列函數的輸入和輸出不是惟一對應關係的,若是兩個散列值相同,兩個輸入值極可能是相同的,但也可能不一樣,這種狀況稱爲「散列碰撞(collision)」。算法

可是用 hash表存儲大數據量時,空間效率仍是很低,當只有一個 hash 函數時,還很容易發生哈希碰撞。sql

布隆過濾器數據結構

BloomFilter 是由一個固定大小的二進制向量或者位圖(bitmap)和一系列映射函數組成的。docker

在初始狀態時,對於長度爲 m 的位數組,它的全部位都被置爲0,以下圖所示:

當有變量被加入集合時,經過 K 個映射函數將這個變量映射成位圖中的 K 個點,把它們置爲 1(假定有兩個變量都經過 3 個映射函數)。

查詢某個變量的時候咱們只要看看這些點是否是都是 1 就能夠大機率知道集合中有沒有它了

  • 若是這些點有任何一個 0,則被查詢變量必定不在;
  • 若是都是 1,則被查詢變量很可能存在

爲何說是可能存在,而不是必定存在呢?那是由於映射函數自己就是散列函數,散列函數是會有碰撞的。

誤判率

布隆過濾器的誤判是指多個輸入通過哈希以後在相同的bit位置1了,這樣就沒法判斷到底是哪一個輸入產生的,所以誤判的根源在於相同的 bit 位被屢次映射且置 1。

這種狀況也形成了布隆過濾器的刪除問題,由於布隆過濾器的每個 bit 並非獨佔的,頗有可能多個元素共享了某一位。若是咱們直接刪除這一位的話,會影響其餘的元素。(好比上圖中的第 3 位)

特性

  • 一個元素若是判斷結果爲存在的時候元素不必定存在,可是判斷結果爲不存在的時候則必定不存在
  • 布隆過濾器能夠添加元素,可是不能刪除元素。由於刪掉元素會致使誤判率增長。

添加與查詢元素步驟

添加元素

  1. 將要添加的元素給 k 個哈希函數
  2. 獲得對應於位數組上的 k 個位置
  3. 將這k個位置設爲 1

查詢元素

  1. 將要查詢的元素給k個哈希函數
  2. 獲得對應於位數組上的k個位置
  3. 若是k個位置有一個爲 0,則確定不在集合中
  4. 若是k個位置所有爲 1,則可能在集合中

優勢

相比於其它的數據結構,布隆過濾器在空間和時間方面都有巨大的優點。布隆過濾器存儲空間和插入/查詢時間都是常數 $O(K)$,另外,散列函數相互之間沒有關係,方便由硬件並行實現。布隆過濾器不須要存儲元素自己,在某些對保密要求很是嚴格的場合有優點。

布隆過濾器能夠表示全集,其它任何數據結構都不能;

缺點

可是布隆過濾器的缺點和優勢同樣明顯。誤算率是其中之一。隨着存入的元素數量增長,誤算率隨之增長。可是若是元素數量太少,則使用散列表足矣。

另外,通常狀況下不能從布隆過濾器中刪除元素。咱們很容易想到把位數組變成整數數組,每插入一個元素相應的計數器加 1, 這樣刪除元素時將計數器減掉就能夠了。然而要保證安全地刪除元素並不是如此簡單。首先咱們必須保證刪除的元素的確在布隆過濾器裏面。這一點單憑這個過濾器是沒法保證的。另外計數器迴繞也會形成問題。

在下降誤算率方面,有很多工做,使得出現了不少布隆過濾器的變種。

布隆過濾器使用場景和實例

在程序的世界中,布隆過濾器是程序員的一把利器,利用它能夠快速地解決項目中一些比較棘手的問題。

如網頁 URL 去重、垃圾郵件識別、大集合中重複元素的判斷和緩存穿透等問題。

布隆過濾器的典型應用有:

  • 數據庫防止穿庫。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter來減小不存在的行或列的磁盤查找。避免代價高昂的磁盤查找會大大提升數據庫查詢操做的性能。

  • 業務場景中判斷用戶是否閱讀過某視頻或文章,好比抖音或頭條,固然會致使必定的誤判,但不會讓用戶看到重複的內容。

  • 緩存宕機、緩存擊穿場景,通常判斷用戶是否在緩存中,若是在則直接返回結果,不在則查詢db,若是來一波冷數據,會致使緩存大量擊穿,形成雪崩效應,這時候能夠用布隆過濾器當緩存的索引,只有在布隆過濾器中,纔去查詢緩存,若是沒查詢到,則穿透到db。若是不在布隆器中,則直接返回。

  • WEB攔截器,若是相同請求則攔截,防止重複被攻擊。用戶第一次請求,將請求參數放入布隆過濾器中,當第二次請求時,先判斷請求參數是否被布隆過濾器命中。能夠提升緩存命中率。Squid 網頁代理緩存服務器在 cache digests 中就使用了布隆過濾器。Google Chrome瀏覽器使用了布隆過濾器加速安全瀏覽服務

  • Venti 文檔存儲系統也採用布隆過濾器來檢測先前存儲的數據。

  • SPIN 模型檢測器也使用布隆過濾器在大規模驗證問題時跟蹤可達狀態空間。

Coding~

知道了布隆過濾去的原理和使用場景,咱們能夠本身實現一個簡單的布隆過濾器

自定義的 BloomFilter

public class MyBloomFilter {

    /**
     * 一個長度爲10 億的比特位
     */
    private static final int DEFAULT_SIZE = 256 << 22;

    /**
     * 爲了下降錯誤率,使用加法hash算法,因此定義一個8個元素的質數數組
     */
    private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};

    /**
     * 至關於構建 8 個不一樣的hash算法
     */
    private static HashFunction[] functions = new HashFunction[seeds.length];

    /**
     * 初始化布隆過濾器的 bitmap
     */
    private static BitSet bitset = new BitSet(DEFAULT_SIZE);

    /**
     * 添加數據
     *
     * @param value 須要加入的值
     */
    public static void add(String value) {
        if (value != null) {
            for (HashFunction f : functions) {
                //計算 hash 值並修改 bitmap 中相應位置爲 true
                bitset.set(f.hash(value), true);
            }
        }
    }

    /**
     * 判斷相應元素是否存在
     * @param value 須要判斷的元素
     * @return 結果
     */
    public static boolean contains(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (HashFunction f : functions) {
            ret = bitset.get(f.hash(value));
            //一個 hash 函數返回 false 則跳出循環
            if (!ret) {
                break;
            }
        }
        return ret;
    }

    /**
     * 模擬用戶是否是會員,或用戶在不在線。。。
     */
    public static void main(String[] args) {

        for (int i = 0; i < seeds.length; i++) {
            functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
        }

        // 添加1億數據
        for (int i = 0; i < 100000000; i++) {
            add(String.valueOf(i));
        }
        String id = "123456789";
        add(id);

        System.out.println(contains(id));   // true
        System.out.println("" + contains("234567890"));  //false
    }
}

class HashFunction {

    private int size;
    private int seed;

    public HashFunction(int size, int seed) {
        this.size = size;
        this.seed = seed;
    }

    public int hash(String value) {
        int result = 0;
        int len = value.length();
        for (int i = 0; i < len; i++) {
            result = seed * result + value.charAt(i);
        }
        int r = (size - 1) & result;
        return (size - 1) & result;
    }
}

What?咱們寫的這些早有大牛幫咱們實現,還造輪子,真是浪費時間,No,No,No,咱們學習過程當中是能夠造輪子的,造輪子自己就是咱們本身對設計和實現的具體落地過程,不只能提升咱們的編程能力,在造輪子的過程當中確定會遇到不少咱們沒有思考過的問題,成長看的見~~

實際項目使用的時候,領導和我說項目必定要穩定運行,沒自信的我放棄了本身的輪子。

Guava 中的 BloomFilter

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>
public class GuavaBloomFilterDemo {

    public static void main(String[] args) {
        //後邊兩個參數:預計包含的數據量,和容許的偏差值
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100000, 0.01);
        for (int i = 0; i < 100000; i++) {
            bloomFilter.put(i);
        }
        System.out.println(bloomFilter.mightContain(1));
        System.out.println(bloomFilter.mightContain(2));
        System.out.println(bloomFilter.mightContain(3));
        System.out.println(bloomFilter.mightContain(100001));

        //bloomFilter.writeTo();
    }
}

分佈式環境中,布隆過濾器確定還須要考慮是能夠共享的資源,這時候咱們會想到 Redis,是的,Redis 也實現了布隆過濾器。

固然咱們也能夠把布隆過濾器經過 bloomFilter.writeTo() 寫入一個文件,放入OSS、S3這類對象存儲中。

Redis 中的 BloomFilter

Redis 提供的 bitMap 能夠實現布隆過濾器,可是須要本身設計映射函數和一些細節,這和咱們自定義沒啥區別。

Redis 官方提供的布隆過濾器到了 Redis 4.0 提供了插件功能以後才正式登場。布隆過濾器做爲一個插件加載到 Redis Server 中,給 Redis 提供了強大的布隆去重功能。

在已安裝 Redis 的前提下,安裝 RedisBloom,有兩種方式

直接編譯進行安裝

git clone https://github.com/RedisBloom/RedisBloom.git
cd RedisBloom
make     #編譯 會生成一個rebloom.so文件
redis-server --loadmodule /path/to/rebloom.so   #運行redis時加載布隆過濾器模塊
redis-cli    # 啓動鏈接容器中的 redis 客戶端驗證

使用Docker進行安裝

docker pull redislabs/rebloom:latest # 拉取鏡像
docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest #運行容器
docker exec -it redis-redisbloom bash
redis-cli

使用

布隆過濾器基本指令:

  • bf.add 添加元素到布隆過濾器
  • bf.exists 判斷元素是否在布隆過濾器
  • bf.madd 添加多個元素到布隆過濾器,bf.add 只能添加一個
  • bf.mexists 判斷多個元素是否在布隆過濾器
127.0.0.1:6379> bf.add user Tom
(integer) 1
127.0.0.1:6379> bf.add user John
(integer) 1
127.0.0.1:6379> bf.exists user Tom
(integer) 1
127.0.0.1:6379> bf.exists user Linda
(integer) 0
127.0.0.1:6379> bf.madd user Barry Jerry Mars
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists user Barry Linda
1) (integer) 1
2) (integer) 0

咱們只有這幾個參數,確定不會有誤判,當元素逐漸增多時,就會有必定的誤判了,這裏就不作這個實驗了。

上面使用的布隆過濾器只是默認參數的布隆過濾器,它在咱們第一次 add 的時候自動建立。

Redis 還提供了自定義參數的布隆過濾器,bf.reserve 過濾器名 error_rate initial_size

  • error_rate:容許布隆過濾器的錯誤率,這個值越低過濾器的位數組的大小越大,佔用空間也就越大
  • initial_size:布隆過濾器能夠儲存的元素個數,當實際存儲的元素個數超過這個值以後,過濾器的準確率會降低

可是這個操做須要在 add 以前顯式建立。若是對應的 key 已經存在,bf.reserve 會報錯

127.0.0.1:6379> bf.reserve user 0.01 100
(error) ERR item exists
127.0.0.1:6379> bf.reserve topic 0.01 1000
OK

我是一名 Javaer,確定還要用 Java 來實現的,Java 的 Redis 客戶端比較多,有些尚未提供指令擴展機制,筆者已知的 Redisson 和 lettuce 是可使用布隆過濾器的,咱們這裏用 Redisson

public class RedissonBloomFilterDemo {

    public static void main(String[] args) {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user");
        // 初始化布隆過濾器,預計統計元素數量爲55000000,指望偏差率爲0.03
        bloomFilter.tryInit(55000000L, 0.03);
        bloomFilter.add("Tom");
        bloomFilter.add("Jack");
        System.out.println(bloomFilter.count());   //2
        System.out.println(bloomFilter.contains("Tom"));  //true
        System.out.println(bloomFilter.contains("Linda"));  //false
    }
}

擴展

爲了解決布隆過濾器不能刪除元素的問題,布穀鳥過濾器橫空出世。論文《Cuckoo Filter:Better Than Bloom》做者將布穀鳥過濾器和布隆過濾器進行了深刻的對比。相比布穀鳥過濾器而言布隆過濾器有如下不足:查詢性能弱、空間利用效率低、不支持反向操做(刪除)以及不支持計數。

因爲使用較少,暫不深刻。

文章持續更新,能夠微信搜「 JavaKeeper 」第一時間閱讀,無套路領取 500+ 本電子書和 30+ 視頻教學和源碼,本文 GitHub github.com/JavaKeeper 已經收錄,Javaer 開發、面試必備技能兵器譜,有你想要的。

參考與感謝

https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf

http://www.justdojava.com/2019/10/22/bloomfilter/

http://www.javashuo.com/article/p-aenfkllo-mo.html

http://www.javashuo.com/article/p-yqqvbcif-cg.html

相關文章
相關標籤/搜索