目录
场景说明:
通常情况下,缓存是加速系统响应的一种途径,一般只有系统的部分数据。当请求了缓存中没有的数据时,这时候就会回源到DB里面。此时如果黑客故意对上面数据发起大量请求,则DB有可能会挂掉,这就是缓存击穿。当然缓存挂掉的话,正常的用户请求也有可能造成缓存击穿的效果
1. 什么是布隆过滤器
布隆过滤器,Bloom Filter是1970年由Bloom提出的,它是由一组哈希(Hash)函数和一个位阵列组成。布隆过滤器可以用于查询一个元素是否存在于一个集合当中,查询结果为以下二者之一:
这个元素可能存在于这个集合当中。
这个元素一定不存在于这个集合当中。
布隆过滤器的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
布隆过滤器在实际中主要用来解决网页URL去重复,垃圾邮件检测,大集合中重复元素判断和缓存击穿等问题。
2.设计原理
如果想判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路. 但是随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也越来越慢(O(n),O(logn))。
这时候我们可以利用哈希表这种数据结构,基于哈希函数的特性,它在理想情况下(不发生哈希冲突),检索速度可以达到O(1)。一张哈希表的示意图如下所示:
哈希函数是这样的:
hashcode=H(key)hashcode=H(key)
这里的hashcode就是哈希桶的索引,如何哈希函数H足够完美,那么每个key就会对应一个唯一的hashcode,但实际上往往会出现哈希冲突,即两个不同的key对应同一个hashcode,如下图所示,“John Smith” 和 “Sandra Dee” 经过哈希之后得到了相同的哈希值“02”。
布隆过滤器使用了上面的思路,即利用哈希表这个数据结构,通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点(每个点只能表示0或者1),这样一来,我们只要看看这个点是不是1就知道在集合中有没有它了。这就是Bloom Filter的基本思想。
但是在哈希冲突的情况下,我们无法使用一个哈希函数来判断一个元素是否存在于集合之中,解决方法也简单,就是使用多个哈希函数,如果其中有一个哈希函数判断该元素不在集合中(元素经过Hash之后映射在位阵列中的点为0),那肯定就不在。如果它们都判断存在,那也有一定可能性它们都在说谎,不过这要比只用一个哈希函数来判断“一个元素存在于集合之中”的可靠性要高很多。这种多个Hash组成的数据结构就叫Bloom Filter。
一个Bloom Filter是基于一个m位的位阵列(b1,…bm),这些位阵列的初始值为0。另外,还有一系列的hash函数(h1,…hk),这些hash函数的值域属于1~m。下图是一个bloom filter插入x,y,z并判断某个值w是否在该数据集的示意图:
上图中,m=18,k=3;
插入x时,三个hash函数分别得到蓝线对应的三个值,并将对应的位向量改为1,插入y,z时,类似的,分别将红线,紫线对应的位向量改为1。
查找时,当查找x时,三个hash值对应的位向量都为1,因此判断x在此数据集中。y,z也是如此。但是当查找w时,w有个hash值对应的位向量为0,因此可以判断不在此集合中。但是,假如w的最后那个hash值比上图中的大1,这是就会认为w在此集合中,而事实上,w可能不在此集合中,因此可能出现误报。显然的,插入数据越多,1的位数越多,误报的概率越大。
产生误报的原因是由于哈希碰撞导致的巧合而将不同的元素存储在相同的比特位上。幸运的是,布隆过滤器有一个可预测的误判率(FPP):
n 是已经添加元素的数量;
k 哈希的次数;
m 布隆过滤器的长度(如比特数组的大小);
极端情况下,当布隆过滤器没有空闲空间时(满),每一次查询都会返回 true 。这也就意味着 m 的选择取决于期望预计添加元素的数量 n ,并且 m 需要远远大于 n 。
实际情况中,布隆过滤器的长度 m 可以根据给定的误判率(FFP)的和期望添加的元素个数 n 的通过如下公式计算:
对于 m/n 比率表示每一个元素需要分配的比特位的数量,也就是哈希函数 k 的数量可以调整误判率。通过如下公式来选择最佳的 k 可以减少误判率(FPP):
了解完上述的内容之后,我们可以得出一个结论,当我们搜索一个值的时候,若该值经过 K 个哈希函数运算后的任何一个索引位为 ”0“,那么该值肯定不在集合中。但如果所有哈希索引值均为 ”1“,则只能说该搜索的值可能存在集合中
3. 布隆过滤器应用
在实际工作中,布隆过滤器常见的应用场景如下:
1.网页爬虫对 URL 去重,避免爬取相同的 URL 地址;
2.反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
3.Google Chrome 使用布隆过滤器识别恶意 URL;
4.Medium 使用布隆过滤器避免推荐给用户已经读过的文章;
5.Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆过滤器减少对不存在的行和列的查找。
除了上述的应用场景之外,布隆过滤器还有一个应用场景就是解决缓存穿透的问题。所谓的缓存穿透就是服务调用方每次都是查询不在缓存中的数据,这样每次服务调用都会到数据库中进行查询,如果这类请求比较多的话,就会导致数据库压力增大,这样缓存就失去了意义。
利用布隆过滤器我们可以预先把数据查询的主键,比如用户 ID 或文章 ID 缓存到过滤器中。当根据 ID 进行数据查询的时候,我们先判断该 ID 是否存在,若存在的话,则进行下一步处理。若不存在的话,直接返回,这样就不会触发后续的数据库查询。需要注意的是缓存穿透不能完全解决,我们只能将其控制在一个可以容忍的范围内。
4.代码实践
4.1 引入依赖
4.1.1 redisson方式
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson-version}</version>
</dependency>
4.1.2 添加redisson配置类
package com.huachun.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
public class RedissonManager {
@Value("${spring.redis.host}")
private String address;
@Value("${spring.redis.password}")
private String password;
@Bean(name = "myRedisson", destroyMethod = "shutdown")
public RedissonClient getRedisson() throws Exception {
Config config = new Config();
config.useSingleServer().setAddress(address).setPassword(password);
return Redisson.create(config);
}
}
4.1.3 业务代码
package com.huachun.service.impl;
import com.alicp.jetcache.Cache;
import com.alicp.jetcache.anno.CacheType;
import com.alicp.jetcache.anno.CreateCache;
import com.huachun.dao.HcTestDao;
import com.huachun.model.HcTest;
import com.huachun.service.JetCacheService;
import com.huachun.utils.RlockUtils;
import com.huachun.utils.SnowFlakeGenerateIdWorker;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class JetCacheServiceImpl implements JetCacheService {
/**
* 预计插入的数据
*/
private static Integer expectedInsertions = 10000;
/**
* 误判率
*/
private static Double fpp = 0.01;
@Autowired
private HcTestDao hcTestDao;
@Autowired
private RlockUtils rlockUtils;
@CreateCache(expire = 100, name = "JetCacheService-", localExpire = 100, cacheType = CacheType.BOTH)
private Cache<String, HcTest> cache;
@Resource(description = "myRedisson")
private RedissonClient redissonClient;
@Override
public HcTest getData(String str) {
String logBy = this.getClass().getName() + ".getCache:{}";
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("JetCacheService");
if (!bloomFilter.contains(str)) {
return null;
}
RLock rLock = redissonClient.getLock("jetcache-lock");
HcTest hcTest = cache.get(str);
try {
if (rLock.tryLock(0, 10, TimeUnit.SECONDS) && null == hcTest) {
hcTest = hcTestDao.queryById(str);
hcTest.setId(str);
cache.put(str, hcTest);
}
} catch (Exception exception) {
log.error(logBy, exception);
} finally {
rlockUtils.unLock(rLock);
}
return hcTest;
}
@Override
public void addData(HcTest hcTest) {
String logBy = this.getClass().getName() + ".addData:{}";
try {
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("JetCacheService");
bloomFilter.tryInit(expectedInsertions, fpp);
SnowFlakeGenerateIdWorker snowFlakeGenerateIdWorker = new SnowFlakeGenerateIdWorker(1, 3);
String id = snowFlakeGenerateIdWorker.generateNextId();
bloomFilter.add(id);
hcTest.setId(id);
hcTestDao.insert(hcTest);
} catch (Exception exception) {
log.error(logBy, exception);
}
}
}
4.1.4 测试
1.新增两条数据
存储到表中数据如下
第一条数据可以正常获取
第二条也可以正常获取
输入一个没有插入数据的id进行查询,发现已经被过滤了,也可以打断点测试,这里就不做过多的展示
5.缓存同步
5.1 手动方式
package com.huachun.service.impl;
import com.alicp.jetcache.Cache;
import com.alicp.jetcache.anno.*;
import com.huachun.dao.HcTestDao;
import com.huachun.model.HcTest;
import com.huachun.service.JetCacheService;
import com.huachun.utils.RlockUtils;
import com.huachun.utils.SnowFlakeGenerateIdWorker;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class JetCacheServiceImpl implements JetCacheService {
/**
* 预计插入的数据
*/
private static Integer expectedInsertions = 10000;
/**
* 误判率
*/
private static Double fpp = 0.01;
@Autowired
private HcTestDao hcTestDao;
@Autowired
private RlockUtils rlockUtils;
@CreateCache(expire = 100, name = "JetCacheService-", localExpire = 100, cacheType = CacheType.BOTH)
private Cache<String, HcTest> cache;
@Resource(description = "myRedisson")
private RedissonClient redissonClient;
@Override
public HcTest getData(String str) {
String logBy = this.getClass().getName() + ".getCache:{}";
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("JetCacheService");
if (!bloomFilter.contains(str)) {
return null;
}
RLock rLock = redissonClient.getLock("jetcache-lock");
HcTest hcTest = cache.get(str);
rLock.lock(10, TimeUnit.SECONDS);
try {
if (rLock.tryLock(0, 10, TimeUnit.SECONDS) && null == hcTest) {
hcTest = hcTestDao.queryById(str);
if (null == hcTest) {
hcTest = new HcTest();
hcTest.setId(str);
cache.put(str, hcTest);
}
}
} catch (Exception exception) {
log.error(logBy, exception);
} finally {
rlockUtils.unLock(rLock);
}
return hcTest;
}
@Override
public void addData(HcTest hcTest) {
String logBy = this.getClass().getName() + ".addData:{}";
try {
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("JetCacheService");
bloomFilter.tryInit(expectedInsertions, fpp);
SnowFlakeGenerateIdWorker snowFlakeGenerateIdWorker = new SnowFlakeGenerateIdWorker(1, 3);
String id = snowFlakeGenerateIdWorker.generateNextId();
bloomFilter.add(id);
hcTest.setId(id);
hcTestDao.insert(hcTest);
} catch (Exception exception) {
log.error(logBy, exception);
}
}
@Override
public void updateData(HcTest hcTest) {
cache.put(hcTest.getId(), hcTest);
hcTestDao.update(hcTest);
}
@Override
public void removeData(String str) {
cache.remove(str);
hcTestDao.deleteById(str);
}
}
// 创建缓存
@CreateCache(expire = 100, name = "JetCacheService-", localExpire = 100, cacheType = CacheType.BOTH)
private Cache<String, HcTest> cache;
// 添加缓存
cache.put(str, hcTest);
// 缓存更新
cache.put(hcTest.getId(), hcTest);
// 缓存删除
cache.remove(str);
同步测试正常
5.2 注解方式
@Autowired
private HcTestDao hcTestDao;
@Cached(name = "JetCacheServiceAnno.", key = "#str")
@Override
public HcTest getData(String str) {
return hcTestDao.queryById(str);
}
@Override
public void addData(HcTest hcTest) {
hcTestDao.insert(hcTest);
}
@CacheUpdate(name = "JetCacheServiceAnno.", value = "#hcTest", key = "#hcTest.id")
@Override
public void updateData(HcTest hcTest) {
hcTestDao.update(hcTest);
}
@CacheInvalidate(name = "JetCacheServiceAnno.", key = "#str")
@Override
public void removeData(String str) {
hcTestDao.deleteById(str);
}
注解方式测试正常
6.总结
Bloom Filter有以下几个特点:
1.不存在漏报(False Negative),即某个元素在某个集合中,肯定能报出来。
2.可能存在误报(False Positive),即某个元素不在某个集合中,可能也被爆出来。
3.确定某个元素是否在某个集合中的代价和总的元素数目无关。
优点:
相比于其它的数据结构,Bloom Filter在空间和时间方面都有巨大的优势。Bloom Filter存储空间和插入/查询时间都是常数。另外, Hash函数相互之间没有关系,方便由硬件并行实现。Bloom Filter不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
缺点:
一般情况下不能从Bloom Filter中删除元素. 我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在Bloom Filter里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
原文链接:https://blog.csdn.net/waitingbb123/article/details/107119561
原文链接:https://blog.csdn.net/fouy_yun/article/details/81075432