文章目录
如何判断一个元素是不是在一个集合里
一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(哈希表)等等数据结构都是这种思路,但是随着集合中元素的增加,需要的存储空间越来越大;同时检索速度也越来越慢,检索时间复杂度分别是O(n)、O(log n)、O(1)
。
为了解决存储空间和检索速度慢的问题,可以使用布隆过滤器数据结构。
什么是布隆过滤器
BloomFilter
是一种空间效率极高的概率型算法和数据结构,它可以用来判断某个元素是否在集合内,具有运行快速,内存占用小
的特点。
而高效插入和查询的代价就是,BloomFilter
是一个基于概率的数据结构:它只能告诉我们一个元素绝对不在集合内或可能在集合内。
BloomFilter
的基础数据结构是一个很长的二进制向量(可理解为数组,并且只能存放0、1),还有一系列Hash函数计算元素的位置。
Hash函数:SHA1、SHA256、MD5...
布隆过滤器的原理
初始化一个位数组,当一个元素X被加入集合时,通过 M 个散列函数将这个元素映射成一个位数组(Bit array)中的 N 个点,把N个节点设置为 1 。检索时,只要看看这些点是不是都是1就知道元素是否在集合中;如果这些点有任何一个 0,则被检元素一定不在;如果都是1,则被检元素很可能在(之所以说可能
是误差的存在)。因为不同元素通过散列函数得出的节点是有重复的,所以可能存在不存在的元素所有的节点也是1。
布隆过滤器应用的经典场景
- 垃圾邮件地址过滤
- 解决缓存穿透问题
布隆过滤器优势和劣势
- 优势:
全量存储但是不存储元素本身,在某些对保密要求非常严格的场合有优势;
空间高效率;
插入/查询时间都是常数O(k)
,远远超过一般的算法; - 劣势:
存在误算率,随着存入的元素数量增加,误算率随之增加;
一般情况下不能从布隆过滤器中删除元素;
数组长度以及hash函数个数确定过程复杂;
使用GooleGuava实现BloomFilter
BloomFilter流程:
- 首先需要 k 个 hash 函数,每个函数可以把 key 散列成为 1 个整数;
- 初始化时,需要一个长度为 n 比特的数组,每个比特位初始化为 0;
- 某个 key 加入集合时,用 k 个 hash 函数计算出 k 个散列值,并把数组中对应的比特位置为 1;
- 判断某个 key 是否在集合时,用 k 个 hash 函数计算出 k 个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为可能存在集合中。只要有一位是 0 ,认为一定不存在集合中。
BloomFilter实战:
场景描述:
100w字符串放入布隆过滤器,另外随机生成1w字符串,判断他们在100w里面是否存在。
了解误判率对hash函数个数以及bit数组长度的影响。
使用BloomFilter
解决缓存击穿的问题。
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.junit.Test;
import java.util.*;
public class BloomFilterTest {
private static final int insertions = 1000000; // 100w
@Test
public void bfTest() {
// 初始化一个存储string数据的布隆过滤器,初始化大小100w,不能设置为0
BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions, 0.001);
// 初始化一个存储string数据的set,初始化大小100w
Set<String> sets = new HashSet<>(insertions);
// 初始化一个存储string数据的set,初始化大小100w
List<String> lists = new ArrayList<String>(insertions);
// 向三个容器初始化100万个随机并且唯一的字符串---初始化操作
for (int i = 0; i < insertions; i++) {
String uuid = UUID.randomUUID().toString();
bf.put(uuid);
sets.add(uuid);
lists.add(uuid);
}
int wrong = 0;//布隆过滤器错误判断的次数
int right = 0;//布隆过滤器正确判断的次数
for (int i = 0; i < 10000; i++) {
String test = i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString();//按照一定比例选择bf中肯定存在的字符串
if (bf.mightContain(test)) {
if (sets.contains(test)) {
right++;
} else {
wrong++;
}
}
}
System.out.println("=================right=====================" + right);//100
System.out.println("=================wrong=====================" + wrong);//10
}
}
布隆过滤器落地场景是:优化关联查询
优化背景: 查询订单需要关联预警订单数据,由于每查询一笔预警数据就要查询一次预警表,效率低,即是判断该订单是否预警。
解决方案: 可以先将预警的订单放到布隆过滤器中存放一份,则查询订单的时候可以用于关联。
应用该场景的原因: 大部分订单还是正常的,所以没必要每次去关联预警表。
先去布隆过滤器查询该订单是否存在,不存在则直接返回正常,存在则去预警表查询,允许一定的误差率。
为什么预警的订单在布隆过滤器中存放一份也需要去预警表查询?
因为判断了存在的数据可能会有一定的误差率,在布隆过滤器中存在,不一定在预警表是存在的。
使用BitSet实现一个简单的布隆过滤器
import java.util.Arrays;
import java.util.BitSet;
public class MyBloomFilter {
// 布隆过滤器容量
private static final int DEFAULT_SIZE = 2 << 28;
// bit数组,用来存放结果
private static BitSet bitSet = new BitSet(DEFAULT_SIZE);
// 后面hash函数会用到,用来生成不同的hash值,可随意设置
private static final int[] hashInts = {1, 6, 16, 38, 58, 68};
// add方法,计算出key的hash值,并将对应下标置为true
public void add(Object key) {
Arrays.stream(hashInts).forEach(i -> bitSet.set(hash(key, i)));
}
// 判断key是否存在,true不一定说明key存在,但是false一定说明不存在
public boolean isContain(Object key) {
boolean result = true;
for (int i : hashInts) {
// 短路与,只要有一个bit位为false,则返回false
result = result && bitSet.get(hash(key, i));
}
return result;
}
// hash函数,借鉴了hashmap的扰动算法
private int hash(Object key, int i) {
int h;
return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16)));
}
public static void main(String[] args) {
MyBloomFilter myNewBloomFilter = new MyBloomFilter();
myNewBloomFilter.add("James");
myNewBloomFilter.add("Allen");
myNewBloomFilter.add("Kobe");
System.out.println(myNewBloomFilter.isContain("James"));//true
System.out.println(myNewBloomFilter.isContain("James "));//false
System.out.println(myNewBloomFilter.isContain("James1"));//false
System.out.println(myNewBloomFilter.isContain("Allen"));//true
System.out.println(myNewBloomFilter.isContain("Kobe123"));//false
}
}