如何判断一个元素在亿级数据中是否存在?——布隆过滤器

最近,有朋友去面试,被问到这么一个问题:

现在有一个非常庞大的数据,假设全是 int 类型。现在我给你一个数,你需要告诉我它是否存在其中(尽量高效)。

需求其实很清晰,只需要判断一个数是否存在即可。但是这里有一个比较重要的前提:非常庞大的数据

常规实现:

先不考虑这个条件,我们脑海中出现的第一种方案是什么呢?
我想大多数想到的都是用 HashSet 来存放数据,因为它的写入查询的效率都比较高。写入和判断元素是否存在都有对应的 API,所以实现起来比较简单。
为此,我写了一个Demo,利用 HashSet 来存放数据,同时为了展示效果,我把堆最大内存设置为64M,并加入 GC 日志的打印。

-Xms64m -Xmx64m -XX:+PrintHeapAtGC -XX:+
HeapDumpOnOutOfMemoryError

堆内存设置

HashSetHeap:

/**
 * 往Set存放1000W个数据
 * @author K. L. Mao
 * @create 2018/11/30
 */
public class HashSetHeap {

    public static void hashSetTest(){
        long start = System.currentTimeMillis();
        int capacity = 10000000;
        Set<Integer> set = new HashSet<>(capacity);
        for (int i = 0; i < capacity; i++) {
            set.add(i);
        }
        System.out.println(set.contains(2));
        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - start));
    }
}

测试类,HeapTest:

/**
 * 测试类
 * @author K. L. Mao
 * @create 2018/11/30
 */
public class HeapTest {
    public static void main(String[] args){
        HashSetHeap.hashSetTest();
    }

运行测试类,控制台输出如下:


测试类控制台

内存溢出!!!
可见,在内存有限的情况下,我们不能使用这种方式。

Bloom Filter

基于上面分析的条件,要实现这个需求最需要解决的是如何将庞大的数据 load 到内存中
而我们是否可以换种思路,因为只是需要判断数据是否存在,也不是需要把数据查询出来,所以完全没有必要将真正的数据存放进去。
伟大的的科学家们已经帮我们想到了这样的需求。
BurtonHowardBloom 在 1970 年提出了一个叫做 BloomFilter (中文翻译:布隆过滤)的算法。
它主要就是用于解决:判断一个元素是否在一个集合中,但它的优势是只需要占用很小的内存空间以及有着高效的查询效率。

Bloom Filter 原理

下面来分析下它的实现原理。

官方的说法是:它是一个保存了很长的二进制向量,同时结合 Hash 函数实现的。

听起来比较绕,但是通过一个图就比较容易理解了。

Bloom Filter 原理图

如图所示:

  • 首先需要初始化一个二进制的数组,长度设为 L (图中为 8),同时初始值全为0。
  • 当写入一个 A1=1000 的数据时,需要进行 H 次 hash 函数的运算(这里为2次);与 HashMap 有点类似,通过算出 hashcode 与 L 取模后定位到0、2处,将该处的值设为1。
  • A2=2000 也是同理计算后将4、7位置设为1。
  • 当有一个 B1=1000 需要判断是否存在时,也是做两次 hash 运算,定位到0、2处,此时他们的值都为1,所以认为 B1=1000 存在于集合中。
  • 当有一个 B2=3000 时,也是同理。第一次 Hash 定位到 index=4 时,数组中的值为1,所以再进行第二次 hash 运算,结果定位到 index=5 的值为0,所以认为 B2=3000 不存在于集合中。
    整个的写入、查询的流程就是这样,汇总起来就是:

对写入的数据做 H 次 hash 运算定位到数组中的位置,同时将数据改为 1。当有数据查询时也是同样的方式定位到数组中。

一旦其中的有一位为 0 则认为数据肯定不存在于集合,否则数据可能存在于集合中
所以,布隆过滤器有以下几个特点:
1、只要返回数据不存在,则肯定不存在。
2、返回数据存在,只能是大概率存在。
3、不能清除其中的数据。
第一点应该都能理解,重点解释下2、3点。
为什么返回存在的数据却可能存在呢,这其实也和HashMap类似。
在有限的数组长度中,存放大量的数据,即便是再完美的 hash 算法也会有冲突,所以有可能两个完全不同的 A、B 两个数据最后定位到的位置是一样的。
这时拿 B 进行查询时那自然就是误报了。
删除数据也是同理,当我把 B 的数据删除了,其实也相当于是把 A 的数据删掉了,这样也会造成后续的误报。
基于以上的 Hash 冲突的前提,所以BloomFilter有一定的误报率,这个误报率和 hash 算法的次数 H,以及数组长度 L 都有关。

自己实现一个布隆过滤

