布隆過濾器,你也可以處理十幾億的大資料
阿新 • • 發佈:2020-05-09
>文章收錄在 GitHub [JavaKeeper](https://github.com/Jstarfish/JavaKeeper) ,N線網際網路開發必備技能兵器譜
## 什麼是 BloomFilter
**布隆過濾器**(英語:Bloom Filter)是 1970 年由布隆提出的。它實際上是一個很長的二進位制向量和一系列隨機對映函式。主要用於判斷一個元素是否在一個集合中。
通常我們會遇到很多要判斷一個元素是否在某個集合中的業務場景,一般想到的是將集合中所有元素儲存起來,然後通過比較確定。連結串列、樹、散列表(又叫雜湊表,Hash table)等等資料結構都是這種思路。但是隨著集合中元素的增加,我們需要的儲存空間也會呈現線性增長,最終達到瓶頸。同時檢索速度也越來越慢,上述三種結構的檢索時間複雜度分別為$O(n)$,$O(logn)$,$O(1)$。
這個時候,布隆過濾器(Bloom Filter)就應運而生。
## 布隆過濾器原理
瞭解布隆過濾器原理之前,先回顧下 Hash 函式原理。
### 雜湊函式
雜湊函式的概念是:將任意大小的輸入資料轉換成特定大小的輸出資料的函式,轉換後的資料稱為雜湊值或雜湊編碼,也叫雜湊值。下面是一幅示意圖:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1ge9vykn7uej31at0u01kx.jpg)
所有雜湊函式都有如下基本特性:
- 如果兩個雜湊值是不相同的(根據同一函式),那麼這兩個雜湊值的原始輸入也是不相同的。這個特性是雜湊函式具有確定性的結果,具有這種性質的雜湊函式稱為**單向雜湊函式**。
- 雜湊函式的輸入和輸出不是唯一對應關係的,如果兩個雜湊值相同,兩個輸入值很可能是相同的,但也可能不同,這種情況稱為“**雜湊碰撞**(collision)”。
但是用 hash表儲存大資料量時,空間效率還是很低,當只有一個 hash 函式時,還很容易發生雜湊碰撞。
### 布隆過濾器資料結構
BloomFilter 是由一個固定大小的二進位制向量或者點陣圖(bitmap)和一系列對映函式組成的。
在初始狀態時,對於長度為 m 的位陣列,它的所有位都被置為0,如下圖所示:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gebubj40y3j312e0i277i.jpg)
當有變數被加入集合時,通過 K 個對映函式將這個變數對映成點陣圖中的 K 個點,把它們置為 1(假定有兩個變數都通過 3 個對映函式)。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gebuc2ee3dj31o10u0drg.jpg)
查詢某個變數的時候我們只要看看這些點是不是都是 1 就可以大概率知道集合中有沒有它了
- 如果這些點有任何一個 0,則被查詢變數一定不在;
- 如果都是 1,則被查詢變數很**可能存在**
為什麼說是可能存在,而不是一定存在呢?那是因為對映函式本身就是雜湊函式,雜湊函式是會有碰撞的。
### 誤判率
布隆過濾器的誤判是指多個輸入經過雜湊之後在相同的bit位置1了,這樣就無法判斷究竟是哪個輸入產生的,因此誤判的根源在於相同的 bit 位被多次對映且置 1。
這種情況也造成了布隆過濾器的刪除問題,因為布隆過濾器的每一個 bit 並不是獨佔的,很有可能多個元素共享了某一位。如果我們直接刪除這一位的話,會影響其他的元素。(比如上圖中的第 3 位)
### 特性
- **一個元素如果判斷結果為存在的時候元素不一定存在,但是判斷結果為不存在的時候則一定不存在**。
- **布隆過濾器可以新增元素,但是不能刪除元素**。因為刪掉元素會導致誤判率增加。
### 新增與查詢元素步驟
#### 新增元素
1. 將要新增的元素給 k 個雜湊函式
2. 得到對應於位陣列上的 k 個位置
3. 將這k個位置設為 1
#### 查詢元素
1. 將要查詢的元素給k個雜湊函式
2. 得到對應於位陣列上的k個位置
3. 如果k個位置有一個為 0,則肯定不在集合中
4. 如果k個位置全部為 1,則可能在集合中
### 優點
相比於其它的資料結構,布隆過濾器在空間和時間方面都有巨大的優勢。布隆過濾器儲存空間和插入/查詢時間都是常數 $O(K)$,另外,雜湊函式相互之間沒有關係,方便由硬體並行實現。布隆過濾器不需要儲存元素本身,在某些對保密要求非常嚴格的場合有優勢。
布隆過濾器可以表示全集,其它任何資料結構都不能;
### 缺點
但是布隆過濾器的缺點和優點一樣明顯。誤算率是其中之一。隨著存入的元素數量增加,誤算率隨之增加。但是如果元素數量太少,則使用散列表足矣。
另外,一般情況下不能從布隆過濾器中刪除元素。我們很容易想到把位陣列變成整數陣列,每插入一個元素相應的計數器加 1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全地刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器裡面。這一點單憑這個過濾器是無法保證的。另外計數器迴繞也會造成問題。
在降低誤算率方面,有不少工作,使得出現了很多布隆過濾器的變種。
## 布隆過濾器使用場景和例項
在程式的世界中,布隆過濾器是程式設計師的一把利器,利用它可以快速地解決專案中一些比較棘手的問題。
如網頁 URL 去重、垃圾郵件識別、大集合中重複元素的判斷和快取穿透等問題。
布隆過濾器的典型應用有:
- 資料庫防止穿庫。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter來減少不存在的行或列的磁碟查詢。避免代價高昂的磁碟查詢會大大提高資料庫查詢操作的效能。
- 業務場景中判斷使用者是否閱讀過某視訊或文章,比如抖音或頭條,當然會導致一定的誤判,但不會讓使用者看到重複的內容。
- 快取宕機、快取擊穿場景,一般判斷使用者是否在快取中,如果在則直接返回結果,不在則查詢db,如果來一波冷資料,會導致快取大量擊穿,造成雪崩效應,這時候可以用布隆過濾器當快取的索引,只有在布隆過濾器中,才去查詢快取,如果沒查詢到,則穿透到db。如果不在布隆器中,則直接返回。
- WEB攔截器,如果相同請求則攔截,防止重複被攻擊。使用者第一次請求,將請求引數放入布隆過濾器中,當第二次請求時,先判斷請求引數是否被布隆過濾器命中。可以提高快取命中率。Squid 網頁代理快取伺服器在 cache digests 中就使用了布隆過濾器。Google Chrome瀏覽器使用了布隆過濾器加速安全瀏覽服務
- Venti 文件儲存系統也採用布隆過濾器來檢測先前儲存的資料。
- SPIN 模型檢測器也使用布隆過濾器在大規模驗證問題時跟蹤可達狀態空間。
## Coding~
知道了布隆過濾去的原理和使用場景,我們可以自己實現一個簡單的布隆過濾器
### 自定義的 BloomFilter
```java
public class MyBloomFilter {
/**
* 一個長度為10 億的位元位
*/
private static final int DEFAULT_SIZE = 256 << 22;
/**
* 為了降低錯誤率,使用加法hash演算法,所以定義一個8個元素的質數陣列
*/
private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};
/**
* 相當於構建 8 個不同的hash演算法
*/
private static HashFunction[] functions = new HashFunction[seeds.length];
/**
* 初始化布隆過濾器的 bitmap
*/
private static BitSet bitset = new BitSet(DEFAULT_SIZE);
/**
* 新增資料
*
* @param value 需要加入的值
*/
public static void add(String value) {
if (value != null) {
for (HashFunction f : functions) {
//計算 hash 值並修改 bitmap 中相應位置為 true
bitset.set(f.hash(value), true);
}
}
}
/**
* 判斷相應元素是否存在
* @param value 需要判斷的元素
* @return 結果
*/
public static boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (HashFunction f : functions) {
ret = bitset.get(f.hash(value));
//一個 hash 函式返回 false 則跳出迴圈
if (!ret) {
break;
}
}
return ret;
}
/**
* 模擬使用者是不是會員,或使用者在不線上。。。
*/
public static void main(String[] args) {
for (int i = 0; i < seeds.length; i++) {
functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
}
// 新增1億資料
for (int i = 0; i < 100000000; i++) {
add(String.valueOf(i));
}
String id = "123456789";
add(id);
System.out.println(contains(id)); // true
System.out.println("" + contains("234567890")); //false
}
}
class HashFunction {
private int size;
private int seed;
public HashFunction(int size, int seed) {
this.size = size;
this.seed = seed;
}
public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
int r = (size - 1) & result;
return (size - 1) & result;
}
}
```
> What?我們寫的這些早有大牛幫我們實現,還造輪子,真是浪費時間,No,No,No,我們學習過程中是可以造輪子的,造輪子本身就是我們自己對設計和實現的具體落地過程,不僅能提高我們的程式設計能力,在造輪子的過程中肯定會遇到很多我們沒有思考過的問題,成長看的見~~
> 實際專案使用的時候,領導和我說專案一定要穩定執行,沒自信的我放棄了自己的輪子。
### Guava 中的 BloomFilter
```xml
```
```java
public class GuavaBloomFilterDemo {
public static void main(String[] args) {
//後邊兩個引數:預計包含的資料量,和允許的誤差值
Blo