BloomFilter
概述
- 現在有一個非常龐大的數據,假設全是 int 類型。現在我給你一個數,你需要告訴我它是否存在其中(盡量高效)。
- 我想大多數想到的都是用 HashMap 來存放數據,因為它的寫入查詢的效率都比較高。但是在內存有限的情況下我們不能使用這種方式,因為很容易導致內存溢出
- 因為只是需要判斷數據是否存在,也不是需要把數據查詢出來,所以完全沒有必要將真正的數據存放進去,Bloom Filter就是為了解決這個問題,它主要就是用於解決判斷一個元素是否在一個集合中,但它的優勢是只需要占用很小的內存空間以及有着高效的查詢效率。
Bloom Filter 原理

- 首先需要初始化一個二進制的數組,長度設為 L(圖中為 8),同時初始值全為 0 。
- 當寫入一個 A1=1000 的數據時,需要進行 H 次 hash 函數的運算(這里為 2 次);與 HashMap 有點類似,通過算出的 HashCode 與 L 取模后定位到 0、2 處,將該處的值設為 1。
- A2=2000 也是同理計算后將 4、7 位置設為 1。
- 當有一個 B1=1000 需要判斷是否存在時,也是做兩次 Hash 運算,定位到 0、2 處,此時他們的值都為 1 ,所以認為 B1=1000 存在於集合中。
- 當有一個 B2=3000 時,也是同理。第一次 Hash 定位到 index=4 時,數組中的值為 1,所以再進行第二次 Hash 運算,結果定位到 index=5 的值為 0,所以認為 B2=3000 不存在於集合中。
布隆過濾有以下幾個特點:
- 只要返回數據不存在,則肯定不存在。
- 返回數據存在,但只能是大概率存在。
- 同時不能清除其中的數據。
- 為什么返回存在的數據卻是可能存在呢,這其實也和 HashMap 類似。在有限的數組長度中存放大量的數據,即便是再完美的 Hash 算法也會有沖突,所以有可能兩個完全不同的 A、B 兩個數據最后定位到的位置是一模一樣的。這時拿 B 進行查詢時那自然就是誤報了。刪除數據也是同理,當我把 B 的數據刪除時,其實也相當於是把 A 的數據刪掉了,這樣也會造成后續的誤報。基於以上的 Hash 沖突的前提,所以 Bloom Filter 有一定的誤報率,這個誤報率和 Hash 算法的次數 H,以及數組長度 L 都是有關的。
實現一個布隆過濾
public class BloomFilters {
/**
* 數組長度
*/
private int arraySize;
/**
* 數組
*/
private int[] array;
public BloomFilters(int arraySize) {
this.arraySize = arraySize;
array = new int[arraySize];
}
/**
* 寫入數據
* @param key
*/
public void add(String key) {
int first = hashcode_1(key);
int second = hashcode_2(key);
int third = hashcode_3(key);
array[first % arraySize] = 1;
array[second % arraySize] = 1;
array[third % arraySize] = 1;
}
/**
* 判斷數據是否存在
* @param key
* @return
*/
public boolean check(String key) {
int first = hashcode_1(key);
int second = hashcode_2(key);
int third = hashcode_3(key);
int firstIndex = array[first % arraySize];
if (firstIndex == 0) {
return false;
}
int secondIndex = array[second % arraySize];
if (secondIndex == 0) {
return false;
}
int thirdIndex = array[third % arraySize];
if (thirdIndex == 0) {
return false;
}
return true;
}
/**
* hash 算法1
* @param key
* @return
*/
private int hashcode_1(String key) {
int hash = 0;
int i;
for (i = 0; i < key.length(); ++i) {
hash = 33 * hash + key.charAt(i);
}
return Math.abs(hash);
}
/**
* hash 算法2
* @param data
* @return
*/
private int hashcode_2(String data) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < data.length(); i++) {
hash = (hash ^ data.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return Math.abs(hash);
}
/**
* hash 算法3
* @param key
* @return
*/
private int hashcode_3(String key) {
int hash, i;
for (hash = 0, i = 0; i < key.length(); ++i) {
hash += key.charAt(i);
hash += (hash << 10);
hash ^= (hash >> 6);
}
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
return Math.abs(hash);
}
}
- 首先初始化了一個 int 數組。
- 寫入數據的時候進行三次 hash 運算,同時把對應的位置置為 1。
- 查詢時同樣的三次 hash 運算,取到對應的值,一旦值為 0 ,則認為數據不存在。
Guava 實現
- Google Guava 庫中也實現了該算法,下面來看看業界權威的實現。
@Test
public void guavaTest() {
long star = System.currentTimeMillis();
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(),
10000000,
0.01);
for (int i = 0; i < 10000000; i++) {
filter.put(i);
}
Assert.assertTrue(filter.mightContain(1));
Assert.assertTrue(filter.mightContain(2));
Assert.assertTrue(filter.mightContain(3));
Assert.assertFalse(filter.mightContain(10000000));
long end = System.currentTimeMillis();
System.out.println("執行時間:" + (end - star));
}
- 源碼分析
- 構造方法中有兩個比較重要的參數,一個是預計存放多少數據,一個是可以接受的誤報率。我這里的測試 demo 分別是 1000W 以及 0.01。Guava 會通過你預計的數量以及誤報率幫你計算出你應當會使用的數組大小 numBits 以及需要計算幾次 Hash 函數 numHashFunctions 。
應用場景
- 布隆過濾的應用還是蠻多的,比如數據庫、爬蟲、防緩存擊穿等。
- yahoo, gmail等郵箱垃圾郵件過濾功能