redis-分佈式布隆過濾器(Bloom Filter)詳解(第一版)

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

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

如今有3中途徑git

  • 1 經過數據庫查詢,可是不能快速查詢。
  • 2 把電話號碼預先放在一個集合中,若是用long類型存儲的話,50億 * 8字節 = 大於須要40GB(內存浪費或者嚴重不夠)
  • 3 使用redis的hyperloglog,可是準確度不高。

相似的問題:github

  • 垃圾郵件過濾
  • 文字處理中的錯誤單詞檢測
  • 網絡爬蟲重複URL檢測
  • 會員抽獎
  • 判斷一個元素在億級數據中是否存在
  • 緩存穿透

而布隆過濾器則能夠解決上述問題redis

1 什麼是布隆過濾器

布隆過濾器(Bloom Filter)是1970年由布隆提出的。它其實是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器能夠用於檢索一個元素是否在一個集合中。它的優勢是空間效率和查詢時間都遠遠超過通常的算法,缺點是有必定的誤識別率和刪除困難。算法

當一個元素被加入集合時,經過 K 個 Hash 函數將這個元素映射成一個位陣列(Bit array)中的 K 個點,把它們置爲 1。檢索時,咱們只要看看這些點是否是都是 1 就(大約)知道集合中有沒有它了:數據庫

若是這些點有任何一個 0,則被檢索元素必定不在; 若是都是 1,則被檢索元素極可能在。數組

  • 添加元素的原理緩存

    1 將要添加的元素給k個hash函數安全

    2 獲得對應於位數組上的k個位置bash

    3 將這k個位置設置成 1

  • 查詢元素原理 1 將要查詢的元素給k個hash函數

    2 獲得對應數組的k個元素

    3 若是k個位置中有一個爲0,則確定不在集合中

    4.若是k個位置所有爲1,則有可能在集合中

優勢

它的優勢是空間效率和查詢時間都遠遠超過通常的算法,布隆過濾器存儲空間和插入 / 查詢時間都是常數O(k)。另外, 散列函數相互之間沒有關係,方便由硬件並行實現。布隆過濾器不須要存儲元素自己,在某些對保密要求很是嚴格的場合有優點。

缺點

  • 隨着數據的增長,誤判率隨之增長;;只能判斷數據是否必定不存在,而沒法判斷數據是否必定存在。

    若是數據A,通過hash1(A)、hash2(A)、hash3(A),獲得其hash值一、三、5,而後咱們在其二進制向量位置一、三、5設置1,而後數據B,通過hash1(B)、hash2(B)、hash3(B),其實hash值也是一、三、5,咱們在作業務處理的時候判斷B是否存在的時候發現 其二進制向量位置返回1,認爲其已經存在,就跳過相關業務處理,實際上根本不存在,這就是因爲hash碰撞引發的問題。也就存在了偏差率。

  • 沒法作到刪除數據

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

3 redis實現布隆過濾器的三種方式

引入guava pom.xml

<dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>23.0</version>
    </dependency>
複製代碼

基於Mur3Mur3 hash算法(低碰撞,高性能)

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

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());

    }
}

複製代碼
誤傷的數量:320
複製代碼
BloomFilter<Integer> bloomFilter =  BloomFilter.create(Funnels.integerFunnel(), size,0.01);
 誤傷的數量:100
複製代碼

缺點:

1 基於本地緩存,容量受限制 2 多個應用就有多個布隆過濾器,多應用同步複雜。

2 redis分佈式布隆過濾器的實現(基於guava的實現)

主要是把guava布隆過濾器的相關源碼提取了出來,用於分佈式redis布隆過濾器。

package jedis.bloomFilter.bloomFilterGuava;

import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;

//@Configurable
public class BloomFilterHelper<T> {
    private int numHashFunctions;//hash循環次數
    private int bitSize;//bitsize長度
    private Funnel<T> funnel;