    /**
     * 自定义布隆过滤器
     * @author K. L. Mao
     * @create 2018/11/30
     */
    @Data
    public class BloomFilters {
        /**
         *  数组长度
         */
        private int arraySize;

        /**
         *  数组
         */
        private int[] array;

        public BloomFilters(int arraySize){
            this.arraySize = arraySize;
            array = new int[arraySize];
        }

        /**
         * 写入数据
         * @param key
         */
        public void add(String key){
            int first = hashcode_1(key);
            int second = hashcode_2(key);
            int third = hashcode_3(key);

            array[first % arraySize] = 1;
            array[second % arraySize] = 1;
            array[third % arraySize] = 1;
        }

        /**
         * 判断数据是否存在
         * @param key
         * @return
         */
        public boolean check(String key){
            int first = hashcode_1(key);
            int second = hashcode_2(key);
            int third = hashcode_3(key);

            int firstIndex = array[first % arraySize];
            if (firstIndex == 0){
                return false;
            }
            int secondIndex = array[second % arraySize];
            if (secondIndex == 0){
                return false;
            }
            int thirdIndex = array[third % arraySize];
            if (thirdIndex == 0){
                return false;
            }
            return true;
        }

        /**
         * hash 算法1
         * @param key
         * @return
         */
        private int hashcode_1(String key) {
            int hash = 0;
            int i;
            for (i = 0; i < key.length(); ++i) {
                hash = 33 * hash + key.charAt(i);
            }
            return Math.abs(hash);
        }

        /**
         * hash 算法2
         * @param data
         * @return
         */
        private int hashcode_2(String data) {
            final int p = 16777619;
            int hash = (int) 2166136261L;
            for (int i = 0; i < data.length(); i++) {
                hash = (hash ^ data.charAt(i)) * p;
            }
            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;
            return Math.abs(hash);
        }

        /**
         *  hash 算法3
         * @param key
         * @return
         */
        private int hashcode_3(String key) {
            int hash, i;
            for (hash = 0, i = 0; i < key.length(); ++i) {
                hash += key.charAt(i);
                hash += (hash << 10);
                hash ^= (hash >> 6);
            }
            hash += (hash << 3);
            hash ^= (hash >> 11);
            hash += (hash << 15);
            return Math.abs(hash);
        }

1、首先初始化了一个 int 数组。
2、写入数据的时候进行三次 hash 运算,同时把对应的位置置为1。
3、查询时同样的三次 hash 运算,取到对应的值,一旦有个值为0,则认为数据不存在。
下面来测试下:
bloomFilterTest:

