在前一篇文章中我們介紹了散列表和BitMap的相關概念與部分應用。本文將會具體講解BitMap的擴展:布隆過濾器(Bloom filter)。
概念
Hash表實際上爲每一個可能出現的數字提供了一個一一映射的關係,每個元素都相當於有了自己的獨享的一份空間,這個映射由散列函數來提供。Hash表甚至還能記錄每個元素出現的次數,利用這一點可以實現更復雜的功能。我們的需求是集合中每個元素有一個獨享的空間並且能找到一個到這個空間的映射方法。獨享的空間對於我們的問題來說,一個Boolean就夠了,或者說,1個bit就夠了,我們只想知道某個元素出現過沒有。如果爲每個所有可能的值分配1個bit,這就是BitMap所要完成的工作。然而當數據量大到一定程度,所需要的存儲空間將會超出可承受的範圍,如寫64bit類型的數據,需要大概2EB存儲。
布隆過濾器(Bloom Filter)是1970年由布隆提出的。布隆過濾器可以用於檢索一個元素是否在一個集合中。布隆過濾器是一種空間效率極高的概率型算法和數據結構,它實際上是一個很長的二進制向量和一系列隨機映射函數。BitMap對於每一個可能的整型值,通過直接尋址的方式進行映射,相當於使用了一個哈希函數,而布隆過濾器就是引入了k(k>1)個相互獨立的哈希函數,保證在給定的空間、誤判率下,完成元素判重的過程。
算法描述
集合表示與元素查詢
具體來看Bloom Filter是如何用位數組表示集合的。初始狀態時,Bloom Filter是一個包含m位的位數組,每一位都置爲0。
Bloom Filter使用k個相互獨立的哈希函數(Hash Function),它們分別將集合中的每個元素映射到{1,…,m}
的範圍中。對任意一個元素x,第i個哈希函數映射的位置hash_i(x)就會被置爲1(1≤i≤k)。
當一個元素被加入集合中時,通過k各散列函數將這個元素映射成一個位數組中的k個點,並將這k個點全部置爲1。下圖是k=3時的布隆過濾器。
x、y、z經由哈希函數映射將各自在Bitmap中的3個位置置爲1,當w出現時,僅當3個標誌位都爲1時,才表示w在集合中。圖中所示的情況,布隆過濾器將判定w不在集合中。
錯誤率
Bloom Filter有一定的誤判率。在判斷一個元素是否屬於某個集合時,有可能會把不屬於這個集合的元素誤判爲屬於這個集合。因此,它不適合那些"零誤判"的應用場合。在能容忍低誤判的應用場景下,布隆過濾器通過極少的誤判換區了存儲空間的極大節省。
那麼布隆過濾器的誤差有多少?我們假設所有哈希函數散列足夠均勻,散列後落到Bitmap每個位置的概率均等。Bitmap的大小爲m、原始數集大小爲n、哈希函數個數爲k:
- k個相互獨立的散列函數,接收一個元素時Bitmap中某一位置爲0的概率爲:
- 假設原始集合中,所有元素都不相等(最嚴格的情況),將所有元素都輸入布隆過濾器,此時某一位置仍爲0的概率爲:
某一位置爲1的概率爲:
- 當我們對某個元素進行判重時,誤判即這個元素對應的k個標誌位不全爲1,但所有k個標誌位都被置爲1,誤判率ε約爲:
場景
布隆過濾器的最大的用處就是,能夠迅速判斷一個元素是否在一個集合中。因此他有如下三個使用場景:
- 網頁爬蟲對URL的去重,避免爬取相同的URL地址
- 反垃圾郵件,從數十億個垃圾郵件列表中判斷某郵箱是否垃圾郵箱(同理,垃圾短信)
- 緩存擊穿,將已存在的緩存放到布隆過濾器中,當黑客訪問不存在的緩存時迅速返回避免緩存及DB掛掉。 緩存系統中,按照KEY去查詢VALUE,當KEY對應的VALUE一定不存在的時候並對KEY併發請求量很大的時候,就會對後端造成很大的壓力。如果緩存集中在一段時間內失效,發生大量的緩存穿透,所有的查詢都落在數據庫上,造成了緩存雪崩。
由於緩存不命中,每次都要查詢持久層。從而失去緩存的意義。 這裏只要增加一個bloom算法的服務,服務端插入一個key時,在這個服務中設置一次。需要查詢服務端時,先判斷key在後端是否存在,這樣就能避免服務端的壓力。
實現與應用
下面我們介紹使用Google實現的BloomFilter
。
引入依賴
1 <dependency> 2 <groupId>com.google.guava</groupId> 3 <artifactId>guava</artifactId> 4 </dependency>
查找某個元素
1 private static int size = 1000000; 2 3 private static BloomFilter<Integer> bloomFilter = 4 BloomFilter.create(Funnels.integerFunnel(), size); 5 6 @Test 7 public void consumeTest() { 8 for (int i = 0; i < size; i++) { 9 bloomFilter.put(i); 10 } 11 long startTime = System.nanoTime(); // 獲取開始時間 12 13 // 判斷這一百萬個數中是否包含29999這個數 14 15 if (bloomFilter.mightContain(29999)) { 16 System.out.println("命中了"); 17 } 18 long endTime = System.nanoTime(); // 獲取結束時間 19 System.out.println("程序運行時間: " + (endTime - startTime) + "納秒"); 20 }
使用BloomFilter
查找一個元素29999,非常快速。
誤判率
1 private static int size = 1000000; 2 3 private static BloomFilter<Integer> bloomFilter = 4 BloomFilter.create(Funnels.integerFunnel(), size); 5 6 @Test 7 public void errorTest() { 8 9 for (int i = 0; i < size; i++) { 10 bloomFilter.put(i); 11 } 12 13 List<Integer> list = new ArrayList<>(1000); 14 // 取10000個不在過濾器裏的值,看看有多少個會被認爲在過濾器裏 15 for (int i = size + 10000; i < size + 20000; i++) { 16 if (bloomFilter.mightContain(i)) { 17 list.add(i); 18 } 19 } 20 System.out.println("誤判的數量:" + list.size()); 21 }
上述代碼所示,我們取10000個不在過濾器裏的值,卻還有330個被認爲在過濾器裏,這說明了誤判率爲0.03。即,在不做任何設置的情況下,默認的誤判率爲0.03。
BloomFilter
默認的構造函數如下:
1 @CheckReturnValue 2 public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) { 3 return create(funnel, expectedInsertions, 0.03D); 4 }
當然我們可以通過如下的構造函數,手動設置誤判率。
1private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,0.01);
實際應用
1 static int sizeOfNumberSet = Integer.MAX_VALUE >> 4; 2 3 static Random generator = new Random(); 4 5 @Test 6 public void actualTest() { 7 int error = 0; 8 HashSet<Integer> hashSet = new HashSet<Integer>(); 9 BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), sizeOfNumberSet); 10 System.out.println(sizeOfNumberSet); 11 for (int i = 0; i < sizeOfNumberSet; i++) { 12 int number = generator.nextInt(); 13 if (filter.mightContain(number) != hashSet.contains(number)) { 14 error++; 15 } 16 filter.put(number); 17 hashSet.add(number); 18 } 19 20 System.out.println( 21 "Error count: " 22 + error 23 + ", error rate = " 24 + String.format("%f", (float) error / (float) sizeOfNumberSet)); 25 }
BloomFilter
實際的應用類似如上所示,換成redis客戶端調用即可,用於redis緩存擊穿等場景。
總結
本文主要講了布隆過濾器相關概念、算法描述、錯誤率統計和布隆過濾器的實現與應用。布隆過濾器是BitMap的一種工業實現,解決了使用BitMap時當數據量大到一定程度,所需要的存儲空間將會超出可承受的範圍的問題。
布隆過濾器就是引入了k(k>1)個相互獨立的哈希函數,保證在給定的空間、誤判率下,完成元素判重的過程。布隆過濾器有一個誤判率的概念,誤判率越低,則數組越長,所佔空間越大。誤判率越高則數組越小,所佔的空間越小。最後,我們通過Google實現的BloomFilter
,介紹如何使用布隆過濾器並自定義調整誤判率。
相比於其它的數據結構,布隆過濾器在空間和時間方面都有巨大的優勢。布隆過濾器存儲空間和插入/查詢時間都是常數(O(k))。哈希表也能用於判斷元素是否在集合中,但是布隆過濾器只需要哈希表的1/8或1/4的空間複雜度就能完成同樣的問題。
布隆過濾器的缺點除了誤算率之外(隨着存入的元素數量增加,誤算率隨之增加。但是如果元素數量太少,則使用散列表足矣),不能從布隆過濾器中刪除元素。我們很容易想到把位數組變成整數數組,每插入一個元素相應的計數器加1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全地刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器裏面。這一點單憑這個過濾器是無法保證的。
參考
- 大量數據去重:Bitmap和布隆過濾器(Bloom Filter) https://blog.csdn.net/zdxiq000/article/details/57626464
- 布隆過濾器 (Bloom Filter) 詳解 http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html
- Bloom Filter布隆過濾器 https://blog.csdn.net/pipisorry/article/details/64127666