大數據量下的集合過濾—Bloom Filter

算法背景

若是想判斷一個元素是否是在一個集合裏,通常想到的是將集合中全部元素保存起來,而後經過比較肯定。鏈表、樹、散列表(又叫哈希表,Hash table)等等數據結構都是這種思路,存儲位置要麼是磁盤,要麼是內存。不少時候要麼是以時間換空間,要麼是以空間換時間。html

在響應時間要求比較嚴格的狀況下,若是咱們存在內裏,那麼隨着集合中元素的增長,咱們須要的存儲空間愈來愈大,以及檢索的時間愈來愈長,致使內存開銷太大、時間效率變低。java

 

此時須要考慮解決的問題就是,在數據量比較大的狀況下,既知足時間要求,又知足空間的要求。即咱們須要一個時間和空間消耗都比較小的數據結構和算法。Bloom Filter就是一種解決方案。node

 

Bloom Filter 概念

布隆過濾器(英語:Bloom Filter)是1970年由布隆提出的。它其實是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器能夠用於檢索一個元素是否在一個集合中。它的優勢是空間效率和查詢時間都遠遠超過通常的算法,缺點是有必定的誤識別率和刪除困難。redis

 

Bloom Filter 原理

 

布隆過濾器的原理是,當一個元素被加入集合時,經過K個散列函數將這個元素映射成一個位數組中的K個點,把它們置爲1。檢索時,咱們只要看看這些點是否是都是1就(大約)知道集合中有沒有它了:若是這些點有任何一個0,則被檢元素必定不在;若是都是1,則被檢元素極可能在。這就是布隆過濾器的基本思想。算法

 

 

Bloom Filter跟單哈希函數Bit-Map不一樣之處在於:Bloom Filter使用了k個哈希函數,每一個字符串跟k個bit對應。從而下降了衝突的機率。api

 

 

Bloom Filter的缺點

 

bloom filter之因此能作到在時間和空間上的效率比較高,是由於犧牲了判斷的準確率、刪除的便利性數組

  • 存在誤判,可能要查到的元素並無在容器中,可是hash以後獲得的k個位置上值都是1。若是bloom filter中存儲的是黑名單,那麼能夠經過創建一個白名單來存儲可能會誤判的元素。
  • 刪除困難。一個放入容器的元素映射到bit數組的k個位置上是1,刪除的時候不能簡單的直接置爲0,可能會影響其餘元素的判斷。能夠採用Counting Bloom Filter

 

 

Bloom Filter 實現

布隆過濾器有許多實現與優化,Guava中就提供了一種Bloom Filter的實現。數據結構

 

在使用bloom filter時,繞不過的兩點是預估數據量n以及指望的誤判率fpp,dom

在實現bloom filter時,繞不過的兩點就是hash函數的選取以及bit數組的大小。數據結構和算法

 

對於一個肯定的場景,咱們預估要存的數據量爲n,指望的誤判率爲fpp,而後須要計算咱們須要的Bit數組的大小m,以及hash函數的個數k,並選擇hash函數

 

(1)Bit數組大小選擇 

     根據預估數據量n以及誤判率fpp,bit數組大小的m的計算方式:

 

(2)哈希函數選擇

           由預估數據量n以及bit數組長度m,能夠獲得一個hash函數的個數k:

           哈希函數的選擇對性能的影響應該是很大的,一個好的哈希函數要能近似等機率的將字符串映射到各個Bit。選擇k個不一樣的哈希函數比較麻煩,一種簡單的方法是選擇一個哈希函數,而後送入k個不一樣的參數。

 

 

哈希函數個數k、位數組大小m、加入的字符串數量n的關係能夠參考Bloom Filters - the mathBloom_filter-wikipedia

 

看看Guava中BloomFilter中對於m和k值計算的實現,在com.google.common.hash.BloomFilter類中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
  * 計算 Bloom Filter的bit位數m
  *
  * <p>See http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives for the
  * formula.
  *
  * @param n 預期數據量
  * @param p 誤判率 (must be 0 < p < 1)
  */ 
@VisibleForTesting 
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 ))); 
    
    
    
    
/**
  * 計算最佳k值,即在Bloom過濾器中插入的每一個元素的哈希數
  *
  * <p>See http://en.wikipedia.org/wiki/File:Bloom_filter_fp_probability.svg for the formula.
  *
  * @param n 預期數據量
  * @param m bloom filter中總的bit位數 (must be positive)
  */ 
