大数据量背景下快速排重利器

一、前言

当我们使用主流数据结构如 Lists, Maps, Sets, Trees等等时,我可以得到确切的结果,无论这个数据存在或是不存在。概率数据结构能够提供一种基于内存的,快速的查找出一种可能而非确切的结果。这种概率数据结构就是布隆过滤器(Bloom filter)。 布隆过滤器可以检查值是“可能在集合中”还是“绝对不在集合中”。

2.1 定义

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

•二进制向量,简单理解就是一个二进制数组。这个数组里面存放的值要么是0,要么是1。•映射函数,它可以将一个元素映射成一个位阵列(Bit array)中的一个点。所以通过这个点,就能判断集合中是否有此元素。

2.2 基本思想

•当一个元素被加入集合时,通过K个散列函数将这个元素映射到一个位数组中的K个点,把它们置为1。•检索某个元素时,再通过这K个散列函数将这个元素映射,看看这些位置是不是都是1就能知道集合中这个元素存不存在。如果这些位置有任何一个0,则该元素一定不存在;如果都是1,则被检元素很可能存在。•Bloom Filter跟单个哈希函数映射不同,Bloom Filter使用了k个哈希函数,每个元素跟k个bit对应。从而降低了冲突的概率。

 

2.3 特性

1)一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。也就是说布隆过滤器只能判断数据是否一定不存在,而无法判断数据是否一定存在。 2) 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。

2.4 优缺点

2.4.1 优点

1.二进制组成的数组,内存占用空间少,并且插入和查询速度很快,常数级别。2.Hash函数相互之间没有必然联系,方便由硬件并行实现。3.只存储0和1,不需要存储元素本身,在某些对保密要求非常严格的场合有优势。

2.4.2 缺点

1.

存在误差率。随着存入的元素数量增加,误算率随之增加。(比如现实中你是否遇到正常邮件也被放入垃圾邮件目录,正常短信被拦截)可以增加一个小的白名单,存储那些可能被误判的元素。

2.

删除困难。一个元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。因为其他元素的映射也有可能在相同的位置置为1。可以采用Counting Bloom Filter解决。

二、实践方案

2.1 Guava 实现 BloomFilter

首先引入guava的jar包,

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>27.0.1-jre</version>
</dependency>

在Java代码中做如下的创建

    public static void main(String[] args) {
    // 1.创建符合条件的布隆过滤器
    // 预期数据量10000,错误率0.0001
    BloomFilter<CharSequence> bloomFilter =
            BloomFilter.create(Funnels.stringFunnel(
                                    Charset.forName("utf-8")),10000, 0.0001);
    // 2.将一部分数据添加进去
    for (int i = 0; i < 5000; i++) {
        bloomFilter.put("" + i);
    }
    System.out.println("数据写入完毕");
    // 3.测试结果
    for (int i = 0; i < 10000; i++) {
        if (bloomFilter.mightContain("" + i)) {
            System.out.println(i + "存在");
        } else {
            System.out.println(i + "不存在");
        }
    }
}

2.2 Redission 实现方案

public class RedissonBloomFilterDemo {

    public static void main(String[] args) {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user");
        // 初始化布隆过滤器,预计统计元素数量为55000000,期望误差率为0.03
        bloomFilter.tryInit(55000000L, 0.03);
        bloomFilter.add("Tom");
        bloomFilter.add("Jack");
        System.out.println(bloomFilter.count());   //2
        System.out.println(bloomFilter.contains("Tom"));  //true
        System.out.println(bloomFilter.contains("Linda"));  //false
    }
}

三、实际应用场景

以邮箱黑名单为例,当用户注册或者登录请求的时候,我们需要判断用户是否属于这10亿个黑名单邮箱中,如果存在则需要对该用户进行拦截。业务规则虽然简单,难点在于数据量巨大,如何存储这10亿个元素,并且要能快速在10个元素之间去检索

import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;
import org.springframework.data.redis.core.RedisTemplate;

public class RedisBloom<T> {

    private int numHashFunctions;

    private int bitSize;

    private Funnel<T> funnel;

    private RedisTemplate redisTemplate;

    private RedisBloom(){

    }

    /**
     *
     * @param funnel
     * @param expectedInsertions 预计元数总量大小
     * @param fpp 允许的误差,例如 0.0001,允许万分之一的误差
     * @param redisTemplate
     */
    public RedisBloom(Funnel<T> funnel, int expectedInsertions, double fpp, RedisTemplate redisTemplate) {
        Preconditions.checkArgument(funnel != null, "funnel不能为空");
        this.funnel = funnel;
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
        this.redisTemplate = redisTemplate;
    }

    private 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)));
    }


    /**
     * 根据给定的布隆过滤器添加值
     */
    public void addByBloomFilter(String key, T value) {
        int[] offset = this.murmurHashOffset(value);
        for (int i : offset) {
            redisTemplate.opsForValue().setBit(key, i, true);
        }
    }

    /**
     * 根据给定的布隆过滤器判断值是否存在
     */
    public  boolean includeByBloomFilter(String key, T value) {
        int[] offset = murmurHashOffset(value);
        for (int i : offset) {
            if (!redisTemplate.opsForValue().getBit(key, i)) {
                return false;
            }
        }
        return true;
    }
}

RedisBloom是实现功能的关键,包含了计算bitmap的核心算法,其实大部分代码都是来源于Guava库里面的BloomFilterStrategies类,但是因为这个类是专门为Guava的BloomFilter类使用的,所以没有对外暴露一些重要的算法逻辑。再来看怎么结合redis一起使用BloomFilter,addByBloomFilter,往redis里面添加元素includeByBloomFilter,检查元素是否在redis bloomFilter里面。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值