   public static void bloomFilterTest(){
       long start = System.currentTimeMillis();
           int capacity = 10000000;
           BloomFilters bloomFilters = new BloomFilters(capacity);
           for (int i = 0; i < capacity; i++) {
               bloomFilters.add(i + "");
           }
           System.out.println(bloomFilters.check(2+ ""));
           long end = System.currentTimeMillis();
           System.out.println("执行时间:" + (end - start));
       }

测试类,HeapTest:

/**
 * 测试类
 * @author K. L. Mao
 * @create 2018/11/30
 */
public class HeapTest {
    public static void main(String[] args){
//        HashSetHeap.hashSetTest();
        BloomFilters.bloomFilterTest();
    }

执行结果如下:

自定义布隆过滤器执行结果

只花了 1 秒钟就写入了 1000W 的数据同时做出来准确的判断。
当我把 10000000 这个数拿去查询时,却返回了 true,这也体现了 BloomFilter 的误报率。
我们可以提高数组长度以及 hash 计算次数来降低误报率,但是相应的 CPU、内存的消耗就会提高;这就需要根据业务自行权衡了。
仔细观察 GC 日志发现,自定义的布隆过滤器 fullGC 非常的频繁(12次),同时老年代的使用率也达到了 90%,接近崩溃的边缘。
总的来说,就是内存利用率做的不好。
其实,Google guava 包提供了现成的 API,下面来看看业界权威的实现。

private static void guavaTest(){
        long start = System.currentTimeMillis();
        int capacity = 10000000;
        BloomFilter<Integer> filter = BloomFilter.create(
                Funnels.integerFunnel(), capacity, 0.01);
        for (int i = 0; i < capacity; i++) {
            filter.put(i);
        }
        System.out.println(filter.mightContain(capacity));
        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - start));
    }

也是同样的 1000W 的数据,执行没有问题。


guava 布隆过滤器执行结果

观察 GC 日志,发现没有一次 fullGC,同时老年代的使用率很低。和自定义的一对比,这里明细要好很多,也可以写入更多的数据。

源码分析

来看看 Google 的guava是如何实现的。
构造方法中有两个比较重要的参数,一个是预计存放多少数据,一个是可以接受的误报率。
我这里的 demo 分别是 1000W 和 0.01。

guava BloomFilter构造器

guava 会通过你预计的数量以及误报率帮你计算出你应当会使用的数组大小 numBits 以及需要计算几次 hash 函数 numHashFunctions。

put 写入函数

public <T> boolean put(T object, Funnel<? super T> funnel, int numHashFunctions, BloomFilterStrategies.LockFreeBitArray bits) {
            long bitSize = bits.bitSize();
            byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
            long hash1 = this.lowerEight(bytes);
            long hash2 = this.upperEight(bytes);
            boolean bitsChanged = false;
            long combinedHash = hash1;

            for(int i = 0; i < numHashFunctions; ++i) {
                bitsChanged |= bits.set((combinedHash & 9223372036854775807L) % bitSize);
                combinedHash += hash2;
            }

            return bitsChanged;
        }
  • 根据 murmur3_128() 方法得到一个 128 位长度的 byte[]。
  • 分别取高低 8 位,得到两个 hash 值。
  • 再根据初始化时的 hash 运算次数进行 hash 运算。

bitsChanged |= bits.set((combinedHash & 9223372036854775807L) % bitSize);

其实也是 hash 取模 拿到 index 后去赋值1。

重点是 bits.set() 方法。

static final class LockFreeBitArray {
        private static final int LONG_ADDRESSABLE_BITS = 6;
        final AtomicLongArray data;
        private final LongAddable bitCount;

        LockFreeBitArray(long bits) {
            this(new long[Ints.checkedCast(LongMath.divide(bits, 64L, RoundingMode.CEILING))]);
        }

        LockFreeBitArray(long[] data) {
            Preconditions.checkArgument(data.length > 0, "data length is zero!");
            this.data = new AtomicLongArray(data);
            this.bitCount = LongAddables.create();
            long bitCount = 0L;
            long[] var4 = data;
            int var5 = data.length;

            for(int var6 = 0; var6 < var5; ++var6) {
                long value = var4[var6];
                bitCount += (long)Long.bitCount(value);
            }

            this.bitCount.add(bitCount);
        }

        boolean set(long bitIndex) {
            if (this.get(bitIndex)) {
                return false;
            } else {
                int longIndex = (int)(bitIndex >>> 6);
                long mask = 1L << (int)bitIndex;

                long oldValue;
                long newValue;
                do {
                    oldValue = this.data.get(longIndex);
                    newValue = oldValue | mask;
                    if (oldValue == newValue) {
                        return false;
                    }
                } while(!this.data.compareAndSet(longIndex, oldValue, newValue));

                this.bitCount.increment();
                return true;
            }
        }

