布隆过滤器
在1970年的时候,有一个叫做布隆的前辈对于判断海量元素中元素是否存在的问题进行了研究,也就是到底需要多大的位图容量和多少个哈希函数,它发表了一篇论文,提出的这个容器就叫做布隆过滤器。
工作原理
首先,布隆过滤器的本质就是一个位数组和若干个哈希函数。
集合里面有3个元素,要把它存到布隆过滤器里面去,应该怎么做?首先是a元素,这里我们用3次计算。b.c元素也一样。
元素已经存进去之后,现在我要来判断一个元素在这个容器里面是否存在,就要使用同样的三个函数进行计算。
比如d元素,我用第一个函数f1计算,发现这个位置上是1,没问题。第二个位置也是1,第三个位置也是1。
如果经过三次计算得到的下标位置值都是1,这种情况下,能不能确定d元素一定在这个容器里面呢?实际上是不能的。比如这张图里面,这三个位置分别是把a,b,c存进去的时候置成1的,所以即使d元素之前没有存进去,也会得到三个1,判断返回true。
所以,这个是布隆过滤器的一个很重要的特性,因为哈希碰撞不可避免,所以它会存在一定的误判率。这种把本来不存在布隆过滤器中的元素误判为存在的情况,我们把它叫做假阳性(False Positive Probability,FPP)。
我们再来看另一个元素,e元素。我们要判断它在容器里面是否存在,一样地要用这三个函数去计算。第一个位置是1,第二个位置是1,第三个位置是0。
e元素是不是一定不在这个容器里面呢?可以确定一定不存在。如果说当时已经把e元素存到布隆过滤器里面去了,那么这三个位置肯定都是1,不可能出现0。
总结:布隆过滤器的特点:
从容器的角度来说
- 如果布隆过滤器判断元素在集合中存在,不一定存在
- 如果布隆过滤器判断不存在,一定不存在电包学
从元素的角度来说
- 如果元素实际存在,布隆过滤器一定判断存在
- 如果元素实际不存在,布隆过滤器可能判断存在
Guava的实现
谷歌的Guava里面就提供了一个现成的布隆过滤器。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
<dependency>
创建布隆过滤器:
BloomFilter<String> bf -BloomFilter. create(Funnels.stringFunnel(Charsets. UTF_8),insertions);
布隆过滤器提供的存放元素的方法是put()。
布隆过滤器提供的判断元素是否存在的方法是mightContain()。
if(bf.mightContain(data)){
if(sets.contains(data)){
//判断存在实际存在的时候,命中
right++;
continue;
}
//判断存在却不存在的时候,错误
wrongt++;
}
布隆过滤器把误判率默认设置为0.03,也可以在创建的时候指定。
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
return create(funnel, expectedInsertions, 0.03D);
}
位图的容量是基于元素个数和误判率计算出来的。
long numBits = optimalNumOfBits(expectedInsertions, fpp);
根据位数组的大小,我们进一步计算出了哈希函数的个数。
int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
存储100万个元素只占用了0.87M的内存,生成了5个哈希函数。
布隆过滤器在项目中的使用
布隆过滤器的工作位置:
因为要判断数据库的值是否存在,所以第一步是加载数据库所有的数据。在去Redis查询之前,先在布隆过滤器查询,如果bf说没有,那数据库肯定没有,也不用去查了。
如果bf说有,才走之前的流程。
/**
* 布隆过滤器并发测试,在redis和数据库之间
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@EnableAutoConfiguration
public class BloomTestsConcurrency {
@Resource
private RedisTemplate redisTemplate;
@Autowired
private UserService userService;
private static final int THREAD_NUM = 1000; // 并发线程数量,Windows机器不要设置过大
static BloomFilter<String> bf;
static List<User> allUsers;
@PostConstruct
public void init() {
// 从数据库获取数据,加载到布隆过滤器
long start = System.currentTimeMillis();
allUsers = userService.getAllUser();
if (allUsers == null || allUsers.size() == 0) {
return;
}
// 创建布隆过滤器,默认误判率0.03,即3%
bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), allUsers.size());
// 误判率越低,数组长度越长,需要的哈希函数越多
// bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), allUsers.size(), 0.0001);
// 将数据存入布隆过滤器
for (User user : allUsers) {
bf.put(user.getAccount());
}
long end = System.currentTimeMillis();
System.out.println("查询并加载"+allUsers.size()+"条数据到布隆过滤器完毕,总耗时:"+(end -start ) +"毫秒");
}
@Test
public void cacheBreakDownTest() {
long start = System.currentTimeMillis();
allUsers = userService.getAllUser();
CyclicBarrier cyclicBarrier = new CyclicBarrier(THREAD_NUM);
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_NUM);
for (int i = 0; i < THREAD_NUM; i++){
executorService.execute(new BloomTestsConcurrency().new MyThread(cyclicBarrier, redisTemplate, userService));
}
executorService.shutdown();
//判断是否所有的线程已经运行完
while (!executorService.isTerminated()) {
}
long end = System.currentTimeMillis();
System.out.println("并发数:"+THREAD_NUM + ",新建线程以及过滤总耗时:"+(end -start ) +"毫秒,演示结束");
}
public class MyThread implements Runnable {
private CyclicBarrier cyclicBarrier;
private RedisTemplate redisTemplate;
private UserService userService;
public MyThread(CyclicBarrier cyclicBarrier, RedisTemplate redisTemplate, UserService userService) {
this.cyclicBarrier = cyclicBarrier;
this.redisTemplate = redisTemplate;
this.userService = userService;
}
@Override
public void run() {
//所有子线程等待,当子线程全部创建完成再一起并发执行后面的代码
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
// 1.1 (测试:布隆过滤器判断不存在,拦截——如果没有布隆过滤器,将造成缓存穿透)
// 随机产生一个字符串,在redis中不存在
String randomUser = UUID.randomUUID().toString();
// 1.2 (测试:布隆过滤器判断存在,从Redis缓存取值,如果Redis为空则查询数据库并写入Redis)
// 从List中获取一个存在的用户
// String randomUser = allUsers.get(new Random().nextInt(allUsers.size())).getAccount();
String key = "Key:" + randomUser;
Date date1 = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 如果布隆过滤器中不存在这个用户直接返回,将流量挡掉
if (!bf.mightContain(randomUser)) {
System.out.println(sdf.format(date1)+" 布隆过滤器中不存在,非法请求");
return;
}
// 查询缓存,如果缓存中存在直接返回缓存数据
ValueOperations<String, String> operation =
(ValueOperations<String, String>) redisTemplate.opsForValue();
Object cacheUser = operation.get(key);
if (cacheUser != null) {
Date date2 = new Date();
System.out.println(sdf.format(date2)+" 命中redis缓存");
return;
}
// TODO 防止并发重复写缓存,加锁
synchronized (randomUser) {
// 如果缓存不存在查询数据库
List<User> user = userService.getUserByAccount(randomUser);
if (user == null || user.size() == 0) {
// 很容易发生连接池不够用的情况 HikariPool-1 - Connection is not available, request timed out after
System.out.println(" Redis缓存不存在,查询数据库也不存在,发生缓存穿透!!!");
return;
}
// 将mysql数据库查询到的数据写入到redis中
Date date3 = new Date();
System.out.println(sdf.format(date3)+" 从数据库查询并写入Reids");
operation.set("Key:" + user.get(0).getAccount(), user.get(0).getAccount());
}
}
}
}
布隆过滤器的不足与变种
如果数据库删除了,布隆过滤器的数据也要删除。但是布隆过滤器里面没有提供删除的方法。为什么布隆过滤器不提供删除的方法呢?或者说,如果删除了布隆过滤器的元素,会发生什么问题?
比如我们把a删除了,那个三个位置都要改成0。但是再来判断b元素是否存在的时候,因为有一个位置变成了0,所以b元素也判断不存在。就是因为存在哈希碰撞,所以元素只能存入,不能删除。
那如果我们要实现删除的功能,怎么做呢?类似于HashMap的链地址法,我们可以在每个下标位置上增加一个计数器。比如这个位置命中了两次,计数器就是2,当删除a元素的时候,先把计数器改成1。删除b元素的时候,计数器变成0,这个时候下标对应的位才置成0。
实际上在布隆过滤器提出来的几十年里面,出现了很多布隆过滤器的变种,这种通过计数器提供删除功能的bf就叫做Counting Bloom Filter。
/**
* https://github.com/Baqend/Orestes-Bloomfilter
*/
public class CountingBloomFilterTest {
public static void main(String[] args) {
CountingBloomFilter<String> cbf = new FilterBuilder(1000,
0.01).buildCountingBloomFilter();
cbf.add("http://xiecongcong.com");
cbf.add("http://alibaba.com");
cbf.add("http://baidu.com");
cbf.remove("http://baidu.com");
System.out.println(cbf.contains("http://xiecongcong.com")); //true
System.out.println(cbf.contains("http://alibaba.com")); //true
System.out.println(cbf.contains("http://baidu.com")); //false
}
}
布隆过滤器的其他应用场景
布隆过滤器解决的问题是什么?如何在海量元素中快速判断一个元素是否存在。所以除了解决缓存穿透的问题之外,我们还有很多其他的用途。
比如爬数据的爬虫,爬过的url我们不需要重复爬,那么在几十亿的url里面,怎么判断一个url是不是已经爬过了?
还有我们的邮箱服务器,发送垃圾邮件的账号我们把它们叫做spamer,在这么多的邮箱账号里面,怎么判断一个账号是不是spamer?
等等一些场景,我们都可以用到布隆过滤器。