布隆过滤器
1、布隆过滤器概念
相信大多数人听到布隆过滤器的时候都是在处理Redis的缓存穿透问题,那么缓存穿透是什么?布隆过滤器是什么?它又是如何解决问题的呢?
什么是布隆过滤器?
其实布隆过滤器(Bloom Filter) 早在上个世纪就被一位叫布隆的人提供,它的出现旨在解决检索问题,说白了布隆过滤器也就是为了检索一个元素是否在一个集合中
布隆过滤器的技术选型
既然知道了布隆过滤器是为了检索元素,那么如何判断一个集合中是否包含这个元素呢?
我相信大家都能明白:最直接的办法就是将所有集合中的元素保存起来,然后与目标元素一一进行比较。
保存数据的数据结构有很多,我们常见的数组、链表、树等等都是,可是当遇到数据量非常大的时候,存储的需求也会越来越大,检索的速度也随之慢了下来。
解决问题:
不过世界上还有一种叫作散列表(又叫哈希表,Hash table)的数据结构。它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。
2、布隆过滤器优缺点
优点:
- 因为存储的是二进制数据,因此占用的空间很小
- 它的插入和查询速度是很是快的,时间复杂度是O(n)
- 保密性很好,由于自己不存储任何原始数据,只有二进制数据
缺点:
- 布隆过滤器并不是绝对的准确也有可能存在误差
- 如果两个对象的hash值计算出一样,就会导致误差
- 布隆过滤器删除数据并非如此简单,布隆过滤器无法保证删除的元素的确在布隆过滤器里面
3、布隆过滤器的应用
之前文章开头提到过,我们常常使用布隆过滤器来解决redis缓存穿透的问题,那么我们先看看什么是缓存穿透?
其实也就是说当我们以Redis作为缓存数据库的时候,如果查询的key不存在,则就会一直检索缓存,当在高并发场景下缓存中(包括本地缓存和Redis缓存)的某一个Key被高并发的访问没有命中,此时回去数据库中访问数据,导致数据库并发的执行大量查询操作,对DB造成巨大的压力
布隆过滤器插入数据 布隆过滤器就是一个二进制数据的集合。当一个数据加入这个集合时:
- 经过K个哈希函数计算该数据,返回K个计算出的hash值
- 这些K个hash值映射到对应的K个二进制的数组下标
- 将K个下标对应的二进制数据改为1。 例如,第一个哈希函数返回a,第二个第三个哈希函数返回b与c,那么:a、b、c对应的二进制就会被改为1.
布隆过滤器的查询过程 布隆过滤器主要做用就是查询一个数据,在不在这个二进制的集合中,查询过程以下:
- 经过K个哈希函数计算该数据,对应计算出的K个hash值
- 经过hash值找到对应的二进制的数组下标
- 判断:若是存在一处位置的二进制数据是0,那么该数据不存在。若是都是1,该数据存在集合中。
布隆过滤器存在的问题 刚才我们谈到缺点的时候说到,布隆过滤器其实查询是有误差,那么我们分析一下为什么?
- 存在一种可能,数据库存储的是小狗,但是此时我们查询的key为dog,经过计算
小狗与dog的hash值一样 - 那么可能查询
dog的时候,会产生误差
应用总结:网页URL的去重,垃圾邮件的判别,集合重复元素的判别,数据库防止查询击穿,使用BloomFilter来减少不存在的行或列的磁盘查找。
4、手写一个布隆过滤器
通过上面是介绍,相信大家对布隆过滤器也有了自己的认识,那么我们可以尝试着自己模拟一下
public class MyBloomFilter {
/**
* 一个长度为10 亿的比特位
*/
private static final int DEFAULT_SIZE = 256 << 22;
/**
* 为了降低错误率,使用加法hash算法,所以定义一个8个元素的质数数组
*/
private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};
/**
* 相当于构建 8 个不同的hash算法
*/
private static HashFunction[] functions = new HashFunction[seeds.length];
/**
* 初始化布隆过滤器的 bitmap
*/
private static BitSet bitset = new BitSet(DEFAULT_SIZE);
/**
* 添加数据
*
* @param value 需要加入的值
*/
public static void add(String value) {
if (value != null) {
for (HashFunction f : functions) {
//计算 hash 值并修改 bitmap 中相应位置为 true
bitset.set(f.hash(value), true);
}
}
}
/**
* 判断相应元素是否存在
* @param value 需要判断的元素
* @return 结果
*/
public static boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (HashFunction f : functions) {
ret = bitset.get(f.hash(value));
//一个 hash 函数返回 false 则跳出循环
if (!ret) {
break;
}
}
return ret;
}
/**
* 测试。。。
*/
public static void main(String[] args) {
for (int i = 0; i < seeds.length; i++) {
functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
}
// 添加1亿数据
for (int i = 0; i < 100000000; i++) {
add(String.valueOf(i));
}
String id = "123456789";
add(id);
System.out.println(contains(id)); // true
System.out.println("" + contains("234567890")); //false
}
}
class HashFunction {
private int size;
private int seed;
public HashFunction(int size, int seed) {
this.size = size;
this.seed = seed;
}
public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
int r = (size - 1) & result;
return (size - 1) & result;
}
}