        boolean get(long bitIndex) {
            return (this.data.get((int)(bitIndex >>> 6)) & 1L << (int)bitIndex) != 0L;
        }
}

其实 set 方法是LockFreeBitArray的一个函数, LockFreeBitArray 就是真正存放数据的底层数据结构。
利用了一个AtomicLongArray(线程安全的数组)来存放数据。
来看 set 方法:

  • 在 set 之前,先通过 get() 判断这个数据是否存在于集合中,如果存在,则set 失败。
  • 接下来就是通过位运算对AtomicLongArray进行赋值(采用 CAS保证线程安全) 。
  • get() 方法的计算逻辑和 set 类似,只要判断不等于 0 就返回存在该值。
    来看 mightContain 是否存在函数:
      public <T> boolean mightContain(T object, Funnel<? super T> funnel, int numHashFunctions, BloomFilterStrategies.LockFreeBitArray bits) {
            long bitSize = bits.bitSize();
            long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
            int hash1 = (int)hash64;
            int hash2 = (int)(hash64 >>> 32);

            for(int i = 1; i <= numHashFunctions; ++i) {
                int combinedHash = hash1 + i * hash2;
                if (combinedHash < 0) {
                    combinedHash = ~combinedHash;
                }

                if (!bits.get((long)combinedHash % bitSize)) {
                    return false;
                }
            }

            return true;
        }

前面几步的逻辑都是类似的,只是调用了刚才的 get() 方法判断元素是否存在而言。

缓存穿透

在互联网行业,应对高并发的场景,使用缓存,是很常见的手段。这样能够避免一些重复的请求,到达 DB,从而降低数据库压力。但是,在使用缓存时,我们应该注意一种场景,那就是缓存穿透。当客户端一直使用不存在的 key 请求服务端时,就会一直查询数据库,这样缓存就没起到作用了。

缓存穿透的解决方案:

1、缓存空对象

当客户端使用一个 key 请求服务器时,我们会先去缓存(如redis)去查询是否存在该 key 所对应的数据,如果存在则直接返回结果;如果不存在,则去数据库查询。此时,如果数据库有该数据,则将该数据写入缓存,如果数据库没有该数据,则将一个空对象写入缓存(一般设置过期时间为5分钟)。在5分钟之内,如果客户端仍然拿这个不存在的 key 请求服务端,就会直接返回一个空对象过去了,而不会去查库。由于实现比较简单,这里就不代码演示了。

2、布隆过滤器

我们上面介绍的布隆过滤器,也是解决缓存穿透的一个方案,并且很多大厂也都在用它。下面,我们就用代码示例,来看下它是怎么用的。

/**
 * 布隆过滤器
 * @author K. L. Mao
 * @create 2018/12/1
 */
@Component
public class BloomFilters {

    private static final int capacity = 1000000;
    private static final double fpp = 0.01;
    private BloomFilter<String> filter;

    @Autowired
    private CmfOrderService cmfOrderService;

    @PostConstruct
    public void init(){
        filter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), capacity, fpp);
        List<CmfOrder> cmfOrders = cmfOrderService.findAllCmfOrder();
        if (cmfOrders.isEmpty()){
            return;
        }
        cmfOrders.forEach(cmfOrder -> filter.put(cmfOrder.getPaymentSeqNo()));
    }

    /**
     * 加入key
     * @param key
     */
    public void put(String key){
        filter.put(key);
    }

    /**
     * 判断是否存在
     * @param key
     * @return
     */
    public boolean mightContain(String key){
        return filter.mightContain(key);
    }
}