@VisibleForTesting 
static  int  optimalNumOfHashFunctions( long  n,  long  m) { 
   // (m / n) * log(2), but avoid truncation due to division! 
   return  Math.max( 1 , ( int ) Math.round(( double ) m / n * Math.log( 2 ))); 

  

BloomFilter實現的另外一個重點就是怎麼利用hash函數把數據映射到bit數組中。Guava的實現是對元素經過MurmurHash3計算hash值,將獲得的hash值取高8個字節以及低8個字節進行計算,以得當前元素在bit數組中對應的多個位置。MurmurHash3算法詳見:Murmur哈希,於2008年被髮明。這個算法hbase,redis,kafka都在使用。

 

這個過程的實如今兩個地方:

  • 將數據放入bloom filter中
  • 判斷數據是否已在bloom filter中

這兩個地方的實現大同小異,區別只是,前者是put數據,後者是查數據。

 

這裏看一下put的過程,hash策略以MURMUR128_MITZ_64爲例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public  <T>  boolean  put( 
     T object, Funnel<?  super  T> funnel,  int  numHashFunctions, LockFreeBitArray bits) { 
   long  bitSize = bits.bitSize(); 
    
   //利用MurmurHash3獲得數據的hash值對應的字節數組 
   byte [] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal(); 
    
    
   //取低8個字節、高8個字節,轉成long類型 
   long  hash1 = lowerEight(bytes); 
   long  hash2 = upperEight(bytes); 
    
   boolean  bitsChanged =  false
    
    
   //這裏的combinedHash = hash1 + i * hash2 
   long  combinedHash = hash1; 
    
    
   //根據combinedHash,獲得放入的元素在bit數組中的k個位置,將其置1 
   for  ( int  i =  0 ; i < numHashFunctions; i++) { 
     bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize); 
     combinedHash += hash2; 
  
   return  bitsChanged; 

  

判斷元素是否在bloom filter中的方法mightContain與上面的實現基本一致,再也不贅述。

 

Bloom Filter的使用

 

簡單寫個demo,用法很簡單,相似HashMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package  com.qunar.sage.wang.common.bloom.filter; 
    
import  com.google.common.base.Charsets; 
import  com.google.common.hash.BloomFilter; 
import  com.google.common.hash.Funnel; 
import  com.google.common.hash.Funnels; 
import  com.google.common.hash.PrimitiveSink; 
import  lombok.AllArgsConstructor; 
import  lombok.Builder; 
import  lombok.Data; 
import  lombok.ToString; 
    
/**
  * BloomFilterTest
  *
  * @author sage.wang
  * @date 18-5-14 下午5:02
  */ 
public  class  BloomFilterTest { 
        
     public  static  void  main(String[] args) { 
         long  expectedInsertions =  10000000
         double  fpp =  0.00001
    
         BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp); 
    
         bloomFilter.put( "aaa" ); 
         bloomFilter.put( "bbb" ); 
         boolean  containsString = bloomFilter.mightContain( "aaa" ); 
         System.out.println(containsString); 
    
         BloomFilter<Email> emailBloomFilter = BloomFilter 
                 .create((Funnel<Email>) (from, into) -> into.putString(from.getDomain(), Charsets.UTF_8), 
                         expectedInsertions, fpp); 
    
         emailBloomFilter.put( new  Email( "sage.wang" "quanr.com" )); 
         boolean  containsEmail = emailBloomFilter.mightContain( new  Email( "sage.wangaaa" "quanr.com" )); 
         System.out.println(containsEmail); 
    
    
     @Data 
     @Builder 
     @ToString 
     @AllArgsConstructor 
     public  static  class  Email { 
         private  String userName; 
         private  String domain; 
    
    

  

Bloom Filter的應用

 

常見的幾個應用場景:

  • cerberus在收集監控數據的時候, 有的系統的監控項量會很大, 須要檢查一個監控項的名字是否已經被記錄到db過了, 若是沒有的話就須要寫入db.
  • 爬蟲過濾已抓到的url就再也不抓,可用bloom filter過濾
  • 垃圾郵件過濾。若是用哈希表,每存儲一億個 email地址,就須要 1.6GB的內存(用哈希表實現的具體辦法是將每個 email地址對應成一個八字節的信息指紋,而後將這些信息指紋存入哈希表,因爲哈希表的存儲效率通常只有 50%,所以一個 email地址須要佔用十六個字節。一億個地址大約要 1.6GB,即十六億字節的內存)。所以存貯幾十億個郵件地址可能須要上百 GB的內存。而Bloom Filter只須要哈希表 1/8到 1/4 的大小就能解決一樣的問題。
相關文章
相關標籤/搜索