redis-分布式布隆过滤器(Bloom Filter)详解(初版)

随声附和 提交于 2020-11-23 19:38:59

redis-分布式布隆过滤器(Bloom Filter)详解(初版)

1 布隆过滤器(Bloom Filter)原理以及应用

假设现在有50亿个电话号码,现在有1万个电话号码,需要快速判断这些电话号码是否已经存在?

现在有3中途径

  • 1 通过数据库查询,但是不能快速查询。
  • 2 把电话号码预先放在一个集合中,如果用long类型存储的话,50亿 * 8字节 = 大于需要40GB(内存浪费或者严重不够)
  • 3 使用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, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。

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
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!