首先,定义一个布隆过滤器的配置类,让 Spring 去管理它,这样我们就可以让该配置类在构造完成之后,调用 init() 方法。
在 init() 方法里面,我们是将库里的所有订单查询出来,并且将所有的支付流水号paymentSeqNo赋值给 BloomFilter
这里我的预估容量是100W,允许误报率为0.01,你们可以根据实际的业务量进行调整。

    /**
     * 初始化支付单
     *
     * @param memberId
     * @param orderNo
     * @param fundChannelCode
     */
    @Override
    public void initCmfOrder(String memberId, String paymentSeqNo, String fundChannelCode) {
        // 每个订单入库之前,将支付流水号加入布隆过滤器
        bloomFilters.put(paymentSeqNo);
        CmfOrder cmfOrder= new CmfOrder();
        cmfOrder.setFundChannelCode(fundChannelCode);
        cmfOrder.setMemberId(memberId);
        cmfOrder.setPaymentSeqNo(paymentSeqNo);
        cmfOrderMapper.save(cmfOrder);
    }

每个订单入库之前,将支付流水号加入布隆过滤器。

    /**
     * 查询订单结果
     *
     * @param resultInfoForm
     * @return
     */
    @Override
    public ResultInfoDTO getResultInfo(ResultInfoForm resultInfoForm) throws GlobalException {
        log.info("查询订单结果,请求参数:{}", resultInfoForm);
        String paymentSeqNo = resultInfoForm.getOrderNo();
        // 到缓存查询
        ResultInfoDTO resultInfoDTO = jedisService.getFromCache(CacheTypeEnum.RESULT_INFO.getText(), paymentSeqNo);
        if (resultInfoDTO != null) {
            log.info("该订单号已有缓存,直接取缓存数据,resultInfoDTO:{}", resultInfoDTO);
            return resultInfoDTO;
        }
        if (!bloomFilters.mightContain(paymentSeqNo)){
            log.warn("订单号不存在,paymentSeqNo:{}", paymentSeqNo);
            throw new GlobalException(CodeMsg.ORDER_NO_ERROR);
        }
        //获取CmfOrder
        final CmfOrder cmfOrder = acquireCmfOrderByPaymentSeqNo(paymentSeqNo);
        return packageBindingResultInfo(cmfOrder);
    }

查询订单结果时,先去缓存查:jedisService.getFromCache(key),如果有,直接返回结果;如果没有,则使用布隆过滤器判断是否存在该 key:bloomFilters.mightContain(key)。如果布隆过滤器不存在该 key,则直接返回客户端错误信息;如果存在该 key,才允许其去查库。这样就能够抵挡住大部分不存在 key 的查库请求了。

但是,由于上面的BloomFilter 是 JDK 自带的,数据只保存在内存中,所以只适用单机环境,集群环境下是不适用的。那么,如何让其在集群环境也能支持呢?这时候我们就需要引入 Redis 的BizMap存储类型。

  • 基于Redis 的 BloomFilter
    由于 Redis 的BizMap存储的是二进制,正好和BloomFilter的存储完全吻合。于是,我们可以使用 Redis 来实现集群环境下的BloomFilter
    唯一需要注意的是 Redis 的BitMap只支持2^32 大小,对应到内存也就是 512MB,数组的下标最大只能是2^32-1。不过这个限制可以通过构建多个 Redis 的Bitmap通过 hash 取模的方式分散一下即可。万分之一的误判率,512MB 可以放下2亿条数据。
  1. RedisService
package yongda.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;
import yongda.fw.cache.JedisService;

/**
 * @author K. L. Mao
 * @create 2019/1/20
 */
@Component
public class RedisService {

    @Autowired
    private ShardedJedisPool jedisPool;

    /**
     * key 键对应的值 value 对应的 ascii 码,在 offset 的位置(从左向右数) 变为 value。
     * @param key
     * @param offset
     * @param value
     */
    public void putBitInCache(String key, long offset, boolean value) {
        ShardedJedis jedis = jedisPool.getResource();
        jedis.setbit(key, offset, value);
    }

    /**
     * 获取 Bit
     * @param key
     * @return
     */
    public String getBitFromCache(String key) {
        ShardedJedis jedis = jedisPool.getResource();
        return jedis.get(key);
    }


