当遇到需要去重,或者过滤数据的时候,一般都直接想到使用redis的set集合进行处理,当数据量少的时候,没有任何问题,使用方便&准确性高;但是当数据量庞大时,变面临着内存的急剧上升,最终导致服务异常。除了使用set集合将数据全部去重后存储下来,还可以使用布隆过滤器进行去重、过滤的操作。
原理
布隆过滤器的核心思想可以简化为位数组和多个哈希函数的结合。通过这些工具,布隆过滤器能够实现空间高效、查询快速且允许少量误报的数据判断。核心要诀:布隆过滤中不存在,该数据一定不存在;布隆过滤器中存在,该数据不一定存在。(存在发生多个hash函数同时碰撞的情况)
拆解它的工作原理:
位数组
布隆过滤器使用一个长度为 m
的位数组,每一位都初始化为 0
。这些位将用于表示元素的存在情况。
哈希函数
布隆过滤器配备了 k
个独立的哈希函数。每个哈希函数都会为输入的元素生成一个 0
到 m-1
之间的索引值,也就是将元素“映射”到位数组中的某个位置。通过多个哈希函数,一个元素会影响位数组中的多个位置,从而降低哈希冲突的风险。
添加元素
当我们将一个元素添加到布隆过滤器时,k
个哈希函数会为该元素生成 k
个不同的索引值,并将位数组中的这 k
个位置设置为 1
。例如,某个元素的哈希结果是 2、8 和 12,那么我们会将位数组中的第 2、8、12 位设置为 1
。
查询元素
查询某个元素是否存在时,布隆过滤器会使用相同的 k
个哈希函数。哈希函数会生成对应的 k
个索引值并检查位数组中的这些位置是否都为 1
。如果都为 1
,则表示该元素可能存在;如果有任何一位为 0
,则可以确定该元素不存在。
误报率
布隆过滤器的一个关键特性是可能误报。由于位数组大小有限,多个元素可能会通过不同的哈希函数映射到相同的位置,导致某些不在集合中的元素被误认为存在。这种情况称为误报。不过,布隆过滤器不会出现漏报,即如果某个元素确实存在,它一定会被正确识别。
误报率的大小与位数组的长度 m
、哈希函数的数量 k
、以及集合中元素的数量 n
有关。通过合理选择这些参数,我们可以在误报率和存储效率之间做出权衡。
优点
- 相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(即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();
}