[博学谷学习记录]超强总结,用心分享|架构 布隆过滤器(BloomFilter)

文章内容是学习过程中的知识总结,如有纰漏,欢迎指正

文章目录

前言

一、什么是 BloomFilter?

1.产生的契机

2.数据结构&设计思想

3.误判率问题分析

4.不支持删除

5.如何选择哈希函数个数和布隆过滤器长度

二、布隆过滤器实现

1.Guava

2.Redisson

总结


前言

问题1:什么是Redis缓存穿透?缓存穿透如何解决?

问题2:如何在海量元素中(例如 10 亿无序、不定长、不重复)快速判断一个元素是否存在?


以下是本篇文章正文内容

一、什么是 BloomFilter?

布隆过滤器(英语:Bloom Filter)是 1970 年由Burton Howard Bloom提出的,是一种空间效率高的概率型数据结构,以下简称BF。

本质上其实就是一个很长的二进制向量和一系列随机映射函数。专门用来检测集合中是否存在特定的元素

1.产生的契机

回想一下,我们平常在检测集合中是否存在某元素时,都会采用比较的方法。考虑以下情况:

  • 如果集合用线性表存储,查找的时间复杂度为O(n)。
  • 如果用平衡BST(如AVL树、红黑树)存储,时间复杂度为O(logn)。
  • 如果用哈希表存储,并用链地址法与平衡BST解决哈希冲突(参考JDK8的HashMap实现方法),时间复杂度也要有O[log(n/m)],m为哈希分桶数。

 总而言之,当集合中元素的数量极多时,不仅查找会变得很慢,而且占用的空间也会大到无法想象。BF就是解决这个矛盾的利器。

2.数据结构&设计思想

BF是由一个长度为m比特的位数组(bit array)k个哈希函数(hash function)组成的数据结构。位数组均初始化为0,所有哈希函数都可以分别把输入数据尽量均匀地散列。

 基于BitMap:

 如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位,设置为1

 

 

当要插入一个元素时,将其数据分别输入k个哈希函数,产生k个哈希值。以哈希值作为位数组中的下标,将所有k个对应的比特置为1。

当要查询(即判断是否存在)一个元素时,同样将其数据输入哈希函数,然后检查对应的k个比特。如果有任意一个比特为0,表明该元素一定不在集合中。如果所有比特均为1,表明该集合有(较大的)可能性在集合中。为什么不是一定在集合中呢?因为一个比特被置为1有可能会受到其他元素的影响,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在BF中是绝不会出现的

  • 如果这些点有任何一个 0,则被检索元素一定不在
  • 如果都是 1,则被检索元素很可能在。

3.误判率问题分析

 哈希函数有以下两个特点:

  • 如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。
  • 散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的。但也可能不同,这种情况称为 “散列碰撞”(或者 “散列冲突”)

 布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。

4.不支持删除

hash碰撞这种情况也造成了布隆过滤器的删除问题,传统的布隆过滤器并不支持删除操作,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。

5.如何选择哈希函数个数和布隆过滤器长度

很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

 如何选择适合业务的 k 和 m 值呢,这里直接贴一个公式:

二、布隆过滤器实现

1.Guava

引入Guava pom配置

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

代码实现

public class BloomFilterTest {

   @Test
    public  void test1() {
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
        // 插入10万样本数据
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }

        // 用另外十万测试数据,测试误判率
        int count = 0;
        for (int i = capacity; i < size + 100000; i++) {
            if (bloomFilter.mightContain(i)) {
                count++;
                System.out.println(i + "误判了");
            }
        }
        System.out.println("总共的误判数:" + count);
    }
    }
}

运行结果:

 10万数据里有947个误判,约等于0.01%,也就是代码里设置的误判率:fpp = 0.01

代码分析:

核心BloomFilter.create方法:

@VisibleForTesting
  static <T> BloomFilter<T> create(
      Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
    。。。。
}

这里有四个参数:

  • funnel:数据类型(通常是调用Funnels工具类中的)
  • expectedInsertions:指望插入的值的个数
  • fpp:误判率(默认值为0.03)
  • strategy:哈希算法

重点说一下fpp参数

fpp误判率

情景一:fpp = 0.01

  • 误判个数:947 占内存大小:9585058位数

情景二:fpp = 0.03(默认参数)

  • 误判个数:3033 占内存大小:7298440位数

总结

  • 误判率能够经过fpp参数进行调节
  • fpp越小,须要的内存空间就越大:0.01须要900多万位数,0.03须要700多万位数。
  • fpp越小,集合添加数据时,就须要更多的hash函数运算更多的hash值,去存储到对应的数组下标里。(忘了去看上面的布隆过滤存入数据的过程)

2.Redisson

上面使用Guava实现的布隆过滤器是把数据放在了本地内存中。分布式的场景中就不合适了,没法共享内存

还能够用Redis来实现布隆过滤器,这里使用Redis封装好的客户端工具Redisson

pom配置:

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.13.4</version>
</dependency>

java代码:

public class RedissonBloomFilter {

  public static void main(String[] args) {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");
    config.useSingleServer().setPassword("1234");
    //构造Redisson
    RedissonClient redisson = Redisson.create(config);

    RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");
    //初始化布隆过滤器:预计元素为100000000L,偏差率为3%
    bloomFilter.tryInit(100000000L,0.03);
    //将号码10086插入到布隆过滤器中
    bloomFilter.add("10086");

    //判断下面号码是否在布隆过滤器中
    //输出false
    System.out.println(bloomFilter.contains("123456"));
    //输出true
    System.out.println(bloomFilter.contains("10086"));
  }
}

总结

这里回答下文章开头的两个问题:

1.答:缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,造成数据库的压力倍增的情况

解决方案:

(1)接口层增加校验,比如用户鉴权校验,参数做校验 比如:id 做基础校验,id <=0的直接拦截

(2)对于像ID为负数的非法请求直接过滤掉,采用布隆过滤器(Bloom Filter)

(3)针对在数据库中找不到记录的,我们仍然将该空数据存入缓存中,当然一般会设置一个较短的过期时间

2.答:使用文章中布隆过滤器的两种实现方式Guava或者Redisson

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值