布隆過濾器是什麼?(布隆過濾器原理解析)

前言

布隆過濾器是一種用來檢索數據是否在大集合中的高效的、空間佔用少的概率型數據結構,該數據結構由哈希函數以及位數組(二進制向量)來實現的,一般而言,布隆過濾器支持add 和 isExist 操作。

過濾器使用場景

1、布隆過濾器主要用於判斷給定數據是否存在大數據集中。可以用來防止緩存穿透、郵箱的垃圾郵件過濾、黑名單功能等等。利用布隆過濾器可以很有效的減少磁盤 IO 或者網絡請求,因爲一旦一個值必定不存在的話,我們可以不用進行後續昂貴的查詢請求。
2、去重:比如爬給定網址的時候對已經爬取過的 URL 去重/新聞推送去重。

實現原理

add操作-添加元素

布隆過濾器有若干個不同的哈希函數,當我們插入一個數到布隆過濾器時,n個哈希函數會對數據值進行運算,分別映射到位數組的n個位置,此時這個位置的值都更新爲1。此處圖例就是通過將"線性代數"這個值插入布隆過濾器,然後通過三個不同的哈希函數將其映射到三個不同的位置,並將該位置值更新爲1。
在這裏插入圖片描述

isExist 操作-判斷元素存在與否

該操作用來判斷元素是否存在於過濾器中。當我需要判斷某個值是否在布隆過濾器中時,我們使用布隆過濾器的所有hash函數對該元素值進行計算,找到位數組中映射的位置,只有當位數組所有映射位置的值都爲1,才能確定該值存在,否則定爲不存在。例圖中我們查找"高等數學"這個元素是否存在,那我們就用布隆過濾器設置的hash函數(這裏設置的是三個hash函數)對元素值進行hash運算計算出位數組的下標,若三個hash函數計算出的下標的位置中的值都爲1,則該元素存在(其實只是可能存在,原因下面詳述)否則,該元素一定不存在。
在這裏插入圖片描述

布隆過濾器是概率型的原因

當過濾器中數據插入過多,而位數組相對過小,則會存在不同的數據映射的位置有交集。極致時,檢索一個不存在的數據,所映射的所有位置都爲1。所以說過濾器認爲存在的是可能存在,因爲存在誤判。但認爲不存在的,則一定不存在。

如何選擇過濾器的長度和hash函數個數?

過小的布隆過濾器很快所有的 bit 位均爲 1,那麼查詢任何值都會返回“可能存在”,起不到過濾的目的了。布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。另外,哈希函數的個數也需要權衡,個數越多則布隆過濾器 bit 位置位 1 的速度越快,且布隆過濾器的效率越低;但是如果太少的話,那我們的誤報率會變高。

可以使用以下公式來進行長度和hash函數個數的選擇(k爲函數個數,m爲數組長度,n爲插入的元素個數,p爲誤報率)
在這裏插入圖片描述

Redis中的布隆過濾器

redis經常發生緩存擊穿問題,一般面對這種問題,除了在接口層進行校驗,通常採取兩種措施,一種是採用緩存空值的方法,當數據庫和Redis中都不存在key,在數據庫返回null時,在Redis中插入<key,null,expireTime>(緩存時間可以設置個較短的時間),當key再次請求時,Redis直接返回null,而不用再次請求數據庫。另外一種就是使用布隆過濾器進行過濾。

過濾原理

將數據庫中所有的key放入布隆過濾器中,當一個查詢請求過來時,先經過布隆過濾器進行查詢,如果判斷請求查詢值存在,則繼續查緩存;如果判斷請求查詢不存在,直接丟棄,避免查詢數據庫。
在這裏插入圖片描述

Redis應用技巧

redis的bitmap只支持2^32大小,對應到內存就是512MB,數組的下標最大隻能是
2^32-1。但我們的布隆過濾器很大,因此我們通過使用多個bitmap組成一個布隆過濾器。需要注意的是我們對一個元素key值進行hash運算之後應該落在同一個bitmap上。

Redis安裝布隆過濾器

Redis v4.0 之後有了 Module(模塊/插件) 功能,Redis Modules 讓 Redis 可以使用外部模塊擴展其功能 。布隆過濾器就是其中的 Module。
我們可以使用Docker來進行Redis安裝,也可以直接在https://github.com/RedisBloom/RedisBloom下載最新的release源碼,在編譯服務器進行解壓編譯,得到動態庫後再進行插件安裝。

Redis操作過濾器常用命令

1、BF.ADD:將元素添加到布隆過濾器中,如果該過濾器尚不存在,則創建該過濾器。
格式:BF.ADD {key} {item}。
2、BF.MADD : 將一個或多個元素添加到“布隆過濾器”中,並創建一個尚不存在的過濾器。該命令的操作方式BF.ADD與之相同,只不過它允許多個輸入並返回多個值。
格式:BF.MADD {key} {item} [item …] 。
3、BF.EXISTS : 確定元素是否在布隆過濾器中存在。
格式:BF.EXISTS {key} {item}。
4、BF.MEXISTS : 確定一個或者多個元素是否在布隆過濾器中存在
格式:BF.MEXISTS {key} {item} [item …]。
5、BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]:用來創建一個布隆過濾器
key:布隆過濾器的名稱
error_rate :誤報的期望概率。這應該是介於0到1之間的十進制值。例如,對於期望的誤報率0.1%(1000中爲1),error_rate應該設置爲0.001。該數字越接近零,則每個項目的內存消耗越大,並且每個操作的CPU使用率越高。
capacity: 過濾器的容量。當實際存儲的元素個數超過這個值之後,性能將開始下降。實際的降級將取決於超出限制的程度。隨着過濾器元素數量呈指數增長,性能將線性下降。
可選參數:
expansion:如果創建了一個新的子過濾器,則其大小將是當前過濾器的大小乘以expansion。默認擴展值爲2。這意味着每個後續子過濾器將是前一個子過濾器的兩倍。

127.0.0.1:6379> BF.ADD myFilter java
(integer) 1
127.0.0.1:6379> BF.ADD myFilter CSDN
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter java
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter CSDN
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter XUXIANG
(integer) 0

實現布隆過濾器的其它方法

Google開源的 Guava中自帶的布隆過濾器

用Google開源的 Guava中自帶的布隆過濾器,但這種方法的缺點就在於只能單機使用,不能應用於分佈式。
使用方法-在項目中引入 Guava 的依賴

  <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>28.0-jre</version>
  </dependency>

實際使用示例如下

     // 創建布隆過濾器對象,大小位3600,容錯率爲0.01
        BloomFilter<Integer> filter = BloomFilter.create(
                Funnels.integerFunnel(),
                3600,
                0.01);
        // 判斷指定元素是否存在
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));
        // 將元素添加進布隆過濾器
        filter.put(1);
        filter.put(2);
        System.out.println(filter.mightContain(1));
        System.out.println(filter.mightContain(2));

在java中使用BitSet實現布隆過濾器

1、使用位數組BitSet保存數據
2、幾個不同的高效率哈希函數
3、添加元素到位數組(布隆過濾器)的方法實現
4、判斷給定元素是否存在於位數組(布隆過濾器)的方法實現。

package dataStructure;


import java.util.BitSet;

/*
@function 布隆過濾器的實現
@problem description
  一個合適大小的位數組保存數據
        幾個不同的哈希函數
        添加元素到位數組(布隆過濾器)的方法實現
        判斷給定元素是否存在於位數組(布隆過濾器)的方法實現。
@date 20-3-9
*/

public class MyBloomFilter {
    /**
     * 位數組的大小
     */
    private static final int DEFAULT_SIZE = 2 << 24;
    /**
     * 通過這個數組可以創建 6 個不同的哈希函數
     */
    private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};

    /**
     * 位數組。數組中的元素只能是 0 或者 1,BitSet的大小爲long類型大小(64位)的整數倍。非安全性,去重
     */
    private BitSet bits = new BitSet(DEFAULT_SIZE);

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

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

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

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

    /**
     * 靜態內部類。用於 hash 操作!
     */
    public static class SimpleHash {
        private int cap;
        private int seed;
        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

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

    }
    public static void main(String[] args){
        String value1 = "https://javaguide.cn/";
        String value2 = "https://github.com/Snailclimb";
        MyBloomFilter filter = new MyBloomFilter();
        System.out.println(filter.contains(value1));
        System.out.println(filter.contains(value2));
        filter.add(value1);
        filter.add(value2);
        System.out.println(filter.contains(value1));
        System.out.println(filter.contains(value2));
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章