【布隆過濾器】一
【布隆過濾器】一
解決快取穿透的辦法之一,就是布隆過濾器
快取穿透
:指快取和資料庫中都沒有的資料,而使用者不斷髮起請求,如發起id為“-1”的資料或id為其他不存在的資料。這時的使用者很可能是攻擊者,攻擊會導致資料庫壓力過大。
布隆過濾器(Bloom Filter)
布隆過濾器(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,該資料存在集合中。(這裡有缺點,下面會講)
可以通過乘法判斷是否有二進位制資料為0的。
刪除過程
一般不能刪除布隆過濾器裡的資料,這是一個缺點之一,我們下面會分析。
布隆過濾器的優缺點
優點
- 由於儲存的是二進位制資料,所以佔用的空間很小
- 它的插入和查詢速度是非常快的,時間複雜度是O(K),可以聯想一下HashMap的過程
- 保密性很好,因為本身不儲存任何原始資料,只有二進位制資料
缺點
新增資料是通過計算資料的hash值,那麼很有可能存在這種情況:兩個不同的資料計算得到相同的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>
二、程式碼實現
注意:布隆過濾器存在誤判率,可以手動設定,誤判率越小,佔用記憶體和hash計算耗時越大
package cn.lxiaol.www.bloomfilter;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterCase {
/**
* 預計要存入多少資料
*/
private static final int size = 1000000;
/**
* 期望的誤判率
*/
private static double fpp = 0.01;
/**
* 布隆過濾器
*/
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
private static final int total = 1000000;
public static void main(String[] args) {
// 插入100萬條樣本資料
for (int i = 0; i < total; i++) {
bloomFilter.put(i);
}
//用另外10萬測試誤判率
int count = 0;
for (int i = total; i < total+100000; i++) {
if(bloomFilter.mightContain(i)){
count++;
System.out.println(i+"誤判了");
}
}
System.out.println("總誤判數:"+count);
System.out.println("fpp:" + 1.0 * count / 100000);
}
}
10萬資料裡有947個誤判,約等於我們程式碼裡設定的誤判率:fpp = 0.01。
深入分析程式碼
@VisibleForTesting
static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions,
double fpp, BloomFilter.Strategy strategy) {
Preconditions.checkNotNull(funnel);
Preconditions.checkArgument(expectedInsertions >= 0L, "Expected insertions (%s) must be >= 0", expectedInsertions);
Preconditions.checkArgument(fpp > 0.0D, "False positive probability (%s) must be > 0.0", fpp);
Preconditions.checkArgument(fpp < 1.0D, "False positive probability (%s) must be < 1.0", fpp);
Preconditions.checkNotNull(strategy);
if (expectedInsertions == 0L) {
expectedInsertions = 1L;
}
long numBits = optimalNumOfBits(expectedInsertions, fpp);
int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
try {
return new BloomFilter(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
} catch (IllegalArgumentException var10) {
throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", var10);
}
}
這裡有四個引數:
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)。
解決Redis快取雪崩
上面使用Guava實現的布隆過濾器是把資料放在了本地記憶體中。分散式的場景中就不合適了,無法共享記憶體。
我們還可以用Redis來實現布隆過濾器,這裡使用Redis封裝好的客戶端工具Redisson。
其底層是使用資料結構bitMap,大家就把它理解成上面說的二進位制結構。
程式碼實現
pom配置:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.4</version>
</dependency>
java程式碼:
package cn.lxiaol.www.bloomfilter;
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonBloomFilter {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:2020");
config.useSingleServer().setPassword("yiguan789");
// 構造redis
RedissonClient redissonClient = Redisson.create(config);
// 獲取布隆過濾器,並給該過濾器起一個名字,叫做 nickNameList
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("nickNameList");
// 初始化該過濾器,預計元素為 100萬,誤差率為3% 預設是3%
bloomFilter.tryInit(1000000, 0.03);
//將暱稱 "蹬杆老王子" 插入到過濾器中
bloomFilter.add("蹬杆老王子");
//判斷下面暱稱是否在布隆過濾器中
//輸出false
System.out.println(bloomFilter.contains("卡特機長"));
//輸出true
System.out.println(bloomFilter.contains("蹬杆老王子"));
redissonClient.shutdown();
}
}