引子
最近深入学习Redis中,刚好在看到使用布隆过滤器来解决缓存穿透问题,觉得挺好用的,所以记录分享下。
布隆过滤器介绍
布隆过滤器是一个占用空间小,效率很高的概率型数据结构。由一个很长的二进制向量和一系列随机映射函数构成。专门用来检测集合中是否存在特定的元素。优点是空间和时间都远远超过一般算法,缺点是有一定的误判率。
产生契机
回想一下,平时我们在检测集合中是否存在某元素时,都会采用比较的算法。以下情况:
- 使用线性表存储,查找时间复杂度为O(n)
- 用平衡树(如AVL树,红黑树)存储,时间复杂度为O(logn)
- 使用哈希表存储,并用链地址法与平衡BST来解决哈希冲突(JDK8中HashMap实现方法)
总而言之,当集合中元素的数据极多时,不仅查找变得很慢,而且在空间上也会占用到无法想象的地步。恰好布隆就是解决这个矛盾的利器。
原理
布隆是由一个长度为m比特的位数组(BitArray) 和 k个哈希函数(num Hash Functions)组成的数据结构。位数组初始化为0。 当要插入一个元素时,将其数据分别输入k个哈希函数,产生k个哈希值。以哈希值为位数组中的下标,将所有k个对应的比特置为1。 当要查询(判断是否存在)一个元素时,同样将其数据输入哈希函数,然后检查对应k个比特。如果有任意一个比特为0,表明该元素一定不在集合中。如果所有比特都为1,表明该集合有很大的可能性在集合中。为什么是很大可能性呢?因为一个比特位被置为1可能受到其他元素的影响。如图所示:
简单来说:
- 布隆过滤器说某个元素在,可能会被误判
- 布隆过滤器说某个元素不在,那么就一定不在
优缺点&用途
优点:
- 不需要存储数据,只用比特来表示,相对于其他传统占用空间有巨大优势,并且还保密数据
- 时间效率也很高
- 哈希函数互相独立,可以在硬件指令层面并行计算
缺点:
- 存在误判率,不适用于任何要求百分百准确率的情景
- 只能插入和查询元素,不能删除元素。原因和产生误判的原因是相同的。
所以,布隆在查询准确度上没有那么严格,而对时间、空间效率要求较高的场景非常适合。比如用在缓存系统上,防止缓存穿透。
布隆过滤器在Guava中实现
Guava中,布隆过滤器的实现主要涉及两个类,BloomFilter 和 BloomFilterStrategies,首先看下BloomFilter的成员变量。这里用到的是Guava的18.0版本。
/** guava实现的设置每个bit位的bit数组 */
private final BitArray bits;
/** hash函数的个数 */
private final int numHashFunctions;
/** guava中将对象转换为byte的通道 */
private final Funnel<? super T> funnel;
/**
* 将byte转换为n个bit的策略,也是bloomfilter hash映射的具体实现
*/
private final Strategy strategy;- BitArray 是定义在BloomFilterStrategies中的内部类,封装了布隆过滤器底层bit数组的操作。
- numHashFunctions表示哈希函数的个数。
- Funnel,它和PrimitiveSink配套使用,能将任意类型的对象转化成Java基本数据类型,默认用java.nio.ByteBuffer实现,最终均转化为byte数组。
- Strategy是定义在BloomFilter类内部的接口,代码如下,主要有2个方法,put和mightContain
创建布隆过滤器,BloomFilter并没有公有的构造函数,只有一个私有构造函数,对外提供两个create方法,缺省情况下误判率为3%,采用BloomFilterStrategies.MURMUR128_MITZ_64的实现。
BloomFilterStrategies.MURMUR128_MITZ_64是Strategy两个实现之一,Guava以枚举方式提供这两个实现。
二者对应了32位哈希映射函数,和64位哈希映射函数,后者使用Hashing.murmur3_128() 生成的128位,有更大的空间,不过原理都是想通的。
看下它的put方法,用两个hash函数来模拟多个hash函数的情况,这是布隆过滤器的一种优化。
public <T> boolean put(T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
long bitSize = bits.bitSize();
//murmur3_128对输入的funnel计算得到128位的哈希值,funnel将object转换为byte数组,
byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
//高低位hash
long hash1 = lowerEight(bytes);
long hash2 = upperEight(bytes);
boolean bitsChanged = false;
long combinedHash = hash1;
//进行k次循环,每次循环都用hash1与hash2的复合哈希做散列,然后对m取模,将位数组中的对应比特设为1。
for (int i = 0; i < numHashFunctions; i++) {
// Make the combined hash positive and indexable
bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
combinedHash += hash2; } return bitsChanged;}这里注意两点:
- 在循环中实际上应用了双重哈希(double hashing)的思想,即可以用两个哈希函数来模拟k个,其中i为步长:
这种方法在开放定址的哈希表中,也经常用来减少冲突。
哈希值有可能为负数,而负数是不能在位数组中定位的。所以哈希值需要与Long.MAX_VALUE做bitwise AND,直接将其最高位(符号位)置为0,就变成正数了。
在put方法中,先是将索引位置上的二进制置为1,然后用bitsChanged记录插入结果,如果返回true表明没有重复插入成功,而mightContain方法则是将索引位置上的数值取出,并判断是否为0,只要其中出现一个0,那么立即判断为不存在。
public <T> boolean mightContain(T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
long bitSize = bits.bitSize();
byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
long hash1 = lowerEight(bytes);
long hash2 = upperEight(bytes);
long combinedHash = hash1; for (int i = 0; i < numHashFunctions; i++) {
// Make the combined hash positive and indexable
//不同之处,set转换为get,来判断是否存在
if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) { return false; }
combinedHash += hash2; } return true;}总结
本文讲解了布隆过滤器产生,设计原理和场景分析。另外也简单阅读Guava中BloomFilter的相关源码,了解布隆过滤器技术要点。辛苦大家。。缓解眼疲劳