Bootstrap

Redis-布隆过滤器

当遇到需要去重,或者过滤数据的时候,一般都直接想到使用redis的set集合进行处理,当数据量少的时候,没有任何问题,使用方便&准确性高;但是当数据量庞大时,变面临着内存的急剧上升,最终导致服务异常。除了使用set集合将数据全部去重后存储下来,还可以使用布隆过滤器进行去重、过滤的操作。

原理

布隆过滤器的核心思想可以简化为位数组多个哈希函数的结合。通过这些工具,布隆过滤器能够实现空间高效、查询快速且允许少量误报的数据判断。核心要诀:布隆过滤中不存在,该数据一定不存在;布隆过滤器中存在,该数据不一定存在。(存在发生多个hash函数同时碰撞的情况)拆解它的工作原理:

位数组

布隆过滤器使用一个长度为 m位数组,每一位都初始化为 0。这些位将用于表示元素的存在情况。

哈希函数

布隆过滤器配备了 k独立的哈希函数。每个哈希函数都会为输入的元素生成一个 0m-1 之间的索引值,也就是将元素“映射”到位数组中的某个位置。通过多个哈希函数,一个元素会影响位数组中的多个位置,从而降低哈希冲突的风险。

添加元素

当我们将一个元素添加到布隆过滤器时,k 个哈希函数会为该元素生成 k 个不同的索引值,并将位数组中的这 k 个位置设置为 1。例如,某个元素的哈希结果是 2、8 和 12,那么我们会将位数组中的第 2、8、12 位设置为 1

查询元素

查询某个元素是否存在时,布隆过滤器会使用相同的 k 个哈希函数。哈希函数会生成对应的 k 个索引值并检查位数组中的这些位置是否都为 1。如果都为 1,则表示该元素可能存在;如果有任何一位为 0,则可以确定该元素不存在

误报率

布隆过滤器的一个关键特性是可能误报。由于位数组大小有限,多个元素可能会通过不同的哈希函数映射到相同的位置,导致某些不在集合中的元素被误认为存在。这种情况称为误报。不过,布隆过滤器不会出现漏报,即如果某个元素确实存在,它一定会被正确识别。

误报率的大小与位数组的长度 m、哈希函数的数量 k、以及集合中元素的数量 n 有关。通过合理选择这些参数,我们可以在误报率和存储效率之间做出权衡。

image-20241020161842875

优点

  • 相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(即hash函数的个数)。
  • Hash 函数相互之间没有关系,方便由硬件并行实现。
  • 布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
  • 布隆过滤器可以表示全集,其它任何数据结构都不能。

缺点

  • 有误判率存在。
  • 不支持删除单一元素,只能整个过滤器重新生成。

适用场景

  • 预防缓存穿透:布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在(过滤恶意攻击,大量访问无效的key)。
  • 网络爬虫:布隆过滤器可以用来去重已经爬取过的URL。
  • 邮箱的垃圾邮件过滤。
  • 黑白名单。

使用

Redis使用

@Service
public class BloomFilterService {

    @Autowired
    private RedissonClient redisson;

    /**
     * 创建布隆过滤器
     * @param filterName 过滤器名称
     * @param expectedInsertions 预测插入数量
     * @param falseProbability 误判率
     * @param <T>
     * @return
     */
    public <T> RBloomFilter<T> create(String filterName, long expectedInsertions, double falseProbability) {
        RBloomFilter<T> bloomFilter = redisson.getBloomFilter(filterName);
        bloomFilter.tryInit(expectedInsertions, falseProbability);
        return bloomFilter;
    }

}
@SpringBootTest(classes = RedissionApplication.class)
public class BloomFilterTest {

    @Autowired
    private BloomFilterService bloomFilterService;

    @Test
    public void testBloomFilter() {
        // 预期插入数量 (位数组越大,发生hash碰撞的概率便越低,但占据内存空间越大)
        long expectedInsertions = 10000L;
        // 误报率(当无保留越低,使用的hash函数变越多,占用的内存空间越大,效率越低)
        double falseProbability = 0.01;
        RBloomFilter<Long> bloomFilter = bloomFilterService.create("domainBlackList", expectedInsertions, falseProbability);
        // 布隆过滤器增加元素
        bloomFilter.add("hello world");
        // 已添加到布隆过滤器的元素的数量
        long elementCount = bloomFilter.count();
        // 判断下面元素是否在布隆过滤器中
        bloomFilter.contains("hello world");//true
        bloomFilter.contains("hello worlds");//false
        // 预期插入元素的个数
        bloomFilter.getExpectedInsertions();
        // 元素存在的错误概率
        bloomFilter.getFalseProbability();  
        // 每个元素使用的哈希迭代次数
        bloomFilter.getHashIterations()
        // 实例所需Redis内存的位数
        bloomFilter.getSize();
    }
}

Guava工具类使用

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>
private static void GuavaBloomFilter() {
    // 创建布隆过滤器对象
    BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),EXPECTED_INSERTIONS,FALSE_PROBABILITY);
    // 向过滤器中添加元素
    bloomFilter.put("hello world");
    // 判断下面元素是否在布隆过滤器中
    bloomFilter.contains("hello world");//true
    bloomFilter.contains("hello worlds");//false
    // 已添加到布隆过滤器的元素的数量
    long elementCount = bloomFilter.count();
    // 返回元素存在的错误概率
    bloomFilter.expectedFpp();
}
;