相信大家都用过redis缓存,无论是面试或者生产实践中,肯定遇到过缓存穿透相关问题,常见的解决方案如下:
1、缓存空对象
2、布隆过滤器
今天主要和大家分享下用java代码实现布隆过滤器

一、布隆过滤器的概念

布隆过滤器是一种基于位数组和哈希的数据结构,能够高效的插入和查找,相比于HashMap等,布隆过滤器占用内存低。但是布隆过滤器存在一定的误报率,所以使用布隆过滤器,业务上要允许误差,不过误报率可调。

布隆过滤器认为不存在的数据一定不存在,布隆过滤器认为存在的数据可能存在。就好似一个人不认识一个人一定不认识,认识一个人可能认识,和之前的人长得很像。

二、布隆过滤器两个重要参数

假设样本个数为n,即要输入n个对象构建布隆过滤器。

则布隆过滤器大小m和哈希函数的个数k,计算公式如下:(小数向上取整)

java 实现布隆过滤器 布隆过滤器代码实现_大数据


三、Java代码实现

package com.rising.bloom;

import java.util.BitSet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: rising
 * @Description:
 * @Date: create in 2020/9/12 12:03
 */
public class BloomFilter {

    //默认布隆过滤器样本数
    private static int DEFAULT_SIZE = 10 * 10000;
    //默认左移位数
    private static int DEFAULT_LEFT = 4;
    //默认右移位数
    private static int DEFAULT_RIGHT = 16;
    private static Double DEFAULT_FALSE_POSITIVE_RATE = 0.001;
    private int needHashs;
    //布隆过滤器已使用总数
    private AtomicInteger sum = new AtomicInteger(0);
    private RotatingHash[] rotatingHashes;
    private BitSet bitSet;

    /**
     * 默认样本数是10万,误报率是千分之一
     */
    public BloomFilter() {
        this(DEFAULT_LEFT, DEFAULT_RIGHT, DEFAULT_SIZE, DEFAULT_FALSE_POSITIVE_RATE);
    }

    /**
     *
     * @param size  样本数
     * @param falsePositiveRate  误报率
     */
    public BloomFilter(int size, Double falsePositiveRate) {
        this(DEFAULT_LEFT, DEFAULT_RIGHT, size, falsePositiveRate);
    }

    /**
     * 采用的是位移Hash,所以需要提供左位移数,右位移数
     * @param left
     * @param right
     * @param size   样本数
     * @param falsePositiveRate    误报率
     */
    public BloomFilter(int left, int right, int size, Double falsePositiveRate) {
        //计算布隆过滤器总空间,利用已经网上已经推导的公式
        int length = (int)Math.ceil(-Math.log(falsePositiveRate) / (Math.log(2) * Math.log(2))) * size;
        bitSet = new BitSet(length);
        //计算需要的hash函数总数
        needHashs = (int) Math.ceil(0.7 * length / size);
        rotatingHashes = new RotatingHash[needHashs];
        for (int i = 0; i < needHashs; i++) {
            rotatingHashes[i] = new RotatingHash(left + i, right + i, length - 1);
        }
    }

    /**
     * 添加方法
     * @param value
     */
    synchronized public void add(String value) {
        sum.incrementAndGet();
        //通过不同的hash计算出索引,将其所在的位置设置1
        for (RotatingHash rotatingHash : rotatingHashes) {
            bitSet.set(rotatingHash.rotatingHash(value), true);
        }
    }

    /**
     * 必须所有的hash计算值所在的位置都为1,才能返回true(存在误判率)
     * 布隆过滤器认为不存在的一定不存在
     * @param value
     * @return
     */
    public boolean contain(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (RotatingHash rotatingHash : rotatingHashes) {
            ret = ret && bitSet.get(rotatingHash.rotatingHash(value));
        }
        return ret;
    }

    /**
     * 获取总数
     * @return
     */
    public int getSum() {
        return sum.intValue();
    }

    /**
     * 位运算计算hash
     */
    public static class RotatingHash {
        //左移位数
        private int left;
        //右移位数
        private int right;
        //任意质数
        private int prime;

        public RotatingHash(int left, int right, int prime) {
            this.left = left;
            this.right = right;
            this.prime = prime;
        }

        public int rotatingHash(String key) {
            int hash, i;
            for (hash = key.length(), i = 0; i < key.length(); ++i) {
                hash = (hash << left) ^ (hash >> right) ^ key.charAt(i);
            }
            return Math.abs(hash % prime);
        }
    }
}

四、测试案例

package com.rising.bloom;

import com.carrotsearch.sizeof.RamUsageEstimator;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: rising
 * @Description:
 * @Date: create in 2020/9/12 12:25
 */
public class BloomFilterTest {
    public static void main(String[] args) {
        System.out.println("===================测试布隆过滤器===================");
        int datas = 100 * 10000;
        BloomFilter bloomFilter = new BloomFilter(datas, 0.001);
        for (int i = 0; i < datas; i++) {
            bloomFilter.add(String.valueOf(i));
        }
        //测试1000万个不存在,看误报率
        int falseSum = 0;
        long startTime = System.currentTimeMillis();
        for (int i = datas; i < datas + 1000 * 10000; i++) {
            boolean contain = bloomFilter.contain(String.valueOf(i));
            if (contain) {
                falseSum++;
            }
        }
        long endTime = System.currentTimeMillis();
        double bloomByteSize = new BigDecimal(RamUsageEstimator.sizeOf(bloomFilter) / (1024.0 * 1024)).setScale(3, BigDecimal.ROUND_HALF_UP).doubleValue();
        System.out.println("布隆过滤器总数:" + bloomFilter.getSum());
        System.out.println("布隆过滤器误报数:" + falseSum);
        System.out.println("布隆过滤器占用空间:" + bloomByteSize + "MB字节");
        System.out.println("布隆过滤器处理时间:" + (endTime - startTime) + "ms");
        System.out.println("===================测试HashMap===================");
        Map<String, Object> map = new HashMap<String, Object>(datas);
        for (int i = 0; i < datas; i++) {
            map.put(i + "", null);
        }
        //测试1000万个不存在,看误报率
        falseSum = 0;
        startTime = System.currentTimeMillis();
        for (int i = datas; i < datas + 1000 * 10000; i++) {
            boolean contain = map.containsKey(i + "");
            if (contain) {
                falseSum++;
            }
        }
        endTime = System.currentTimeMillis();
        double mapByteSize = new BigDecimal(RamUsageEstimator.sizeOf(map) / (1024.0 * 1024)).setScale(3, BigDecimal.ROUND_HALF_UP).doubleValue();
        System.out.println("HashMap总数:" + map.size());
        System.out.println("HashMap误报数:" + falseSum);
        System.out.println("HashMap占用空间:" + mapByteSize + "MB字节");
        System.out.println("HashMap处理时间:" + (endTime - startTime) + "ms");
    }
}

五、测试结果

java 实现布隆过滤器 布隆过滤器代码实现_大数据_02


可以看到,100万个样本数据,测试1000万个不存在的数据,布隆过滤器误报数只有74,HashMap消耗时间是布隆过滤器将近2倍,同时HashMap占用约92MB,布隆过滤器占用只有2MB不到。