目录
算法背景
问题:
在开发中,经常要判断一个元素是否在一个集合中。
实现方案:
编程中通常使用集合来存储所有元素,然后通过hash值来确定元素是否存在。
如:java中的HashMap、HashSet等。
优点:快速准确
缺点:耗费存储空间
瓶颈:
当集合比较小时,这个问题不明显
当集合比较大时,散列表存储效率低的问题越明显
如:判断邮件地址是否是发送垃圾邮件的地址
采用散列表:将每一个Email地址对应成一个8字节的信息指纹,然后存入散列表,由于散列表的存储效率一般只有50%,因此一个Email地址需要16个字节。一亿Email约1.6GB内存,存储几十亿个地址约上百GB的内存。
布隆过滤器–概念
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询效率都远远超过一般的算法,缺点是有一定的误判率和删除困难。
布隆过滤器—原理
布隆过滤器的原理是:
当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
布隆过滤器—缺点
bloom filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性。
存在误判。可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,可以通过建立一个白名单来存储可能会误判的元素。
删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其它元素的判断。可以采用Counting Bloom Filter(计数布隆过滤器),将标准Bloom Filter位数组的每一位扩展为一个小的计数器(Counter)。
布隆过滤器—实现
在使用bloom filter时,绕不过的两点:
1)预估数据量n
2)期望的误判率fpp
在实现bloom filter时,绕不过的两点:
1)hash函数的选取
2)bit数组的大小
对于一个确定的场景,我们预估要存的数据量为n,期望的误判率为fpp,然后需要计算我们需要的Bit数组的大小m,以及hash函数的个数k,并选择hash函数。
(1)Bit数组大小选择
根据预估数据量n以及误判率fpp,bit数组大小的m的计算方式:
(2)哈希函数选择
由预估数据量n以及bit数组长度m,可以得到一个hash函数的个数k:
哈希函数的选择对性能的影响是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。
下面使用java来实现布隆过滤器:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.BitSet;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
/**
* 布隆过滤器
*
* @author Administrator
*
*/
public class BloomFilterDemo implements Serializable {
private static final long serialVersionUID = -5221305273707291280L;
private int dataCount; // 预期数要存的数据量
private double falsePositive; // 期望的误判率
private BitSet bits; // 位数组,使用BitSet实现
private int bitSize; // 位数组大小
private int hashFunctionCount; // 散列函数的个数
public BloomFilterDemo(int dataCount, double falsePositive) {
super();
this.dataCount = dataCount;
this.falsePositive = falsePositive;
this.init();
}
/**
* 初始化
*/
private void init() {
this.bitSize = (int) this.getNumOfBits(this.dataCount, this.falsePositive);
this.hashFunctionCount = this.getNumOfHashFunctions(this.dataCount, this.bitSize);
this.bits = new BitSet(this.bitSize);
}
/**
* 往布隆过滤器添加数据标记
* 如果不存在就进行记录并返回false,如果存在了就返回true
*
* @param data
* @return
* @throws NoSuchAlgorithmException
*/
public boolean add(String data) throws NoSuchAlgorithmException {
int[] indexs = new int[this.hashFunctionCount];
// 先假定存在
boolean exist = true;
int index;
for (int i = 0; i < this.hashFunctionCount; i++) {
indexs[i] = index = hash(data, i);
if (exist) {
if (!this.bits.get(index)) {
// 只要有一个不存在,就可以认为整个字符串都是第一次出现的
exist = false;
// 补充之前的信息
for (int j = 0; j <= i; j++) {
//将对应的位设置为true
this.bits.set(indexs[j], true);
}
}
} else {
//将对应的位设置为true
this.bits.set(index, true);
}
}
return exist;
}
/**
* 检查数据是否存在
*
* @param data
* @return
* @throws NoSuchAlgorithmException
*/
public boolean check(String data) throws NoSuchAlgorithmException {
for (int i = 0; i < this.hashFunctionCount; i++) {
int index = hash(data, i);
if (!this.bits.get(index)) {
return false;
}
}
return true;
}
/**
* md5实现hash--目前测试,该hash算法的效果比较好
* @param message
* @param funNum
* @return
* @throws NoSuchAlgorithmException
*/
private int hash(String message, int funNum) throws NoSuchAlgorithmException{
MessageDigest md5 = MessageDigest.getInstance("md5");
message = message + String.valueOf(funNum);
byte[] bytes = message.getBytes();
md5.update(bytes);
BigInteger bi = new BigInteger(md5.digest());
return Math.abs(bi.intValue()) % this.bitSize;
}
/**
* 获取Bit数组的大小
*
* @param n
* 预估要存的数据量
* @param p
* 期望的误判率
* @return
*/
private long getNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
// -1 * (n * log(p)) / (log(2) * log(2))
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
/**
* 获取hash函数的数量
*
* @param n
* 预估要存的数据量
* @param m
* Bit数组的大小m
* @return
*/
private int getNumOfHashFunctions(long n, long m) {
// (m / n) * log(2)
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
/**
* 持久化布隆过滤器对象
*
* @param path
*/
public void saveFilterToFile(String path) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path))) {
oos.writeObject(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 读取布隆过滤器对象
*
* @param path
*/
public static BloomFilterDemo readFilterFromFile(String path) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path))) {
return (BloomFilterDemo) ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
String file = "d:\\bloomFilter.obj";
try {
createFilter(file);
readFilter(file);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
public static void createFilter(String file) throws NoSuchAlgorithmException {
int count = 1000 * 10000;
double fpp = 0.00001;
System.out.println("start --- " + new Date());
BloomFilterDemo bloomFilter = new BloomFilterDemo(count, fpp);
int i = 0;
while (i < count) {
String msg = "时间:2018-10-01 10:00:00, 源IP:10.1.1.12,目标IP:192.1.1.205, 攻击类型:ddos攻击 -- " + i;
bloomFilter.add(msg);
i++;
}
bloomFilter.saveFilterToFile(file);
System.out.println("end --- " + new Date());
}
public static void readFilter(String file) throws NoSuchAlgorithmException {
BloomFilterDemo bloomFilter = readFilterFromFile(file);
System.out.println("bitSize: " + bloomFilter.bitSize);
System.out.println("hashFunctionCount: " + bloomFilter.hashFunctionCount);
System.out.println("start --- " + System.currentTimeMillis());
int existCount = 0;
for (int i = 0, size = 10 * 10000; i < size; i++) {
String msg = "时间:2018-10-01 10:00:00, 源IP:10.1.1.12,目标IP:192.1.1.205, 攻击类型:ddos攻击 -- " + i;
msg += System.currentTimeMillis();
if (bloomFilter.check(msg)) {
existCount++;
}
}
System.out.println("end --- " + System.currentTimeMillis());
System.out.println(existCount);
}
}
布隆过滤器—应用
常见的几个应用场景:
(1)网页爬虫对URL的去重,避免爬取相同URL地址
(2)反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)
(3)缓存击穿,将已存在的缓存放到布隆中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉
布隆过滤器—公式推导
预估要存的数据量为:n
期望的误判率为:P
Bit数组的大小为:m
Hash函数的个数为:k
推导过程:
1)对某一特定bit位在一个元素由某特定hash函数插入时没有被置为1的概率为:
2)则k个hash函数都没有将其置为1概率为:
3)如果插入了n个元素,都未将其置为1的概率为:
4)反过来,则此位被置为1的概率为:
5)一个不在集合中的元素,被误判在集合中的概率:
6)根据自然常数公式: lim(1+1/x)^x, x→∞,得出:
7)k为何值时可以使得误判率最低。设误判率为k的函数:
8)设:
9)则简化为:
10)两边取对数:
11)两边对k求导:
12)下面求最值:
则误判率最低时,得出k值:
13)把k代入误判率公式,得出:
14)把k代入误判率公式,得出m值: