布隆过滤器(Bloom Filter)

布隆过滤器(Bloom Filter)

一句话了解布隆过滤器

布隆过滤器(Bloom Filter):本质上是一种概率型数据结构(probabilistic data structure),优点是高效地插入和查询,根据查询结果快速判断某样东西一定不存在或者可能存在,缺点是只能插入不能删除.

布隆过滤器相比于传统的 List,Set,Map 等数据结构,它更高效,占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的.

布隆过滤器数据结构

布隆过滤器是一个 bit向量或者说 bit数组,当我们映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并且对每个生成的哈希值指向的 bit 位置1, bit数组类似于下图:

初始状态

例如针对值"baidu"和三个不同的哈希函数分别生成了哈希值1,4,7则上图转变为:

store baidu

我们现在再存一个值"tencent",如果哈希函数返回3,4,8的话,图继续变为:

store tencent

注意:存入baidu和tencent后,4这个 bit 位由于两个值的哈希函数都返回了这个bit位,因此被覆盖了.

假设现在我们查询"bloom"这个值是否存在,根据哈希函数返回 1,2,8 由此我们可以确定的说"bloom"这个值不存在,但我们查询"baidu"这个值是否存在,根据哈希函数返回 1,4,7 那么就可以说"baidu"这个值存在了吗,答案是不可以的.

这是为什么呢?答案其实也很简单,因为随着增加的值越来越多,被置为1的bit位也会越来越多,比如某个值"filter"即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值设置为了1,那么程序还是会判断"filter"这个值存在.

哈希函数个数和布隆过滤器长度关系

很显然,过小的布隆过滤器很快所有的 bit 位被置为1,那么查询任何值都会返回"可能存在",起不到过滤的目的了.布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小.

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高.

具体计算公式与推倒,请查阅参考与查看中提供的地址.

布隆过滤器的应用

缓存穿透:简单来说就是用户想要查询一个数据,发现缓存(redis)数据库没有(缓存未命中),于是向持久层数据库查询,发现也没有.当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库.这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透.

缓存击穿简图

垃圾邮件过滤,从数十亿个垃圾邮件列表中判断某邮箱是否是杀垃圾邮箱等.

布隆过滤器代码实现

Java由于开源,框架多,轮子多,而且一个功能的轮子还有好多种,光序列化就有fastjson,gson,jackson任你挑选.

布隆过滤器在Google提供的guava包中的实现

Google的guava依赖

<!--Google guava 工具类 -->
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>24.0-jre</version>
</dependency>

小Demo

public class GuavaBloomFilterTest {

    //插入数据量
    private static final int INSERT_NUM = 100_0000;

    //期望误判率
    private static double FPP = 0.02;

    public static void main(String[] args) {
        //初始化一个存储String数据的布隆过滤器,默认误判率是0.03
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), INSERT_NUM, FPP);

        //用于存放所有实际存在的key,用于存在
        Set<String> set = Sets.newHashSetWithExpectedSize(INSERT_NUM);

        //用于存放所有实际存在的key,用于取出
        List<String> list = Lists.newArrayListWithCapacity(INSERT_NUM);

        //插入随机字符串
        String uuid;
        for (int i = 0; i < INSERT_NUM; i++) {
            uuid = UUID.randomUUID().toString();
            bloomFilter.put(uuid);
            set.add(uuid);
            list.add(uuid);
        }

        //记录正确与错误的数量
        int rightNum = 0;
        int wrongNum = 0;

        //进行判断
        String data;
        for (int i = 0; i < 1_0000; i++) {
            //0-10000之间,可以被100整除的数有100个
            data = i % 100 == 0?list.get(i / 100):UUID.randomUUID().toString();

            //这里用了might,看上去不是很自信,所以如果布隆过滤器判断存在了,我们还要去sets中实锤
            if(bloomFilter.mightContain(data)){
                if(set.contains(data)){
                    rightNum++;
                    continue;
                }
                wrongNum++;
            }
        }

        //计算失误率
        final BigDecimal percent = new BigDecimal(wrongNum).divide(new BigDecimal(9900), 2, RoundingMode.HALF_UP);
        final BigDecimal bingo = new BigDecimal(9900 - wrongNum).divide(new BigDecimal(9900), 2, RoundingMode.HALF_UP);

        System.out.println("在100万个元素中,判断100个实际存在的元素,布隆过滤器认为存在的且真正存的:"+rightNum);
        System.out.println("在100万个元素中,判断9900个不存在的元素,误认为存在的:"+wrongNum+",命中率:"+bingo+",误判率:"+percent);

    }
}


Redis实现布隆过滤器

