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…

相关文章
相关标签/搜索