布隆过滤器介绍
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
Hash面临的问题就是冲突。假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳m / 100个元素。显然这就不叫空间效率了(Space-efficient)了。解决方法也简单,就是使用多个Hash,如果它们有一个说元素不在集合中,那肯定就不在;如果它们都说在,也有可能性是不存在的。
布隆过滤器的优点:
• 时间复杂度低,增加和查询元素的时间复杂为O(N),(N为哈希函数的个数,通常情况比较小)
• 保密性强,布隆过滤器不存储元素本身
• 存储空间小,如果允许存在一定的误判,布隆过滤器是非常节省空间的(相比其他数据结构如Set集合)
布隆过滤器的缺点:
• 有点一定的误判率,但是可以通过调整参数来降低
• 无法获取元素本身
• 很难删除元素
在我们项目中使用布隆过滤器主要是为了解决Redis缓存穿透问题
基本使用
引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.6</version>
</dependency>
初始化布隆过滤器
/**
* 容器启动成功以后,连上数据库,查到所有商品id。在布隆里面进行占位
*/
@Slf4j
@Service
public class SkuIdBloomInitService {
@Autowired
SkuInfoService skuInfoService;
@Autowired
RedissonClient redissonClient;
//TODO 布隆只能增,不能删除商品,如果真的数据库删除了商品,需要定期布隆重建。
//【重建】:按钮触发
/**
* 项目一启动就运行
*/
@PostConstruct //当前组件对象创建成功以后执行
public void initSkuBloom(){
log.info("布隆初始化正在进行....");
//1、查询出所有的skuId
List<Long> skuIds = skuInfoService.findAllSkuId();
//2、把所有的id初始化到布隆过滤器中
RBloomFilter<Object> filter =
redissonClient.getBloomFilter(SysRedisConst.BLOOM_SKUID);
//3、初始化布隆过滤器
//long expectedInsertions, 期望插入的数据量
//double falseProbability 误判率
boolean exists = filter.isExists();
if(!exists){
//尝试初始化。如果布隆过滤器没有初始化过,就尝试初始化
filter.tryInit(5000000,0.00001);
}
//4、把所有的商品添加到布隆中。 不害怕某个微服务把这个事情做失败
for (Long skuId : skuIds) {
filter.add(skuId);
}
log.info("布隆初始化完成....,总计添加了 {} 条数据",skuIds.size());
}
详情业务(布隆过滤器使用)
/**
*
* 切面点表达式怎么写?
* execution(* com.atguigu.gmall.item.**.*(..))
* @param skuId
* @return
*/
public SkuDetailTo getSkuDetailWithCache(Long skuId) {
String cacheKey = SysRedisConst.SKU_INFO_PREFIX +skuId;
//1、先查缓存
SkuDetailTo cacheData = cacheOpsService.getCacheData(cacheKey,SkuDetailTo.class);
//2、判断
if(cacheData == null){
//3、缓存没有
//4、先问布隆,是否有这个商品
boolean contain = cacheOpsService.bloomContains(skuId);
if(!contain){
//5、布隆说没有,一定没有
log.info("[{}]商品 - 布隆判定没有,检测到隐藏的攻击风险....",skuId);
return null;
}
//6、布隆说有,有可能有,就需要回源查数据
boolean lock = cacheOpsService.tryLock(skuId); //为当前商品加自己的分布式锁。100w的49号查询只会放进一个
if(lock){
//7、获取锁成功,查询远程
log.info("[{}]商品 缓存未命中,布隆说有,准备回源.....",skuId);
SkuDetailTo fromRpc = getSkuDetailFromRpc(skuId);
//8、数据放缓存
cacheOpsService.saveData(cacheKey,fromRpc);
//9、解锁
cacheOpsService.unlock(skuId);
}
//9、没获取到锁
try {Thread.sleep(1000);
return cacheOpsService.getCacheData(cacheKey,SkuDetailTo.class);
} catch (InterruptedException e) {
}
}
//4、缓存中有
return cacheData;
}
布隆重建
@Override
public void rebuildBloom(String bloomName, BloomDataQueryService dataQueryService) {
RBloomFilter<Object> oldbloomFilter = redissonClient.getBloomFilter(bloomName);
//1、先准备一个新的布隆过滤器。所有东西都初始化好
String newBloomName = bloomName + "_new";
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(newBloomName);
//2、拿到所有商品id
// List<Long> allSkuId = skuInfoService.findAllSkuId();
List list = dataQueryService.queryData(); //动态决定
//3、初始化新的布隆
bloomFilter.tryInit(5000000,0.00001);
for (Object skuId : list) {
bloomFilter.add(skuId);
}
//4、新布隆准备就绪
// ob bb nb
//5、两个交换;nb 要变成 ob。 大数据量的删除会导致redis卡死
//最极致的做法:lua。 自己尝试写一下这lua脚本
oldbloomFilter.rename("bbbb_bloom"); //老布隆下线
bloomFilter.rename(bloomName); //新布隆上线
//6、删除老布隆,和中间交换层
oldbloomFilter.deleteAsync();
redissonClient.getBloomFilter("bbbb_bloom").deleteAsync();
}
测试
@Scheduled(cron = "0 0 3 ? * 3")
public void rebuild(){
// System.out.println("xxxxxx");
bloomOpsService.rebuildBloom(SysRedisConst.BLOOM_SKUID,bloomDataQueryService);
}