1. 程式人生 > >大數據量下的集合過濾—Bloom Filter

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

pos 方案 基本 put mar pac tail nsstring png

算法背景

如果想判斷一個元素是不是在一個集合裏,一般想到的是將集合中所有元素保存起來,然後通過比較確定。鏈表、樹、散列表(又叫哈希表,Hash table)等等數據結構都是這種思路,存儲位置要麽是磁盤,要麽是內存。很多時候要麽是以時間換空間,要麽是以空間換時間。

在響應時間要求比較嚴格的情況下,如果我們存在內裏,那麽隨著集合中元素的增加,我們需要的存儲空間越來越大,以及檢索的時間越來越長,導致內存開銷太大、時間效率變低。

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

Bloom Filter 概念

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

Bloom Filter 原理

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

Bloom Filter跟單哈希函數Bit-Map不同之處在於:Bloom Filter使用了k個哈希函數,每個字符串跟k個bit對應。從而降低了沖突的概率。

技術分享圖片

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,

在實現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 math,Bloom_filter-wikipedia

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

/** 
 * 計算 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為例:

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

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 的大小就能解決同樣的問題。

參考文章

guava 布隆過濾器

那些優雅的數據結構(1) : BloomFilter——大規模數據處理利器

哈希表存儲效率50%的原因

https://blog.csdn.net/hfmbook/article/details/70209184

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