上面使用guava实现布隆过滤器是把数据放在本地内存中,我们项目往往是分布式的,我们还可以把数据放在redis中,用redis来实现布隆过滤器,这就需要我们自己设计映射函数,自己度量二进制向量的长度,下面是我在网上查找所得,本人已经测试,可以正常运行.

redis布隆过滤器核心类

public class RedisBloomHelper<T> {

    //元素预计数量
    private final int numApproxElements;

    //误差率
    private final double fpp;

    //哈希函数个数
    private int numHashFunctions;

    //bit图长度
    private int bitmapLength;

    private Funnel<T> funnel;

    /**
     * 构造布隆过滤器.注意:在同一业务场景下,三个参数务必相同
     *
     * @param numApproxElements 预估元素数量
     * @param fpp               误差率
     */
    public RedisBloomHelper(int numApproxElements, double fpp) {
        this.numApproxElements = numApproxElements;
        this.fpp = fpp;
        this.bitmapLength = optimalNumOfBits(numApproxElements, fpp);
        this.numHashFunctions = optimalNumOfHashFunctions(numApproxElements, bitmapLength);
        this.funnel = (Funnel<T>) Funnels.stringFunnel(Charsets.UTF_8);
    }

    /**
     * 构造布隆过滤器.注意:在同一业务场景下,三个参数务必相同
     *
     * @param numApproxElements 预估元素数量
     * @param fpp               误差率
     * @param funnel            指定存储的存储类型
     */
    public RedisBloomHelper(int numApproxElements, double fpp, Funnel<T> funnel) {
        this.numApproxElements = numApproxElements;
        this.fpp = fpp;
        this.bitmapLength = optimalNumOfBits(numApproxElements, fpp);
        this.numHashFunctions = optimalNumOfHashFunctions(numApproxElements, bitmapLength);
        this.funnel = funnel;
    }

    //计算最优bit数组长度 方法来自guava
    public int optimalNumOfBits(int numApproxElements, double fpp) {
        if (fpp == 0) {
            fpp = Double.MIN_VALUE;
        }
        return (int) (-numApproxElements * Math.log(fpp) / (Math.log(2) * Math.log(2)));
    }

    //计算最优hash函数个数 方法来自guava
    public int optimalNumOfHashFunctions(int numApproxElements, int bitmapLength) {
        return Math.max(1, (int) Math.round((double) bitmapLength / numApproxElements * Math.log(2)));
    }

    /**
     * 计算一个元素值哈希后映射到Bitmap的哪些bit上
     * Guava的BloomFilterStrategies.32
     */
    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 % bitmapLength;
        }

        return offset;
    }

    /**
     * 计算一个元素值哈希后映射到Bitmap的哪些bit上
     * Guava的BloomFilterStrategies.64
     * @param element 元素值
     * @return bit下标的数组
     */
    public long[] getBitIndices(T element) {
        long[] indices = new long[numHashFunctions];

        byte[] bytes = Hashing.murmur3_128()
                .hashObject(element,funnel).asBytes();
        long hash1 = Longs.fromBytes(
                bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]
        );
        long hash2 = Longs.fromBytes(
                bytes[15], bytes[14], bytes[13], bytes[12], bytes[11], bytes[10], bytes[9], bytes[8]
        );

        long combinedHash = hash1;
        for (int i = 0; i < numHashFunctions; i++) {
            indices[i] = (combinedHash & Long.MAX_VALUE) % bitmapLength;
            combinedHash += hash2;
        }
        return indices;
    }
}

redis布隆过滤器工具类

@Component
public class RedisBloomUtils {

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 根据给定的布隆过滤器添加值,在添加一个元素的时候使用
     *
     * @param bloomHelper 布隆过滤器对象
     * @param key         redis中的key
     * @param value       存入的值
     */
    public <T> void add(RedisBloomHelper<T> bloomHelper, String key, T value) {
        add(bloomHelper, key, value, 0, null);
    }

    /**
     * 根据给定的布隆过滤器添加值,在添加一批元素的时候使用,批量添加的性能好,
     * 使用pipeline方式(如果是集群下,请使用优化后RedisPipeline的操作)
     *
     * @param bloomHelper 布隆过滤器对象
     * @param key         redis中的key
     * @param values      存入的元素集合
     */
    public <T> void addList(RedisBloomHelper<T> bloomHelper, String key, List<T> values) {
        addList(bloomHelper, key, values, 0, null);
    }