    /**
     * 判断指定的位置 ASCII 码的 bit 位是否为1。
     * @param key
     * @param offset
     * @return
     */
    public boolean getBitFromCache(String key, int offset) {
        ShardedJedis jedis = jedisPool.getResource();
        return jedis.getbit(key, offset);
    }
}

jedis.setbit(key, offset, value):key 键对应的值 value 对应的 ASCII 码,在 offset 的位置(从左向右数)设置为 value。
jedis.getbit(key, offset): 判断指定的位置 ASCII 码的 bit 位是否为 1。

  1. BloomFilter 工具类
package yongda.config;

import org.springframework.stereotype.Component;
import yongda.enums.CacheTypeEnum;
import yongda.service.CmfOrderService;
import yongda.util.DateUtil;
import yongda.vo.CmfOrder;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Date;
import java.util.List;

/**
 * 使用 redis 的 bizMap 实现 BloomFilter
 * @param <E>
 */
@Component
public class BloomFilter<E> {

    @Resource
    private RedisService redisService;
    @Resource
    private CmfOrderService cmfOrderService;

    // total length of the Bloom filter
    private static int sizeOfBloomFilter;
    // number of hash functions
    private static int numberOfHashFunctions;
    // 误差率
    private static final double falsePositiveProbability = 0.01;
    // 预计容量
    private static final int expectedNumberOfElements = 1000000;
    // encoding used for storing hash values as strings
    private final Charset charset = Charset.forName("UTF-8");
    // MD5 gives good enough accuracy in most circumstances. Change to SHA1 if it's needed
    private static final String hashName = "MD5";
    private static final MessageDigest digestFunction;

    // The digest method is reused between instances
    static {
        MessageDigest tmp;
        try {
            tmp = java.security.MessageDigest.getInstance(hashName);
        } catch (NoSuchAlgorithmException e) {
            tmp = null;
        }
        digestFunction = tmp;
        // numberOfHashFunctions = ceil(-ln(falsePositiveProbability)/ln2)
        numberOfHashFunctions = (int) Math.ceil(-(Math.log(falsePositiveProbability) / Math.log(2)));
        // sizeOfBloomFilter = ceil(numberOfHashFunctions*expectedNumberOfElements/ln2)
        sizeOfBloomFilter = (int) Math.ceil(numberOfHashFunctions * expectedNumberOfElements / Math.log(2));
    }

