布隆过滤器(Bloom Filter):本质上是一种概率型数据结构(probabilistic data structure),优点是高效地插入和查询,根据查询结果快速判断某样东西一定不存在或者可能存在,缺点是只能插入不能删除.
布隆过滤器相比于传统的 List,Set,Map 等数据结构,它更高效,占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的.
布隆过滤器是一个 bit向量或者说 bit数组,当我们映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并且对每个生成的哈希值指向的 bit 位置1, bit数组类似于下图:
例如针对值"baidu"和三个不同的哈希函数分别生成了哈希值1,4,7则上图转变为:
我们现在再存一个值"tencent",如果哈希函数返回3,4,8的话,图继续变为:
注意:存入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依赖
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>24.0-jreversion>
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);
}
}
上面使用guava实现布隆过滤器是把数据放在本地内存中,我们项目往往是分布式的,我们还可以把数据放在redis中,用redis来实现布隆过滤器,这就需要我们自己设计映射函数,自己度量二进制向量的长度,下面是我在网上查找所得,本人已经测试,可以正常运行.
redis布隆过滤器核心类
public class RedisBloomHelper {
//元素预计数量
private final int numApproxElements;
//误差率
private final double fpp;
//哈希函数个数
private int numHashFunctions;
//bit图长度
private int bitmapLength;
private Funnel 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) Funnels.stringFunnel(Charsets.UTF_8);
}
/**
* 构造布隆过滤器.注意:在同一业务场景下,三个参数务必相同
*
* @param numApproxElements 预估元素数量
* @param fpp 误差率
* @param funnel 指定存储的存储类型
*/
public RedisBloomHelper(int numApproxElements, double fpp, Funnel 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 泛型,可以传入任何类型的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 泛型,可以传入任何类型的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 泛型,可以传入任何类型的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×tamp=1587521741&app=news_article&utm_source=weixin&utm_medium=toutiao_android&req_id=202004221015410101290331480C5D2EC4&group_id=6817627686840566283
本公众号所有内容均来自互联网,仅用于分享自己日常工作与学习的心得.