    /**
     * 根据给定的布隆过滤器添加值,在添加一个元素的时候使用
     *
     * @param bloomHelper 布隆过滤器对象
     * @param key         redis中的key
     * @param value       存入的值
     * @param <T>         泛型,可以传入任何类型的value
     * @param timeout     有效时间
     * @param unit        时间单位
     */
    public <T> void add(RedisBloomHelper<T> bloomHelper, String key, T value, long timeout, TimeUnit unit) {
        if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value) || null==bloomHelper)
            throw new IllegalArgumentException("redis参数有误");
        int[] offset = bloomHelper.murmurHashOffset(value);
        for (int i : offset) {
            redisTemplate.opsForValue().setBit(key, i, true);
        }
        if (unit != null && timeout > 0) {
            redisTemplate.expire(key, timeout, unit);
        }
    }

    /**
     * 根据给定的布隆过滤器添加值,在添加一批元素的时候使用,批量添加的性能好,
     * 使用pipeline方式(如果是集群下,请使用优化后RedisPipeline的操作)
     *
     * @param bloomHelper 布隆过滤器对象
     * @param key         redis中的key
     * @param values      存入的集合
     * @param <T>         泛型,可以传入任何类型的value
     * @param timeout     有效时间
     * @param unit        时间单位
     */
    public <T> void addList(RedisBloomHelper<T> bloomHelper, String key, List<T> values, long timeout, TimeUnit unit) {
        if (StringUtils.isEmpty(key) || CollectionUtils.isEmpty(values) || null == bloomHelper)
            throw new IllegalArgumentException("redis参数有误");
        redisTemplate.executePipelined((RedisCallback<Long>) connection -> {
            connection.openPipeline();
            for (T value : values) {
                int[] offset = bloomHelper.murmurHashOffset(value);
                for (int i : offset) {
                    connection.setBit(key.getBytes(), i, true);
                }
            }
            return null;
        });
        if (unit != null && timeout > 0) {
            redisTemplate.expire(key, timeout, unit);
        }
    }

    /**
     * 检查元素在集合中是否(可能)存在
     * @param bloomHelper 布隆过滤器对象
     * @param key         redis中的key
     * @param value       存入的数据
     * @param <T>         泛型,可以传入任何类型的value
     * @return 判断结果
     */
    public <T> boolean mayExist(RedisBloomHelper<T> bloomHelper,String key, T value){
        if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value) || null == bloomHelper)
            throw new IllegalArgumentException("redis参数有误");
        int[] offset = bloomHelper.murmurHashOffset(value);
        for(int i : offset){
            if(!redisTemplate.opsForValue().getBit(key,i)){
                return false;
            }
        }
        return true;
    }

    /**
     * 删除缓存
     * @param key 键值
     */
    public void delete(String key){
        redisTemplate.delete(key);
    }

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisBloomFilterTest {

    @Autowired
    private RedisBloomUtils redisBloomUtils;

    @Test
    public void testRedisBloom() {
        String key = "bloom";
        //删除缓存的key
        redisBloomUtils.delete(key);
        //预计元素个数
        int expectedInsertions = 1000;
        //误差率
        double fpp = 0.2;
        //记录误判的个数
        int j = 0;
        RedisBloomHelper<String> bloomHelper = new RedisBloomHelper<>(expectedInsertions,fpp);
        //存储记录的元素
        List<String> list = Lists.newArrayListWithCapacity(expectedInsertions);
        for (int i = 0; i < 100; i++) {
            list.add(String.valueOf(i));
        }
        long beginTime = System.currentTimeMillis();
        redisBloomUtils.addList(bloomHelper,key,list);
        long costTime = System.currentTimeMillis()-beginTime;
        System.err.println("布隆过滤器添加100个值,耗时:"+costTime+"毫秒!");
        for (int i = 0; i < expectedInsertions; i++) {
            if(redisBloomUtils.mayExist(bloomHelper,key,String.valueOf(i))){
                //记录误判个数
                if(!list.contains(String.valueOf(i))){
                    j++;
                }
            }
        }
        System.err.println("布隆过滤器,误判了"+j+"个,验证结果耗时:"+(System.currentTimeMillis()-beginTime)+"毫秒!");
    }
}

项目源码

https://github.com/liwe17/springboot-socks/tree/master/springboot-daily-case

参考与转载

https://zhuanlan.zhihu.com/p/43263751

https://www.cnblogs.com/qdhxhz/p/11237246.html

https://www.toutiao.com/i6817627686840566283/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1587521741&app=news_article&utm_source=weixin&utm_medium=toutiao_android&req_id=202004221015410101290331480C5D2EC4&group_id=6817627686840566283

声明

本公众号所有内容均来自互联网,仅用于分享自己日常工作与学习的心得.

我的公众号

微信公众号

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值