概念
布隆过滤器主要用于缓存穿透,一般我们会把数据放在Redis里缓存,请求过来先读缓存,有缓存的话直接返回,如果缓存中没有,再去访问数据库查询,然后再把数据库读取的数据放入缓存。但是如果有大量请求过来,而且都在访问一个不在缓存里的数据,这时这些大量请求都会到数据库,对数据库造成很大压力。
可以用很多办法来解决这个问题,如分布式锁、布隆过滤器。布隆过滤器可以缓解缓存穿透问题,为什么说是缓解,而不是解决呢?这是因为布隆过滤器会有一定的误判率。
布隆过滤器(Bloom Filter)本质上是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0。
现在添加一个数据key1,这个数据会通过多个不同的哈希函数计算,并将结果位置上对应位的值置为 “1”,比如Hash1(key1)=5,Hash2(key1)=9,Hash3(key1)=2
这时数据key1就占据了bitmap的2、5、9三个位置,可以看出,布隆过滤器没有存放完整的数据,只是运用一系列随机映射函数计算出位置,然后填充二进制向量。
如果一个数据经过多个映射函数,结果都是1,不一定代表这个数据一定存在,也许其他数据经过映射函数计算的结果也是相同的,也就是说布隆过滤器只能判断数据是否一定不存在,而无法判断数据是否一定存在。布隆过滤器是不能删除数据的,因为他要删除的数据所在bitmap的位置,其他数据有可能也在使用。
- 优点:由于存放的不是完整的数据,所以占用的内存很少,而且新增,查询速度够快;
- 缺点: 随着数据的增加,误判率随之增加;无法做到删除数据;只能判断数据是否一定不存在,而无法判断数据是否一定存在。
实现
布隆过滤器实现有三种:
- client实现bloom算法和bitmap+redis
- client实现bloom算法+redis bitmap
- client+redis bloom.so bitmap
guava实现布隆过滤器
google guava帮助我们实现了如何设计随即映射函数,首先引入pom
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
代码实现
public class Application {
private static int size=1000000;//插入数据
private static double fpp=0.01;//期望的误判率
private static BloomFilter<Integer> bloomFilter=BloomFilter.create(Funnels.integerFunnel(),size,fpp);
public static void main(String[] args) {
//插入数据
for (int i = 0; i < 1000000; i++) {
bloomFilter.put(i);
}
int count=0;
for (int i = 1000000; i <2000000 ; i++) {
if (bloomFilter.mightContain(i)){
count++;
System.out.println(i+"误判了");
}
}
System.out.println("总共的误判书:"+count);
}
}
我们定义了一个布隆过滤器,有两个重要的参数,分别是 我们预计要插入多少数据,我们所期望的误判率,误判率不能为0。
向布隆过滤器插入了0-1000000,然后用1000000-2000000来测试误判率。运行结果:
1999417误判了
1999419误判了
1999501误判了
1999567误判了
1999640误判了
1999697误判了
1999827误判了
1999942误判了
总共的误判书:10314
正常是有100万的数据是不存在的,误判了10314次。误判率是0.010314,与所定义的期望误判率0.01接近
当把误判率改成0.03,运行结果:
1999789误判了
1999822误判了
1999882误判了
1999914误判了
1999956误判了
1999962误判了
1999996误判了
总共的误判书:30155
误判率 fpp 的值越小,匹配的精度越高。当减少误判率 fpp 的值,需要的存储空间也越大,所以在实际使用过程中需要在误判率和存储空间之间做个权衡。
Redis Modules RedisBloom
RedisBloom自己实现映射函数和bitmap
redis.io官网Modules找到RedisBloom,github地址
下载解压编译
unzip master.zip
cd RedisBloom-master/
make
使用redis-server时参数加载 使用绝对路径,也可以配置再配置文件中
redis-server --loadmodule /home/software/RedisBloom-master/redisbloom.so
连接到客户端redis-cli
#BF.RESERVE {key} {error_rate} {capacity} 创建一个空的BF,带着允许的误差率和容量
127.0.0.1:6382> BF.RESERVE k1 0.01 1000000
OK
# 向key的BF里标记存在lisi
127.0.0.1:6382> BF.ADD k1 lisi
(integer) 1
#看lisi是否存在于BF中,1表示存在,0表示不存在
127.0.0.1:6382> BF.EXISTS k1 lisi
(integer) 1
127.0.0.1:6382> BF.EXISTS k1 lisi1
(integer) 0
#BF.INSERT 插入,可以插入多条,选择一些参数,可以认为是RESERVE和ADD的结合体
客户端设计映射函数和bitmap实现布隆过滤器
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.4.2</version>
</dependency>
public class RedisMain {
static final int expectedInsertions = 100;//要插入多少数据
static final double fpp = 0.01;//期望的误判率
//bit数组长度
private static long numBits;
//hash函数数量
private static int numHashFunctions;
static {
numBits = optimalNumOfBits(expectedInsertions, fpp);
numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
}
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.234.128", 6379);
for (int i = 0; i < 100; i++) {
long[] indexs = getIndexs(String.valueOf(i));
for (long index : indexs) {
jedis.setbit("codebear:bloom", index, true);
}
}
for (int i = 100; i < 200; i++) {
long[] indexs = getIndexs(String.valueOf(i));
for (long index : indexs) {
Boolean isContain = jedis.getbit("redis:bloom", index);
if (!isContain) {
System.out.println(i + "肯定没有重复");
}
}
System.out.println(i + "可能重复");
}
}
/**
* 根据key获取bitmap下标
*/
private static long[] getIndexs(String key) {
long hash1 = hash(key);
long hash2 = hash1 >>> 16;
long[] result = new long[numHashFunctions];
for (int i = 0; i < numHashFunctions; i++) {
long combinedHash = hash1 + i * hash2;
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
result[i] = combinedHash % numBits;
}
return result;
}
private static long hash(String key) {
Charset charset = Charset.forName("UTF-8");
return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
}
//计算hash函数个数
private static int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
//计算bit数组长度
private static long optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
}
运行结果
...
198肯定没有重复
198肯定没有重复
198肯定没有重复
198肯定没有重复
198可能重复
199肯定没有重复
199肯定没有重复
199肯定没有重复
199肯定没有重复
199肯定没有重复
199肯定没有重复
199肯定没有重复
199可能重复
布隆过滤器的使用场景
- 过滤大量的数据,比如垃圾邮件过滤,钓鱼网址过滤,判断文章是否给用户推送、判断用户是否已经登陆过等。
- 利用set的不可重复性,会造成很大的空间浪费,布隆过滤器占用空间小,在设置的时候需要有精确度。精确度越高,所需空间越大。但相对来说,节省空间,查重性能高。由于使用的是多个无偏哈希函数,可能会发生不同的元素哈希值一致,故容易误判。若业务可接受误判,可采用布隆过滤器