1.應用
布隆過濾器用於判斷某一個值是不是已經存在。比如我們在使用新聞客戶端看新聞時,它會給我們不停地推薦新的內容,而它每次推薦時都要去重,以去掉那些我們已經看過的內容。
布隆過濾器是專門用來解決這種去重問題的,它在起到去重作用的同時,在空間上還能節省90%以上,但是會有一定的誤判概率。當布隆過濾器說某個值存在時,這個值可能不存在;當它說某個值不存在時,那就肯定不存在。
用在推送去重的場景中,布隆過濾器可以準確地過濾掉那些用戶已經看過的內容,用戶沒有看過的內容,它也會過濾掉一小部分(誤判),這樣就可以保證推薦給用戶的內容都是無重複的。
2.原理
1.布隆過濾器數據結構
布隆過濾器是一個 bit 向量或者說 bit 數組,長這樣:
如果我們要映射一個值到布隆過濾器中,我們需要使用多個不同的哈希函數生成多個哈希值,並對每個生成的哈希值指向的 bit 位置 1,例如針對值 “baidu” 和三個不同的哈希函數分別生成了哈希值 1、4、7,則上圖轉變爲:
Ok,我們現在再存一個值 “tencent”,如果哈希函數返回 3、4、8 的話,圖繼續變爲:
值得注意的是,4 這個 bit 位由於兩個值的哈希函數都返回了這個 bit 位,因此它被覆蓋了。現在我們如果想查詢 “dianping” 這個值是否存在,哈希函數返回了 1、5、8三個值,結果我們發現 5 這個 bit 位上的值爲 0,說明沒有任何一個值映射到這個 bit 位上,因此我們可以很確定地說 “dianping” 這個值不存在。而當我們需要查詢 “baidu” 這個值是否存在的話,那麼哈希函數必然會返回 1、4、7,然後我們檢查發現這三個 bit 位上的值均爲 1,那麼我們可以說 “baidu” 存在了麼?答案是不可以,只能是 “baidu” 這個值可能存在。
這是爲什麼呢?答案跟簡單,因爲隨着增加的值越來越多,被置爲 1 的 bit 位也會越來越多,這樣某個值 “taobao” 即使沒有被存儲過,但是萬一哈希函數返回的三個 bit 位都被其他值置位了 1 ,那麼程序還是會判斷 “taobao” 這個值存在。
2.如何選擇哈希函數個數和布隆過濾器長度
很顯然,過小的布隆過濾器很快所有的 bit 位均爲 1,那麼查詢任何值都會返回“可能存在”,起不到過濾的目的了。布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。
另外,哈希函數的個數也需要權衡,個數越多則布隆過濾器 bit 位置位 1 的速度越快,且布隆過濾器的效率越低;但是如果太少的話,那我們的誤報率會變高。
k 爲哈希函數個數,m 爲布隆過濾器長度,n 爲插入的元素個數,p 爲誤報率
如何選擇適合業務的 k 和 m 值呢,這裏直接貼一個公式:
3.性能分析
1.時間和內存分析
redis 布隆過濾器提供的指令有四個。
- bf.add添加元素
- bf.exists查詢元素是否存在
- bf.madd批量添加元素
- bf.mexists一次查詢多個元素是否存在
和redis基本類型set的部分功能類似,其中bf.add和bf.madd指令可以類比set集合的sadd指令,bf.exists可以類比set集合的sismember指令。這裏對bf.add和bf.exists的性能與set集合進行對比測試。
使用Java編寫一小段測試程序,由於jedis不支持布隆過濾器,可以使用RedisLabs提供的JReBloom:
public class BloomFilterTest {
private static final Jedis jedis = new Jedis("127.0.0.1", 6379);
private static final String VALUE_PRE = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
private static final String TEST_KEY = "test.key";
private static final String TEST_BF_KEY = "test.bf.key";
private static final Client client = new Client("127.0.0.1", 6379);
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println("輸入測試條數:");
int times = scan.nextInt();
System.out.println("輸入測試指令:");
String order = scan.next();
System.out.println("----start test----");
long start = System.currentTimeMillis();
if ("sadd".equals(order)) {
sadd(times);
} else if ("bfadd".equals(order)) {
bfadd(times);
} else if ("bfexists".equals(order)) {
bfexists(times);
} else if ("sismember".equals(order)) {
sismember(times);
}
System.out.println(order + "共耗時" + (System.currentTimeMillis() - start) + "ms");
jedis.close();
client.close();
}
private static void sismember(int times) {
for (int i=0; i<times; i++) {
String item = generateString(i);
jedis.sismember(TEST_KEY, item);
}
}
private static void bfexists(int times) {
for (int i=0; i<times; i++) {
String item = generateString(i);
client.exists(TEST_BF_KEY, item);
}
}
private static void bfadd(int times) {
for (int i=0; i<times; i++) {
String item = generateString(i);
client.add(TEST_BF_KEY, item);
}
}
private static void sadd(int times) {
for (int i=0; i<times; i++) {
String item = generateString(i);
jedis.sadd(TEST_KEY, item);
}
}
private static String generateString(int i) {
return VALUE_PRE + i;
}
}
測試流程:
首先,使用redis的redis-cli info memory命令看下當前redis的內存情況
編譯運行java程序,插入100w條記錄進行測試,value大小在30個字符左右。
1.bf.add指令
內存佔用情況
2.sadd指令
內存佔用
可以看出,bf.add指令與sadd指令在時間上相比並沒有優勢,但是在內存佔用上來說,set集合的內容佔用大概是布隆過濾器的10倍。
3.sismember指令
4.bf.exists指令
2.誤差分析
稍微修改下上面的程序
public class BloomFilterError {
private static final Jedis jedis = new Jedis("127.0.0.1", 6379);
private static final String VALUE_PRE = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789";
private static final String TEST_BF_KEY = "test.bf.key";
private static final Client client = new Client("127.0.0.1", 6379);
public static void main(String[] args) {
for (long i=100000; i<=1000000;) {
int bfexists = bfexists(i);
System.out.printf("count=" + i + "\tbfexists=" + bfexists);
System.out.printf("\terror percentages=%.3f\n", Math.abs(i - bfexists)/(i*0.01));
i += 100000;
}
jedis.close();
client.close();
}
private static int bfexists(long times) {
int correct = 0;
for (int i=0; i<times; i++) {
String item = generateString(i);
if (client.exists(TEST_BF_KEY, item)) {
correct++;
}
}
return correct;
}
private static String generateString(int i) {
return VALUE_PRE + i;
}
}
運行結果:
可以看到存在的正確率是100%。
修改下generateString的邏輯,生成的全是布隆過濾器中不存在的值:
private static String generateString(int i) {
return VALUE_PRE + "0" + i;
}
運行程序,看下判斷的正確率:
可以看到正確率都在98.6%以上。
plus
誤判率可以使用公式計算,要求誤判率越低,所需要的空間越大。如果一千萬的數據,誤判率允許 1%, 大概需要11M左右;如果要求誤判率爲 0.1%,則大概需要 17 M左右。可以使用bloom filter calculator計算。https://krisives.github.io/bloom-calculator/
參考博文: