redis-分布式布隆过滤器(Bloom Filter)详解(初版)
1 布隆过滤器(Bloom Filter)原理以及应用
假设现在有50亿个电话号码,现在有1万个电话号码,需要快速判断这些电话号码是否已经存在?
现在有3中途径
- 1 通过数据库查询,但是不能快速查询。
- 2 把电话号码预先放在一个集合中,如果用long类型存储的话,50亿 * 8字节 = 大于需要40GB(内存浪费或者严重不够)
- 3 使用redis的hyperloglog,但是准确度不高。
类似的问题:
- 垃圾邮件过滤
- 文字处理中的错误单词检测
- 网络爬虫重复URL检测
- 会员抽奖
- 判断一个元素在亿级数据中是否存在
- 缓存穿透
而布隆过滤器则可以解决上述问题
1 什么是布隆过滤器
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位阵列(Bit array)中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:
如果这些点有任何一个 0,则被检索元素一定不在; 如果都是 1,则被检索元素很可能在。
-
添加元素的原理
1 将要添加的元素给k个hash函数
2 得到对应于位数组上的k个位置
3 将这k个位置设置成 1
-
查询元素原理 1 将要查询的元素给k个hash函数
2 得到对应数组的k个元素
3 如果k个位置中有一个为0,则肯定不在集合中
4.如果k个位置全部为1,则有可能在集合中
优点
它的优点是空间效率和查询时间都远远超过一般的算法,布隆过滤器存储空间和插入 / 查询时间都是常数O(k)。另外, 散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
缺点
-
随着数据的增加,误判率随之增加;;只能判断数据是否一定不存在,而无法判断数据是否一定存在。
如果数据A,经过hash1(A)、hash2(A)、hash3(A),得到其hash值1、3、5,然后我们在其二进制向量位置1、3、5设置1,然后数据B,经过hash1(B)、hash2(B)、hash3(B),其实hash值也是1、3、5,我们在做业务处理的时候判断B是否存在的时候发现 其二进制向量位置返回1,认为其已经存在,就跳过相关业务处理,实际上根本不存在,这就是由于hash碰撞引起的问题。也就存在了误差率。
-
无法做到删除数据
一般情况下不能从布隆过滤器中删除元素. 我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加 1, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。
3 redis实现布隆过滤器的三种方式
引入guava pom.xml
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
复制代码
基于Mur3Mur3 hash算法(低碰撞,高性能)
1 guava单机版实现布隆过滤器
package jedis.bloomFilter;
import com.google.common.hash.Funnels;
import com.google.common.hash.BloomFilter;
import java.util.ArrayList;
import java.util.List;
public class GuavaBloomFilter {
private static int size = 10000;
public static void main(String[] args) {
/**
* 默认误差率3%。肯定不存在以及可能存在
* 可通过构造函数去设置误差率
* create(
* Funnel<? super T> funnel, int expectedInsertions, double fpp)
*
*/
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size);
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
for (int i = 0; i < size; i++) {
if (!bloomFilter.mightContain(i)) {
System.out.println("有人逃脱了");
}
}
List<Integer> list = new ArrayList<Integer>(1000);
for (int i = size + 10000; i < size + 20000; i++) {
if (bloomFilter.mightContain(i)) {
list.add(i);
}
}
System.out.println("误伤的数量:" + list.size());
}
}
复制代码
误伤的数量:320
复制代码
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,0.01);
误伤的数量:100
复制代码
缺点:
1 基于本地缓存,容量受限制 2 多个应用就有多个布隆过滤器,多应用同步复杂。
2 redis分布式布隆过滤器的实现(基于guava的实现)
主要是把guava布隆过滤器的相关源码提取了出来,用于分布式redis布隆过滤器。
package jedis.bloomFilter.bloomFilterGuava;
import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;
//@Configurable
public class BloomFilterHelper<T> {
private int numHashFunctions;//hash循环次数
private int bitSize;//bitsize长度
private Funnel<T> funnel;
/**
* @param funnel
* @param expectedInsertions 期望插入长度
* @param fpp 误差率
*/
public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
Preconditions.checkArgument(funnel != null, "funnel不能为空");
this.funnel = funnel;
bitSize = optimalNumOfBits(expectedInsertions, fpp);
numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
}
public int[] murmurHashOffset(T value) {
int[] offset = new int[numHashFunctions];
long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunctions; i++) {
int nextHash = hash1 + i * hash2;
if (nextHash < 0) {
nextHash = ~nextHash;
}
offset[i - 1] = nextHash % bitSize;
}
return offset;
}
/**
* 计算bit数组长度
*/
private int optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
/**
* 计算hash方法执行次数
*/
private int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
}
复制代码
package jedis.bloomFilter.bloomFilterGuava;
import com.google.common.base.Preconditions;
import redis.clients.jedis.JedisCluster;
//@Component
public class RedisBloomFilter {
private JedisCluster cluster;
public RedisBloomFilter(JedisCluster jedisCluster) {
this.cluster = jedisCluster;
}
/**
* 根据给定的布隆过滤器添加值
*/
public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
cluster.setbit(key, i, true);
}
}
/**
* 根据给定的布隆过滤器判断值是否存在
*/
public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
if (!cluster.getbit(key, i)) {
return false;
}
}
return true;
}
}
复制代码
package jedis.bloomFilter.bloomFilterGuava;
import com.google.common.hash.Funnels;
import redis.clients.jedis.JedisCluster;
import java.nio.charset.Charset;
/**
* 基于guava分布式布隆过滤器
*/
public class Test {
public static void main(String[] args) {
BloomFilterHelper bloomFilterHelper = new BloomFilterHelper<>(Funnels.stringFunnel(Charset.defaultCharset()), 1000, 0.1);
JedisCluster cluster = null;
RedisBloomFilter redisBloomFilter = new RedisBloomFilter( cluster);
int j = 0;
for (int i = 0; i < 100; i++) {
redisBloomFilter.addByBloomFilter(bloomFilterHelper, "bloom", i+"");
}
for (int i = 0; i < 1000; i++) {
boolean result = redisBloomFilter.includeByBloomFilter(bloomFilterHelper, "bloom", i+"");
if (!result) {
j++;
}
}
System.out.println("漏掉了" + j + "个");
}
}
复制代码
3 Rebloom插件方式实现布隆过滤器(redis 4.0 以后)
redis4.0 之后支持插件支持布隆过滤器 git: 开源项目:github.com/RedisBloom/…
(也可参考)RedisBloom的客户端:github.com/RedisBloom/…
- 安装Rebloom插件
1 下载并编译
$ git clone git://github.com/RedisLabsModules/rebloom
$ cd rebloom
$ make
复制代码
- 将Rebloom加载到Redis中,在redis.conf里面添加
loadmodule /path/to/rebloom.so
复制代码
- 命令操作
BF.ADD bloom redis
BF.EXISTS bloom redis
BF.EXISTS bloom nonxist
复制代码
- 命令行加载rebloom插件,并且设定每个bloomfilter key的容量和错误率:
cd /usr/redis-4.0.11
./src/redis-server redis.conf --loadmodule /usr/rebloom/rebloom.so INITIAL_SIZE 1000000 ERROR_RATE 0.0001
# 容量100万, 容错率万分之一
复制代码
- java-lua版操作(java代码不提供了,自己把脚本执行就行)
bloomFilterAdd.lua
local bloomName = KEYS[1]
local value = KEYS[2]
-- bloomFilter
local result_1 = redis.call('BF.ADD', bloomName, value)
return result_1
复制代码
bloomFilterExist.lua
local bloomName = KEYS[1]
local value = KEYS[2]
-- bloomFilter
local result_1 = redis.call('BF.EXISTS', bloomName, value)
return result_1
来源:oschina
链接:https://my.oschina.net/u/3872240/blog/4739934