相信大家都用过redis缓存,无论是面试或者生产实践中,肯定遇到过缓存穿透相关问题,常见的解决方案如下:
1、缓存空对象
2、布隆过滤器
今天主要和大家分享下用java代码实现布隆过滤器
一、布隆过滤器的概念
布隆过滤器是一种基于位数组和哈希的数据结构,能够高效的插入和查找,相比于HashMap等,布隆过滤器占用内存低。但是布隆过滤器存在一定的误报率,所以使用布隆过滤器,业务上要允许误差,不过误报率可调。
布隆过滤器认为不存在的数据一定不存在,布隆过滤器认为存在的数据可能存在。就好似一个人不认识一个人一定不认识,认识一个人可能认识,和之前的人长得很像。
二、布隆过滤器两个重要参数
假设样本个数为n,即要输入n个对象构建布隆过滤器。
则布隆过滤器大小m和哈希函数的个数k,计算公式如下:(小数向上取整)
三、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");
}
}
五、测试结果
可以看到,100万个样本数据,测试1000万个不存在的数据,布隆过滤器误报数只有74,HashMap消耗时间是布隆过滤器将近2倍,同时HashMap占用约92MB,布隆过滤器占用只有2MB不到。