BloomFilter

目錄

 

一、布隆過濾器

二、爲什麼要用布隆過濾器?

三、布隆過濾器應用場景

四、布隆過濾器的簡單實現


一、布隆過濾器

布隆過濾器(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;
    }

在實際業務中,可在實現類初始化時,將所需判斷的所有數據查詢導入布隆過濾器,在海量數據中,查詢操作也要做對應的優化。

 

文章參考:

說一說布隆過濾器

Bloom Filter概念和原理

布隆過濾器(Bloom Filter)詳解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章