目錄
一、布隆過濾器
布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。
算法優勢:
- 僅僅保留數據的指紋信息,空間效率極高(指紋信息:根據隨意映射函數找到對應的二進制向量位置,並標記該位置)
- 查詢效率極高,時間複雜度爲:O(n)
- 信息安全性較高
算法不足:
- 存在一定的誤判
- 數據刪除困難
如果想判斷一個元素是不是在一個集合裏,一般想到的是將集合中所有元素保存起來,然後通過比較確定。鏈表、樹、散列表(又叫哈希表,Hash table)等等數據結構都是這種思路。但是隨着集合中元素的增加,我們需要的存儲空間越來越大。同時檢索速度也越來越慢,上述三種結構的檢索時間複雜度分別爲:O(n), O(log n), O(n/k)。
布隆過濾器的原理是,當一個元素被加入集合時,通過K個Hash函數將這個元素映射成一個位數組中的K個點,把它們置爲1。檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:如果這些點有任何一個0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。
二、爲什麼要用布隆過濾器?
通過介紹已經知曉布隆過濾器的作用是檢索一個元素是否在集合中。可能有人認爲這個功能非常簡單,直接放在redis中或者數據庫中查詢就好了。又或者當數據量較小,內存又足夠大時,使用hashMap或者hashSet等結構就好了。但是如果當這些數據量很大,數十億甚至更多,內存裝不下且數據庫檢索又極慢的情況,我們應該如何去處理?這個時候我們不妨考慮下布隆過濾器,因爲它是一個空間效率佔用極少和查詢時間極快的算法,但是需要業務可以忍受一個判斷失誤率。
三、布隆過濾器應用場景
- 網頁黑名單系統
- 垃圾郵件過濾系統
- 爬蟲的網址判重系統
- 解決緩存穿透
對布隆過濾器算法比較感興趣的朋友,可以自行搜索下,這裏暫時不展示
四、布隆過濾器的簡單實現
google已經將布隆過濾器封裝進jar包,可直接引入依賴調用實現
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
PS:guava包在19.0版本以上才封裝有布隆過濾器的實現
testDemo code
public void testDemo(){
//初始化個數
int initNumber = 200000;
//初始化3個存儲String類型的容器
BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), initNumber,0.01);
Set<String> sets = new HashSet<String>(initNumber);
List<String> lists = new ArrayList<String>(initNumber);
//向三個容器存入隨機字符串數據
for (int i = 0; i < initNumber; i++) {
String uuid = UUID.randomUUID().toString();
bf.put(uuid);
sets.add(uuid);
lists.add(uuid);
}
int wrong = 0;//正確個數
int right = 0;//錯誤個數
int testNumber = 10000;
for (int i = 0; i < testNumber; i++) {
//按照一定比例的選擇bf內肯定存在的值
String test = i%100 == 0 ? lists.get(i/100) : UUID.randomUUID().toString();
if(bf.mightContain(test)){
if(sets.contains(test)){
right++;
}else{
wrong++;
}
}
}
System.out.println("正確個數:"+ right);
System.out.println("錯誤個數:"+ wrong);
System.out.println("錯誤率 :"+ ((wrong*1.0)/(testNumber*1.0)));
}
這是簡單的調用實現,BloomFilter.create 有三個參數
- Funnel:描述瞭如何把一個具體的對象類型分解爲原生字段值,從而寫入
- expectedInsertions:插入一個確定的初始化數值
- fpp:期望的錯誤率,默認0.03
進入源碼查看創建過程
static <T> BloomFilter<T> create(
Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
checkNotNull(funnel);
checkArgument(
expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
checkNotNull(strategy);
if (expectedInsertions == 0) {
expectedInsertions = 1;
}
/*
* TODO(user): Put a warning in the javadoc about tiny fpp values,
* since the resulting size is proportional to -log(p), but there is not
* much of a point after all, e.g. optimalM(1000, 0.0000000000000001) = 76680
* which is less than 10kb. Who cares!
*/
//根據初始化數據大小和錯誤率,計算出可用數組長度
long numBits = optimalNumOfBits(expectedInsertions, fpp);
//根據初始化數據大小和數組長度,計算需要使用到的hash容器個數
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);
}
}
PS:計算數組長度和容器個數的所需數量,是開發者們測試得出來的較爲可靠的值,數據展示在對應方法註釋內有提供參考鏈接,這裏就不展示。
如果失誤率越低,所需的數組空間也就越大,採用空間作爲代價,實際場景根據業務自行調整。
插入過程
public <T> boolean put(
T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
long bitSize = bits.bitSize();
//這裏使用hash算法進行hash運算
byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
//獲取字節數組的低8位進行運算
long hash1 = lowerEight(bytes);
//獲取字節數組的高8位進行運算
long hash2 = upperEight(bytes);
boolean bitsChanged = false;
long combinedHash = hash1;
//遍歷容器個數,更改對應的位置標識
for (int i = 0; i < numHashFunctions; i++) {
// Make the combined hash positive and indexable
bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
combinedHash += hash2;
}
return bitsChanged;
}
判斷元素是否存在,原理同插入相似
public <T> boolean mightContain(
T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
long bitSize = bits.bitSize();
byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
long hash1 = lowerEight(bytes);
long hash2 = upperEight(bytes);
long combinedHash = hash1;
for (int i = 0; i < numHashFunctions; i++) {
// Make the combined hash positive and indexable
if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) {
return false;
}
combinedHash += hash2;
}
return true;
}
在實際業務中,可在實現類初始化時,將所需判斷的所有數據查詢導入布隆過濾器,在海量數據中,查詢操作也要做對應的優化。
文章參考: