【数据结构】布隆过滤器:BloomFilter原理及Java实现
布隆過濾器(Bloom Filter)是一個叫做 Bloom 的大佬在1970年提出的。我們可以把它看做由二進制向量(或者說數組)和一系列隨機映射函數(哈希函數)兩部分組成的數據結構。相比于我們平時用的 List,Set,Map 等容器,它占用的空間更少且效率更高,但缺點是其返回的結果是概率性的,而不是非常準確的。理論情況下添加到集合中的元素越多,誤報的可能性就越大。并且,存放在布隆過濾器的數據不容易刪除。
它的基本思想是:當一個元素被加入集合時,通過 K 個散列函數將這個元素映射成一個位數組中的 K 個點,把它們置為1;檢索時,只要看看這些點是不是都是1 就(大約)知道集合中有沒有它了——如果這些點有任何一個0,則被檢元素一定不在,如果都是1,則被檢元素很可能在。
1.原理
布隆過濾器(Bloom Filter)核心實現是一個超大的位數組和幾個哈希函數。如下圖所示,有一個大小為 12 的數組,然后有3個哈希函數:
1.1 流程分析
假如現在有一個 keyword 要放入某個容器了,現在要經過Bloom Filter,具體的操作流程如下:
過濾器初始化:將位數組進行初始化,即數組每位都設置位0
放入元素:
判斷存在:查詢W元素是否存在集合中的時候,同樣的方法將W通過哈希映射到位數組上的3個點
- 如果3個點的其中有一個點不為1,則可以判斷該元素一定不存在集合中
- 如果3個點都為1,則該元素可能存在集合中
因為此處不能判斷該元素是否一定存在集合中,可能存在一定的誤判率。
1.2 誤差分析
可以從圖中可以看到,向容器中 put 時,x1映射到(2/4/8),x2映射到(4/6/10):
假設現在要判斷元素 W 是否在容器中,通過哈希函數計算出的結果是(4/6/8),雖然指定位都為1,但顯然沒有這個元素。因此這種情況說明元素雖然不在集合中,也可能對應的都是1,這是誤判率存在的原因。
2.應用場景
在很多場景下,我們都需要一個能迅速判斷一個元素是否在一個集合中。譬如:
問題:
可能有人會問,我們直接把這些數據都放到數據庫或者redis之類的緩存中不就行了,查詢時直接匹配不就OK了?
是的,當這個集合量比較小,你內存又夠大時,是可以這樣做,你可以直接弄個HashSet、HashMap就OK了。但是當這個量以數十億計,內存裝不下,數據庫檢索極慢時該怎么辦。
4種解決方案:
4種方案優劣:
可以看到,每種方案都有自己的不足之處,所以一定要根據數據規模和業務需求來確定到底使用什么。
3.BloomFilter 代碼實現
下面就直接貼代碼,注釋已經寫得很詳細了,而且布隆過濾器的邏輯本身就不復雜,應該還是比較容易理解的…
class BloomFilter{// 二進制向量(數組)的位數,相當于能存儲1000萬條url左右,誤報率為千萬分之一private static final int BIT_SIZE = 2 << 28 ;// 二進制數組(2^28 byte)private BitSet bits = new BitSet(BIT_SIZE);// 用于生成信息指紋的8個隨機數,最好選取質數(使同一算法產生8個不同hash函數)private static final int[] seeds = new int[]{3, 5, 7, 11, 13, 31, 37, 61}; // 用于存儲具體的8個哈希對象(每個里面包含了不同結果的hash函數)private Hash[] func = new Hash[seeds.length];public BloomFilter(){// 構造BloomFilter時生成這8個隨機哈希對象for(int i = 0; i < seeds.length; i++){func[i] = new Hash(BIT_SIZE, seeds[i]);}}/*** 操作一:向過濾器中添加字符串*/public void addValue(String value) { if(value != null){// 將字符串value哈希為8個或多個整數for(Hash f : func) //然后在這些整數的bit上變為1bits.set(f.hash(value), true); }} /*** 操作二:判斷字符串是否包含在布隆過濾器中*/public boolean contains(String value) { if(value == null) return false; boolean ret = true; // 將要比較的字符串重新以上述方法計算hash值,再與布隆過濾器比對for(Hash f : func)// 注:這里 && ret 很關鍵,若有false了則能保證結果一定是falseret = ret && bits.get(f.hash(value)); return ret; } /*** 隨機哈希值對象(靜態內部類)*/public static class Hash{private int size; // 二進制向量數組大小private int seed; // 隨機數種子public Hash(int cap, int seed){this.size = cap;this.seed = seed;}/*** 計算哈希值(也可以選用別的恰當的哈希函數)*/public int hash(String value){int result = 0;int len = value.length();// 具體Hash算法:加權求和!!!for(int i = 0; i < len; i++){result = seed * result + value.charAt(i);}// 注:取模,得到具體二進制數組下標(result%(size-1))return (size - 1) & result;}} } public class Test {public static void main(String[] args){BloomFilter b = new BloomFilter();b.addValue("www.baidu.com");b.addValue("www.sohu.com");System.out.println(b.contains("www.baidu.com"));System.out.println(b.contains("www.sina.com"));} }4.Guava自帶BloomFilter
除了自己實現BloomFilter,還可以直接使用guava包自帶的布隆過濾器。
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>22.0</version> </dependency>下面是BloomFilter+redis的一個示例,目的是防止緩存穿透:
import com.google.common.hash.BloomFilter;// 初始化布隆過濾器 // 1000:期望存入的數據個數,0.001:期望的誤差率 BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf‐8")), 1000, 0.001);//把所有數據存入布隆過濾器 void init(){ for (String key: keys) { bloomFilter.put(key); } }String get(String key) { 1// 從布隆過濾器這一級緩存判斷下key是否存在 Boolean exist = bloomFilter.mightContain(key); if(!exist){ return ""; } // 從緩存中獲取數據 String cacheValue = cache.get(key); // 緩存為空 if (StringUtils.isBlank(cacheValue)) { // 從存儲中獲取 String storageValue = storage.get(key); cache.set(key, storageValue); // 如果存儲數據為空, 需要設置一個過期時間(300秒) if (storageValue == null) { cache.expire(key, 60 * 5); } return storageValue; } else { // 緩存非空 return cacheValue; } }總結
以上是生活随笔為你收集整理的【数据结构】布隆过滤器:BloomFilter原理及Java实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 看书好还是看视频好?我学五年编程的一点感
- 下一篇: Java JNI调用IC卡读卡器