    @PostConstruct
    public void init(){
        // 获取一个月前的日期
        Date lastMonth = DateUtil.getDateByAdd("m", -1, new Date());
        List<CmfOrder> cmfOrders = cmfOrderService.findCmfOrdersOfLastMonth(lastMonth);
        if (cmfOrders.isEmpty()){
            return;
        }
        redisService.deleteCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText());
        cmfOrders.forEach(cmfOrder -> add(cmfOrder.getPaymentSeqNo().getBytes(charset)));
    }

    /**
     * Adds an object to the Bloom filter. The output from the object's
     * toString() method is used as input to the hash functions.
     *
     * @param element is an element to register in the Bloom filter.
     */
    public void add(E element) {
        add(element.toString().getBytes(charset));
    }

    /**
     * Adds an array of bytes to the Bloom filter.
     *
     * @param bytes array of bytes to add to the Bloom filter.
     */
    private void add(byte[] bytes) {
        if (redisService.getBitFromCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText()) == null) {
            redisService.putBitInCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText(), 0, false);
        }

        int[] hashes = createHashes(bytes, numberOfHashFunctions);
        for (int hash : hashes) {
            redisService.putBitInCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText(), Math.abs(hash % sizeOfBloomFilter), true);
        }
    }

    /**
     * Adds all elements from a Collection to the Bloom filter.
     *
     * @param c Collection of elements.
     */
    public void addAll(Collection<? extends E> c) {
        for (E element : c) {
            add(element);
        }
    }

    /**
     * Returns true if the element could have been inserted into the Bloom filter.
     * Use getFalsePositiveProbability() to calculate the probability of this
     * being correct.
     *
     * @param element element to check.
     * @return true if the element could have been inserted into the Bloom filter.
     */
    public boolean contains(E element) {
        return contains(element.toString().getBytes(charset));
    }

    /**
     * Returns true if the array of bytes could have been inserted into the Bloom filter.
     * Use getFalsePositiveProbability() to calculate the probability of this
     * being correct.
     *
     * @param bytes array of bytes to check.
     * @return true if the array could have been inserted into the Bloom filter.
     */
    private boolean contains(byte[] bytes) {
        int[] hashes = createHashes(bytes, numberOfHashFunctions);
        for (int hash : hashes) {
            if (!redisService.getBitFromCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText(), Math.abs(hash % sizeOfBloomFilter))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if all the elements of a Collection could have been inserted
     * into the Bloom filter. Use getFalsePositiveProbability() to calculate the
     * probability of this being correct.
     *
     * @param c elements to check.
     * @return true if all the elements in c could have been inserted into the Bloom filter.
     */
    public boolean containsAll(Collection<? extends E> c) {
        for (E element : c) {
            if (!contains(element)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 根据字节数组的内容生成摘要,并将结果分割成 4 字节的整数并存储在数组中。
     * 调用 digest 函数,直到生成所需的 int 数。每次生成摘要之前,都需要加盐,并且 salt++。
     *
     * @param data   specifies input data.
     * @param hashes number of hashes/int's to produce.
     * @return array of int-sized hashes
     */
    private static int[] createHashes(byte[] data, int hashes) {
        int[] result = new int[hashes];

        int k = 0;
        byte salt = 0;
        while (k < hashes) {
            byte[] digest;
            synchronized (digestFunction) {
                digestFunction.update(salt);
                salt++;
                digest = digestFunction.digest(data);
            }

            for (int i = 0; i < digest.length / 4 && k < hashes; i++) {
                int h = 0;
                for (int j = (i * 4); j < (i * 4) + 4; j++) {
                    h <<= 8;
                    h |= ((int) digest[j]) & 0xFF;
                }
                result[k] = h;
                k++;
            }
        }
        return result;
    }

    /**
     * Calculates a hash code for this class.
     *
     * @return hash code representing the contents of an instance of this class.
     */
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 61 * hash + sizeOfBloomFilter;
        hash = 61 * hash + expectedNumberOfElements;
        hash = 61 * hash + numberOfHashFunctions;
        return hash;
    }

}

原理是和 JDK 自带的BloomFilter类似的,我们看add方法,它先 Redis 缓存中是否有指定 key(如:orderBloomFilter) 的值,如果没有,则在 offset = 0 处,添加一个值为false,即为 0;然后调用createHashes(byte[] data, int hashes)方法,根据字节数组的内容生成digest,并将结果分割成 4 字节的整数并存储在数组中,数组中的整数可以理解为每次hash所得的hashcode的值。最后,遍历hashcode数组,将hashcode%sizeOfBloomFilter取模所得下标所对应的值设为true,即为 1。

contains方法,同样调用createHashes(byte[] data, int hashes)得到字节数组内容所对应的hashcode数组。遍历hashcode数组,如果有一个hashcode所对应的下标的值不为1,则该数据不存在。反之,只有所有的hashcode所对应的下标的值都为1,才能说明该数据已经存在。

补充:

问:十亿条淘宝购买记录,怎么获取出现最多的前十个?

这个问题,跟上面的问题类似,依旧是有限内存的海量数据处理的题目。这里由于需要获取数据信息,而不是判断是否存在,则不能使用布隆过滤器了。但是细想是一样的,那就是分而治之的思路:
1、对这海量数据进行 hash(id)%mod取摸,mod看数据规模,假如取1024;这样就将10亿数据分发到了1024个小文件上。
2、对每个小文件的 id 作为 key,出现次数作为 value 存储在 map 中,这样就能得到每个小文件上 value前10的map 集合。
3、将1024个小文件上的前10 map 集合进行整合,然后排序,取出前10 map 即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容