一、问题场景:
系统后台对于一些热点数据的访问,为保证API的相应速度,普遍的做法就是将数据预热到内存中减少对数据库的访问,而考虑到key的过期问题或者懒加载问题,兜底的处理就是当缓存没有命中时,触发数据库查询并将数据同步至缓存中。
如果请求没有命中缓存,并且请求的数据在数据库中根本不存在,在缓存中就无法保存此类key的value值(即使保存此key把value置为空,但key的边界无限大,也就意味着需要的服务器内存也就无限大,显然不现实),客户端同时间发送大量请求可能会导致数据库负载过大宕掉。
二、解决思路:
最有效的方法就是限制数据边界,对于一些数据库不存在的数据,要把请求挡在repo层之前。如果表中数据量过大,全部进行缓存到redis或者hashmap中也不现实,此时就可以考虑布隆过滤器。
三、布隆过滤器介绍
Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。
优点:空间效率和查询时间都远远超过一般的算法。
缺点:有一定的误识别率,删除困难。
下面我们具体来看Bloom Filter是如何用位数组表示集合的。初始状态时,Bloom Filter是一个包含m位的位数组,每一位都置为0。
为了表达S={x, x,…,x}这样一个n个元素的集合,Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置h(x)就会被置为1(1≤i≤k)。注意,如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同一个位置(从左边数第五位)。
在判断y是否属于这个集合时,我们对y应用k次哈希函数,如果所有h(y)的位置都是1(1≤i≤k),那么我们就认为y是集合中的元素,否则就认为y不是集合中的元素。下图中y就不是集合中的元素。y或者属于这个集合,或者刚好是一个false positive。
四、布隆过滤器使用
1、根据原理手动实现
测试demo如下:
public class MyBloomFilter {
/**
* 一个长度为10 亿的比特位
*/
private static final int DEFAULT_SIZE = 256 << 22;
/**
* 为了降低错误率,使用加法hash算法,所以定义一个8个元素的质数数组
*/
private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};
/**
* 相当于构建 8 个不同的hash算法
*/
private static HashFunction[] functions = new HashFunction[seeds.length];
/**
* 初始化布隆过滤器的 bitmap
*/
private static BitSet bitset = new BitSet(DEFAULT_SIZE);
/**
* 添加数据
*
* @param value 需要加入的值
*/
public static void add(String value) {
if (value != null) {
for (HashFunction f : functions) {
//计算 hash 值并修改 bitmap 中相应位置为 true
bitset.set(f.hash(value), true);
}
}
}
/**
* 判断相应元素是否存在
* @param value 需要判断的元素
* @return 结果
*/
public static boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (HashFunction f : functions) {
ret = bitset.get(f.hash(value));
//一个 hash 函数返回 false 则跳出循环
if (!ret) {
break;
}
}
return ret;
}
/**
* 测试。。。
*/
public static void main(String[] args) {
for (int i = 0; i < seeds.length; i++) {
functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
}
// 添加1亿数据
for (int i = 0; i < 100000000; i++) {
add(String.valueOf(i));
}
String id = "123456789";
add(id);
System.out.println(contains(id)); // true
System.out.println("" + contains("234567890")); //false
}
}
class HashFunction {
private int size;
private int seed;
public HashFunction(int size, int seed) {
this.size = size;
this.seed = seed;
}
public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
int r = (size - 1) & result;
return (size - 1) & result;
}
}
2、基于框架API实现
目前java主流实现框架,有redisson和guava等等
基于redission的测试demo:
@Test
public void testBloomFilter() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient cient = Redisson.create(config);
RBloomFilter<String> bloomFilter = cient.getBloomFilter("test-bloom-filter");
// 初始化布隆过滤器,数组长度100W,误判率 1%
bloomFilter.tryInit(1000000L, 0.01);
// 添加数据
bloomFilter.add("测试数据1");
// 判断是否存在
System.out.println(bloomFilter.contains("测试数据1"));
System.out.println(bloomFilter.contains("测试数据2"));
}
基于guava的测试demo如下:
public static void main(String[] args){
int capacity = 100000;
/** 初始化容量为10万大小的字符串布隆过滤器,默认误差率为0.03
* 布隆过滤器容量为10万并非指bitmap的长度就是10万,因为需要考虑到存在hash冲突的情况,所以bitmap的实际长度要比10万要大很多
* bitmap长度比需要存在的数据量大小越大,误差率会越低
* */
BloomFilter bloomFilter =BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), capacity);
Set<String> sets = new HashSet<>();
List<String> lists = new ArrayList<>();
for (int i =0;i< capacity; i++){
String str = UUID.randomUUID().toString();
bloomFilter.put(str);
sets.add(str);
lists.add(str);
}
int existsCount = 0;
int mightExistsCount = 0;
for(int i=0;i<10000;i++){
//如果i为100倍数,取实际的值;否则就随机一个字符串
String data = i%100==0?lists.get(i/100):UUID.randomUUID().toString();
/** 通过布隆过滤器判断字符串是否存在*/
if(bloomFilter.mightContain(data)){
/** 如果布隆过滤器认为存在,则表示可能存在的数量mightExistsCount自增1*/
mightExistsCount++;
/** 如果set中存在则existsCount自增1*/
if(sets.contains(data)){
existsCount++;
}
}
}
//测试总次数
BigDecimal total = new BigDecimal(10000);
//错误总次数
BigDecimal error = new BigDecimal(mightExistsCount - existsCount);
//误差率
BigDecimal rate = error.divide(total, 2, BigDecimal.ROUND_HALF_UP);
System.out.println("初始化10万条数据,判断100个真实数据,9900个不存在数据");
System.out.println("实际存在的字符串个数为:" + existsCount);
System.out.println("布隆过滤器认为存在的个数为:" + mightExistsCount);
System.out.println("误差率为:" + rate.doubleValue());
}