Redis-布隆過濾器

猿起於此發表於2020-10-14

1. 布隆過濾器(Bloom Filter)原理以及應用

假設現在有50億個電話號碼,現在有1萬個電話號碼,需要快速判斷這些電話號碼是否已經存在?

現在有3中途徑

  • 通過資料庫查詢,但是不能快速查詢。
  • 把電話號碼預先放在一個集合中,如果用long型別儲存的話,50億 * 8位元組 = 大於需要40GB(記憶體浪費或者嚴重不夠)
  • 使用redis的hyperloglog,但是準確度不高。

類似的問題:

  • 垃圾郵件過濾
  • 文書處理中的錯誤單詞檢測
  • 網路爬蟲重複URL檢測
  • 會員抽獎
  • 判斷一個元素在億級資料中是否存在
  • 快取穿透
  • 而布隆過濾器則可以解決上述問題

1 什麼是布隆過濾器

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

當一個元素被加入集合時,通過 K 個 Hash 函式將這個元素對映成一個位陣列(Bit array)中的 K 個點,把它們置為 1。檢索時,我們只要看看這些點是不是都是 1 就(大約)知道集合中有沒有它了:
如果這些點有任何一個 0,則被檢索元素一定不在; 如果都是 1,則被檢索元素很可能在。

新增元素的原理

  1. 將要新增的元素給k個hash函式
  2. 得到對應於位陣列上的k個位置
  3. 將這k個位置設定成 1

查詢元素原理

  1. 將要查詢的元素給k個hash函式
  2. 得到對應陣列的k個元素
  3. 如果k個位置中有一個為0,則肯定不在集合中
  4. 如果k個位置全部為1,則有可能在集合中

在這裡插入圖片描述

優點

它的優點是空間效率和查詢時間都遠遠超過一般的演算法,布隆過濾器儲存空間和插入 / 查詢時間都是常數O(k)。另外, 雜湊函式相互之間沒有關係,方便由硬體並行實現。布隆過濾器不需要儲存元素本身,在某些對保密要求非常嚴格的場合有優勢。

缺點

隨著資料的增加,誤判率隨之增加;只能判斷資料是否一定不存在,而無法判斷資料是否一定存在。

如果資料A,經過hash1(A)、hash2(A)、hash3(A),得到其hash值1、3、5,然後我們在其二進位制向量位置1、3、5設定1,然後資料B,經過hash1(B)、hash2(B)、hash3(B),其實hash值也是1、3、5,我們在做業務處理的時候判斷B是否存在的時候發現 其二進位制向量位置返回1,認為其已經存在,就跳過相關業務處理,實際上根本不存在,這就是由於hash碰撞引起的問題。也就存在了誤差率。

無法做到刪除資料

一般情況下不能從布隆過濾器中刪除元素. 我們很容易想到把位陣列變成整數陣列,每插入一個元素相應的計數器加 1, 這樣刪除元素時將計數器減掉就可以了。然而要保證安全地刪除元素並非如此簡單。首先我們必須保證刪除的元素的確在布隆過濾器裡面. 這一點單憑這個過濾器是無法保證的。

2. redis實現布隆過濾器的方式

1. guava單機版實現布隆過濾器

引入Guava依賴

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

程式碼實現

package jedis.bloomFilter;

import com.google.common.hash.Funnels;
import com.google.common.hash.BloomFilter;

import java.util.ArrayList;
import java.util.List;

public class GuavaBloomFilter {
    private static  int size = 10000;
    public static void main(String[] args) {
        /**
         * 預設誤差率3%。肯定不存在以及可能存在
         * 可通過建構函式去設定誤差率
         *  create(
         *       Funnel<? super T> funnel, int expectedInsertions, double fpp)
         *
         */
        BloomFilter<Integer> bloomFilter =  BloomFilter.create(Funnels.integerFunnel(), size);
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }

        for (int i = 0; i < size; i++) {
            if (!bloomFilter.mightContain(i)) {
                System.out.println("有人逃脫了");
            }
        }

        List<Integer> list = new ArrayList<Integer>(1000);
        for (int i = size + 10000; i < size + 20000; i++) {
            if (bloomFilter.mightContain(i)) {
                list.add(i);
            }
        }
        System.out.println("誤傷的數量:" + list.size());

    }
}

2. Rebloom外掛方式實現布隆過濾器(redis 4.0 以後)

下載並編譯

$ git clone git://github.com/RedisLabsModules/rebloom
$ cd rebloom
$ make
cd /usr/redis-4.0.11
#載入module 並設定容量與容錯率
# 容量100萬, 容錯率萬分之一
./src/redis-server redis.conf --loadmodule /usr/rebloom/rebloom.so INITIAL_SIZE 1000000 ERROR_RATE 0.0001

redis命令:

BF.ADD bloom redis
BF.EXISTS bloom redis
BF.EXISTS bloom nonxist

3. 手動實現

package com.jd.demo.test;

import java.util.Arrays;
import java.util.BitSet;
import java.util.concurrent.atomic.AtomicBoolean;

public class MyBloomFilter {
    //你的布隆過濾器容量
    private static final int DEFAULT_SIZE = 2 << 28;
    //bit陣列,用來存放結果
    private static BitSet bitSet = new BitSet(DEFAULT_SIZE);
    //後面hash函式會用到,用來生成不同的hash值,可隨意設定,別問我為什麼這麼多8,圖個吉利
    private static final int[] ints = {1, 6, 16, 38, 58, 68};

    //add方法,計算出key的hash值,並將對應下標置為true
    public void add(Object key) {
        Arrays.stream(ints).forEach(i -> bitSet.set(hash(key, i)));
    }

    //判斷key是否存在,true不一定說明key存在,但是false一定說明不存在
    public boolean isContain(Object key) {
         boolean result = true;
        for (int i : ints) {
        	//短路與,只要有一個bit位為false,則返回false
            result = result && bitSet.get(hash(key, i));
        }
        return result;
    }

    //hash函式,借鑑了hashmap的擾動演算法
    private int hash(Object key, int i) {
        int h;
        return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16)));
    }
}

一個線上計算所需空間的地址

計算空間

參考文件:
文件1
文件2

相關文章