前言
我們之前講了Redis的緩存雪崩、穿透、擊穿。在文章里我們說了解決緩存穿透的辦法之一,就是布隆過濾器
,但是上次並沒有講如何使用布隆過濾器。
作為暖男的老哥,給你們補上,請叫我IT老暖男
。

什么是布隆過濾器
布隆過濾器(Bloom Filter),是1970年,由一個叫布隆的小伙子提出的,距今已經五十年了,和老哥一樣老。
它實際上是一個很長的二進制向量和一系列隨機映射函數,二進制大家應該都清楚,存儲的數據不是0就是1,默認是0。
主要用於判斷一個元素是否在一個集合中,0代表不存在
某個數據,1代表存在
某個數據。
懂了嗎?作為暖男
的老哥在給你們畫張圖來幫助理解:

布隆過濾器用途
-
解決Redis緩存穿透(今天重點講解)
-
在爬蟲時,對爬蟲網址進行過濾,已經存在布隆中的網址,不在爬取。
-
垃圾郵件過濾,對每一個發送郵件的地址進行判斷是否在布隆的黑名單中,如果在就判斷為垃圾郵件。
以上只是簡單的用途舉例,大家可以舉一反三,靈活運用在工作中。
布隆過濾器原理
存入過程
布隆過濾器上面說了,就是一個二進制數據的集合。當一個數據加入這個集合時,經歷如下洗禮(這里有缺點,下面會講):
-
通過K個哈希函數計算該數據,返回K個計算出的hash值
-
這些K個hash值映射到對應的K個二進制的數組下標
-
將K個下標對應的二進制數據改成1。
例如,第一個哈希函數返回x,第二個第三個哈希函數返回y與z,那么: X、Y、Z對應的二進制改成1。
如圖所示:
查詢過程
布隆過濾器主要作用就是查詢一個數據,在不在這個二進制的集合中,查詢過程如下:
-
通過K個哈希函數計算該數據,對應計算出的K個hash值
-
通過hash值找到對應的二進制的數組下標
-
判斷:如果存在一處位置的二進制數據是0,那么該數據不存在。如果都是1,該數據存在集合中。(這里有缺點,下面會講)
刪除過程
一般不能刪除布隆過濾器里的數據,這是一個缺點之一,我們下面會分析。
布隆過濾器的優缺點
優點
-
由於存儲的是二進制數據,所以占用的空間很小
-
它的插入和查詢速度是非常快的,時間復雜度是O(K),可以聯想一下HashMap的過程
-
保密性很好,因為本身不存儲任何原始數據,只有二進制數據
缺點
這就要回到我們上面所說的那些缺點了。
添加數據是通過計算數據的hash值,那么很有可能存在這種情況:兩個不同的數據計算得到相同的hash值。

例如圖中的“你好
”和“hello
”,假如最終算出hash值相同,那么他們會將同一個下標的二進制數據改為1。
這個時候,你就不知道下標為2的二進制,到底是代表“你好
”還是“hello
”。
由此得出如下缺點:
一、存在誤判
假如上面的圖沒有存"hello
",只存了"你好
",那么用"hello
"來查詢的時候,會判斷"hello
"存在集合中。
因為“你好
”和“hello
”的hash值是相同的,通過相同的hash值,找到的二進制數據也是一樣的,都是1。
二、刪除困難
到這里我不說大家應該也明白為什么吧,作為你們的暖男老哥
,還是講一下吧。
還是用上面的舉例,因為“你好
”和“hello
”的hash值相同,對應的數組下標也是一樣的。
這時候老哥想去刪除“你好
”,將下標為2里的二進制數據,由1改成了0。
那么我們是不是連“hello
”都一起刪了呀。(0代表有這個數據,1代表沒有這個數據)
到這里是不是對布隆過濾器已經明白了,都說了我是暖男。

實現布隆過濾器
有很多種實現方式,其中一種就是Guava提供的實現方式。
一、引入Guava pom配置
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
復制代碼
二、代碼實現
這里我們順便測一下它的誤判率。
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterCase {
/**
* 預計要插入多少數據
*/
private static int size = 1000000;
/**
* 期望的誤判率
*/
private static double fpp = 0.01;
/**
* 布隆過濾器
*/
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
public static void main(String[] args) {
// 插入10萬樣本數據
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
// 用另外十萬測試數據,測試誤判率
int count = 0;
for (int i = size; i < size + 100000; i++) {
if (bloomFilter.mightContain(i)) {
count++;
System.out.println(i + "誤判了");
}
}
System.out.println("總共的誤判數:" + count);
}
}
復制代碼
運行結果:
10萬數據里有947個誤判,約等於0.01%,也就是我們代碼里設置的誤判率:fpp = 0.01。
深入分析代碼
核心BloomFilter.create
方法
@VisibleForTesting
static <T> BloomFilter<T> create(
Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
。。。。
}
復制代碼
這里有四個參數:
-
funnel
:數據類型(一般是調用Funnels工具類中的) -
expectedInsertions
:期望插入的值的個數 -
fpp
:誤判率(默認值為0.03) -
strategy
:哈希算法
我們重點講一下fpp
參數
fpp誤判率
情景一:fpp = 0.01
-
誤判個數:947
-
占內存大小:9585058位數
情景二:fpp = 0.03
(默認參數)
-
誤判個數:3033
-
占內存大小:7298440位數
情景總結
-
誤判率可以通過
fpp
參數進行調節 -
fpp越小,需要的內存空間就越大:0.01需要900多萬位數,0.03需要700多萬位數。
-
fpp越小,集合添加數據時,就需要更多的hash函數運算更多的hash值,去存儲到對應的數組下標里。(忘了去看上面的布隆過濾存入數據的過程)
上面的numBits
,表示存一百萬個int類型數字,需要的位數為7298440,700多萬位。理論上存一百萬個數,一個int是4字節32位,需要481000000=3200萬位。如果使用HashMap去存,按HashMap50%的存儲效率,需要6400萬位。可以看出BloomFilter的存儲空間很小,只有HashMap的1/10左右
上面的numHashFunctions
表示需要幾個hash函數運算,去映射不同的下標存這些數字是否存在(0 or 1)。