    /**
     * @param funnel
     * @param expectedInsertions 指望插入長度
     * @param fpp 偏差率
     */
    public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
        Preconditions.checkArgument(funnel != null, "funnel不能爲空");
        this.funnel = funnel;
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
    }

    public int[] murmurHashOffset(T value) {
        int[] offset = new int[numHashFunctions];

        long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
        int hash1 = (int) hash64;
        int hash2 = (int) (hash64 >>> 32);
        for (int i = 1; i <= numHashFunctions; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
                nextHash = ~nextHash;
            }
            offset[i - 1] = nextHash % bitSize;
        }

        return offset;
    }

    /**
     * 計算bit數組長度
     */
    private int optimalNumOfBits(long n, double p) {
        if (p == 0) {
            p = Double.MIN_VALUE;
        }
        return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
    }

    /**
     * 計算hash方法執行次數
     */
    private int optimalNumOfHashFunctions(long n, long m) {
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
    }
}

複製代碼
package jedis.bloomFilter.bloomFilterGuava;

import com.google.common.base.Preconditions;
import redis.clients.jedis.JedisCluster;


//@Component
public class RedisBloomFilter {
    private JedisCluster cluster;

    public RedisBloomFilter(JedisCluster jedisCluster) {
        this.cluster = jedisCluster;
    }

    /**
     * 根據給定的布隆過濾器添加值
     */
    public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能爲空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            cluster.setbit(key, i, true);
        }
    }

    /**
     * 根據給定的布隆過濾器判斷值是否存在
     */
    public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能爲空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            if (!cluster.getbit(key, i)) {
                return false;
            }
        }
        return true;
    }

}

複製代碼
package jedis.bloomFilter.bloomFilterGuava;

import com.google.common.hash.Funnels;
import redis.clients.jedis.JedisCluster;

import java.nio.charset.Charset;

/**
 * 基於guava分佈式布隆過濾器
 */
public class Test {
    public static void main(String[] args) {
        BloomFilterHelper bloomFilterHelper =  new BloomFilterHelper<>(Funnels.stringFunnel(Charset.defaultCharset()), 1000, 0.1);

        JedisCluster cluster = null;
        RedisBloomFilter redisBloomFilter = new RedisBloomFilter( cluster);

        int j = 0;
        for (int i = 0; i < 100; i++) {
            redisBloomFilter.addByBloomFilter(bloomFilterHelper, "bloom", i+"");
        }
        for (int i = 0; i < 1000; i++) {
            boolean result = redisBloomFilter.includeByBloomFilter(bloomFilterHelper, "bloom", i+"");
            if (!result) {
                j++;
            }
        }
        System.out.println("漏掉了" + j + "個");
    }
}

複製代碼

3 Rebloom插件方式實現布隆過濾器(redis 4.0 之後)

redis4.0 以後支持插件支持布隆過濾器 git: 開源項目:github.com/RedisBloom/…

(也可參考)RedisBloom的客戶端:github.com/RedisBloom/…

  • 安裝Rebloom插件
1 下載並編譯

$ git clone git://github.com/RedisLabsModules/rebloom
$ cd rebloom
$ make
複製代碼
  • 將Rebloom加載到Redis中,在redis.conf裏面添加
loadmodule /path/to/rebloom.so
複製代碼
  • 命令操做
BF.ADD bloom redis
BF.EXISTS bloom redis
BF.EXISTS bloom nonxist
複製代碼
  • 命令行加載rebloom插件,而且設定每一個bloomfilter key的容量和錯誤率:
cd /usr/redis-4.0.11
./src/redis-server redis.conf --loadmodule /usr/rebloom/rebloom.so INITIAL_SIZE 1000000 ERROR_RATE 0.0001
# 容量100萬, 容錯率萬分之一
複製代碼

  • java-lua版操做(java代碼不提供了,本身把腳本執行就行)

bloomFilterAdd.lua

local bloomName = KEYS[1]
local value = KEYS[2]

-- bloomFilter
local result_1 = redis.call('BF.ADD', bloomName, value)
return result_1
複製代碼

bloomFilterExist.lua

local bloomName = KEYS[1]
local value = KEYS[2]

-- bloomFilter
local result_1 = redis.call('BF.EXISTS', bloomName, value)
return result_1
複製代碼

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

krisives.github.io/bloom-calcu…

相關文章
相關標籤/搜索