文章目錄
前言
布隆過濾器是一種用來檢索數據是否在大集合中的高效的、空間佔用少的概率型數據結構,該數據結構由哈希函數以及位數組(二進制向量)來實現的,一般而言,布隆過濾器支持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));
}
}