原文地址:https://www.inlighting.org/archives/java-implement-bloom-filter/
布隆過濾器可以使用極少的空間來判斷一個元素是否存在某一個集合中,本文不具體討論布隆過濾器的原理,而是探討如何實現(xiàn)一個可用的布隆過濾器。
實現(xiàn)源代碼:https://github.com/Smith-Cruise/java-bloom-filter
本文代碼提取自 Apache ORC 項目。
基本概念
這里附帶一些鏈接,適合不了解布隆過濾器的人閱讀。
誤算率 False Positive
使用布隆過濾器時要注意:一個元素經(jīng)過計算不存在時,那它一定不存在該集合中。但是如果一個元素經(jīng)過計算存在時,它并不一定存在該集合中,所以其產(chǎn)生了一定的誤算率。
所以設計一個可用的布隆過濾器,必須要確保它的誤算率低。
這里我們令布隆過濾器 hasn 函數(shù)個數(shù)為 ,有
個 bits,期望插入數(shù)字個數(shù)為
,誤算率為
。
最佳 hash 函數(shù)數(shù)量計算公式
為此值能夠確保誤算率最低。
最佳 bits 個數(shù)計算公式
想知道上面兩個公式如何推導的,可以參考:https://en.wikipedia.org/wiki/Bloom_filter#Optimal_number_of_hash_functions
這里我們假設希望誤算率維持在 5%,預期會有 100 個元素插入。
故該布隆過濾器的大小應有 640 個 bits 和 4 個 hash 函數(shù)。
Hash 函數(shù)設計
一個好的 Hash 函數(shù)就是要降低碰撞率,這里我們使用 Murmur Hash 3 作為 Hash 函數(shù),它計算速度快,而且經(jīng)過廣泛的測試。
Hash 函數(shù)的實現(xiàn)已經(jīng)附帶在源碼中了。
Bitset
Java 作為一門高級語言,沒有直接提供傳統(tǒng)的 bit 操作。但是我們可以參考官方的 Bitset 源碼實現(xiàn),自己設計一個 Bitset。之所以不使用官方的 Bitset,是因為其過為復雜,沒有必要。
我們使用 long 數(shù)組來存放每一個 bit。在 Java 中,一個 long 占用 64 個 bit,這樣可以一次性多存點。
序列化
我們生成的布隆過濾器肯定要支持持久化到磁盤中,這里我們使用 protobuf 來作為序列化框架。序列化的 proto 文件代碼如下:
message BloomFilter {
required uint32 numHashFunctions = 1;
repeated fixed64 bitset = 2;
}
我們只要保存 hash 函數(shù)的個數(shù)和 bitset 即可。bitset 使用 fixed64 類型是因為 long 占用 64 個 bit,bitset 使用 repeated 修飾是因為其為一個 long 數(shù)組。
源碼食用方法
首先系統(tǒng)安裝完成 protoc,用于編譯 protobuf 文件。
在源碼目錄執(zhí)行命令 protoc --java_out=src/main/java src/main/resources/BloomFilter.proto
。
測試代碼如下,先生成一個布隆過濾器,填充值后序列化到磁盤中名為 bloom_filter
文件。然后讀取文件,檢測某個值是否添加過:
package org.inlighting;
import org.inlighting.proto.BloomFilterProtos;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class Test {
private static final String STORE_PATH = "bloom_filter";
public static void main(String[] args) throws IOException {
// Serialize to bloom_filter to disk.
{
BloomFilter bloomFilter = new BloomFilter(10, 0.05);
System.out.println(bloomFilter.toString());
bloomFilter.addString("Hello World");
bloomFilter.addLong(2L);
bloomFilter.addLong(1);
bloomFilter.add("ni".getBytes(StandardCharsets.UTF_8));
BloomFilterProtos.BloomFilter.Builder builder = BloomFilterProtos.BloomFilter.newBuilder();
BloomFilterIO.serialize(builder, bloomFilter);
builder.build().writeTo(new FileOutputStream(STORE_PATH));
}
// read from bloom_filter
{
BloomFilterProtos.BloomFilter bloomFilterProtos =
BloomFilterProtos.BloomFilter.parseFrom(new FileInputStream(STORE_PATH));
BloomFilter bloomFilter = BloomFilterIO.deserialize(bloomFilterProtos);
System.out.println(bloomFilter.testString("Hello World")); // true
System.out.println(bloomFilter.testLong(2)); // true
System.out.println(bloomFilter.testLong(2L)); // true
System.out.println(bloomFilter.testLong(1)); // true
System.out.println(bloomFilter.testLong(1L)); // true
System.out.println(bloomFilter.test("ni".getBytes(StandardCharsets.UTF_8))); // true
System.out.println(bloomFilter.test("hao".getBytes(StandardCharsets.UTF_8))); // false
System.out.println(bloomFilter.testString("hello world")); // false
System.out.println(bloomFilter.testLong(3)); // false
}
}
}
輸出結果:
numBits: 64, numHashFunctions: 4
true
true
true
true
true
true
false
false
false
生成的 BloomFilter 占用空間 11B,比較理想。
后序看有沒有必要把這個封裝成一個 jar 包上傳到 Maven 中央倉庫供調用。
結尾附帶一個 Bloom Filter 計算器,可以在線計算需要的 hash 函數(shù)的個數(shù)和空間:https://krisives.github.io/bloom-calculator/