布隆過濾器
布隆過濾器是一種由位陣列和多個雜湊函式組成概率資料結構,返回兩種結果 可能存在 和 一定不存在。
布隆過濾器裡的一個元素由多個狀態值共同確定。位陣列儲存狀態值,雜湊函式計算狀態值的位置。
根據它的演算法結構,有如下特徵:
- 使用有限位陣列表示大於它長度的元素數量,因為一個位的狀態值可以同時標識多個元素。
- 不能刪除元素。因為一個位的狀態值可能同時標識著多個元素。
- 新增元素永遠不會失敗。只是隨著新增元素增多,誤判率會上升。
- 如果判斷元素不存在,那麼它一定不存在。
比如下面,X,Y,Z 分別由 3個狀態值共同確定元素是否存在,狀態值的位置通過3個雜湊函式分別計算。
數學關係
誤判概率
關於誤判概率,因為每個位的狀態值可能同時標識多個元素,所以它存在一定的誤判概率。如果位陣列滿,當判斷元素是否存在時,它會始終返回true
,對於不存在的元素來說,它的誤判率就是100%。
那麼,誤判概率和哪些因素有關,已新增元素的數量,布隆過濾器長度(位陣列大小),雜湊函式數量。
根據維基百科推理誤判概率 \( P_{fp} \)有如下關係:
$$ { P_{fp} =\left(1-\left[1-{\frac {1}{m}}\right]^{kn}\right)^{k}\approx \left(1-e^{{-\frac {kn}{m}}}\right)^{k}} $$
- $m$ 是位陣列的大小;
- $n$ 是已經新增元素的數量;
- $k$ 是雜湊函式數量;
- $e$ 數學常數,約等於2.718281828。
由此可以得到,當新增元素數量為0時,誤報率為0;當位陣列全都為1時,誤報率為100%。
不同數量雜湊函式下,$ P_{fp}$ 和 $ n$ 的關係如下圖:
根據誤判概率公式可以做一些事
- 估算最佳布隆過濾器長度。
- 估算最佳雜湊函式數量。
最佳布隆過濾器長度
當 \(n \) 新增元素和 \( P_{fp} \)誤報概率確定時,\( m \) 等於:
$$ {\displaystyle m=-{\frac {n\ln P_{fp}}{(\ln 2)^{2}}} \approx -1.44\cdot n\log _{2}P_{fp}} $$
最佳雜湊函式數量
當 \( n \) 和 \(P_{fp} \) 確定時,\( k \) 等於:
$$ {\displaystyle k=-{\frac {\ln P_{fp} }{\ln 2}}=-\log _{2}P_{fp} } $$
當 \( n \) 和 \( m \) 確定時,\( k \) 等於:
$$ {\displaystyle k={\frac {m}{n}}\ln 2} $$
實現布隆過濾器
使用布隆過濾器前,我們一般會評估兩個因素。
- 預期新增元素的最大數量。
- 業務對錯誤的容忍程度。比如1000個允許錯一個,那麼誤判概率應該在千分之一內。
很多布隆過濾工具都提供了預期新增數量和誤判概率配置引數,它們會根據配置的引數計算出最佳的長度和雜湊函式數量。
Java中有一些不錯的布隆過濾工具包。
Guava
中BloomFilter
。redisson
中RedissonBloomFilter
可以redis 中使用。
看下 Guava
中 BloomFilter
的簡單實現,建立前先計算出位陣列長度和雜湊函式數量。
static <T> BloomFilter<T> create(
Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
/**
* expectedInsertions:預期新增數量
* fpp:誤判概率
*/
long numBits = optimalNumOfBits(expectedInsertions, fpp);
int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
try {
return new BloomFilter<T>(new BitArray(numBits), numHashFunctions, funnel, strategy);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
}
}
根據最佳布隆過濾器長度公式,計算最佳位陣列長度。
static long optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
根據最佳雜湊函式數量公式,計算最佳雜湊函式數量。
static int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
在redisson
中 RedissonBloomFilter
計算方法也是一致。
private int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
private long optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
記憶體佔用
設想一個手機號去重場景,每個手機號佔用22 Byte
,估算邏輯記憶體如下。
expected | HashSet | fpp=0.0001 | fpp=0.0000001 |
---|---|---|---|
100萬 | 18.28MB | 2.29MB | 4MB |
1000萬 | 182.82MB | 22.85MB | 40MB |
1億 | 1.78G | 228.53MB | 400MB |
注:實際實體記憶體佔用大於邏輯記憶體。
誤判概率 \( p \) 和已新增的元素 \( n \),位陣列長度 \( m \),雜湊函式數量 \( k \) 關係如下:
應用場景
- 弱密碼檢測;
- 垃圾郵件地址過濾。
- 瀏覽器檢測釣魚網站;
- 快取穿透。
弱密碼檢測
維護一個雜湊過弱密碼列表。當使用者註冊或更新密碼時,使用布隆過濾器檢查新密碼,檢測到提示使用者。
垃圾郵件地址過濾
維護一個雜湊過垃圾郵件地址列表。當使用者接收郵件,使用布隆過濾器檢測,檢測到標識為垃圾郵件。
瀏覽器檢測釣魚網站
使用布隆過濾器來查詢釣魚網站資料庫中是否存在某個網站的 URL。
快取穿透
快取穿透是指查詢一個根本不存在的資料,快取層和資料庫都不會命中。當快取未命中時,查詢資料庫
- 資料庫不命中,空結果不會寫回快取並返回空結果。
- 資料庫命中,查詢結果寫回快取並返回結果。
一個典型的攻擊,模擬大量請求查詢不存在的資料,所有請求落到資料庫,造成資料庫當機。
其中一種解決方案,將存在的快取放入布隆過濾器,在請求前進行校驗過濾。
小結
對於千萬億級別的資料來說,使用布隆過濾器具有一定優勢,另外根據業務場景合理評估預期新增數量和誤判概率是關鍵。
參考