什么是布隆过滤器?
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
如果要判断一个元素是否存在,以前的思路是遍历集合,将元素依次比较。链表、数组等数据结构都是这种思路,只是不同的数据结构判断的性能不同,时间复杂度、空间复杂度不同。
这些数据结构都必须存储原数据,随着元素的增多,需要的存储空间越来越大,检索的速度也越来越慢。
布隆过滤器的思想是:创建一个超长的位阵列(Bit Array),默认所有bit位都为0,当有元素添加时,将元素通过不同的哈希算法获得一组下标,将位阵列中对应的下标改为1。判断元素是否存在时,只需要判断对应的bit位是否为1即可。
布隆过滤器的优点:
- 不存储元素,空间占用小
- 性能很高
布隆过滤器的缺点:
- 不支持删除
- 存在一定的容错
位阵列足够大时,容错会很小,布隆过滤器可以有效的过滤掉绝大部分数据。
什么是缓存穿透?
使用Redis作为关系型数据库的缓存时,一般的逻辑是:查询时先查询Redis缓存,缓存中不存在时,再去数据库中查询。
使用缓存可以减轻数据库的压力,但是存在“缓存穿透”问题。
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。
对于一个“一定不存在的数据”,就不要去访问数据库了,避免给数据库带来不必要的压力。
可能出现的原因
- 业务代码自身存在问题
- 网络爬虫
- 恶意攻击
布隆过滤器可以帮助我们过滤掉“一定不存在的数据”,对于这些请求,我们可以直接过滤掉,避免访问数据库。
解决方案
- 缓存空对象(简单)
- 布隆过滤器
布隆过滤器实现
这里基于Redis提供的BitMaps来实现,BitMaps可以直接对二进制数据进行bit位操作,刚好符合我们的需求。
RedisBloomFilter
@Component
public class RedisBloomFilter {
//预计数据总量
private long size = 1000000;
//容错率
private double fpp = 0.01;
//二进制向量大小
private long numBits;
//哈希算法数量
private int numHashFunctions;
//redis中的key
private final String key = "goods_filter";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private GoodsMapper goodsMapper;
@PostConstruct
private void init(){
numBits = optimalNumOfBits();
numHashFunctions = optimalNumOfHashFunctions();
//数据重建
List<Goods> goods = goodsMapper.selectList(null);
for (Goods good : goods) {
put(String.valueOf(good.getId()));
}
}
//向布隆过滤器中put
public void put(String id){
long[] indexs = getIndexs(id);
//将对应下标改为1
for (long index : indexs) {
redisTemplate.opsForValue().setBit(key, index, true);
}
}
//判断id是否可能存在
public boolean isExist(String id){
long[] indexs = getIndexs(id);
//只要有一个bit位为1就表示可能存在
for (long index : indexs) {
if (redisTemplate.opsForValue().getBit(key, index)) {
return true;
}
}
return false;
}
//根据key获取bitmap下标(算法借鉴)
private long[] getIndexs(String key) {
long hash1 = hash(key);
long hash2 = hash1 >>> 16;
long[] result = new long[numHashFunctions];
for (int i = 0; i < numHashFunctions; i++) {
long combinedHash = hash1 + i * hash2;
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
result[i] = combinedHash % numBits;
}
return result;
}
//计算哈希值(算法借鉴)
private long hash(String key) {
Charset charset = Charset.defaultCharset();
return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
}
//计算二进制向量大小(算法借鉴)
private long optimalNumOfBits(){
return (long)((double)(-size) * Math.log(fpp) / (Math.log(2.0D) * Math.log(2.0D)));
}
//计算哈希算法数量(算法借鉴)
private int optimalNumOfHashFunctions() {
return Math.max(1, (int)Math.round((double)numBits / (double)size * Math.log(2.0D)));
}
}
控制层、根据ID查询商品信息例子
@RestController
@RequestMapping("goods")
public class GoodsController {
@Autowired
private GoodsMapper goodsMapper;
@Autowired
private RedisBloomFilter redisBloomFilter;
@Autowired
private RedisTemplate<String,Object> redisTemplate;
//使用布隆过滤器 根据ID查询商品
@GetMapping("/{id}")
public R id(@PathVariable String id){
//先查询布隆过滤器,过滤掉不可能存在的数据请求
if (!redisBloomFilter.isExist(id)) {
System.err.println("id:"+id+",布隆过滤...");
return R.success(null);
}
//布隆过滤器认为可能存在,再走流程查询
return R.success(noFilter(id));
}
//不使用过滤器
private Object noFilter(String id){
//先查Redis缓存
Object o = redisTemplate.opsForValue().get(id);
if (o != null) {
//命中缓存
System.err.println("id:"+id+",命中redis缓存...");
return o;
}
//缓存未命中 查询数据库
System.err.println("id:"+id+",查询DB...");
Goods goods = goodsMapper.selectById(id);
//结果存入Redis
redisTemplate.opsForValue().set(id, goods);
return goods;
}
}
启动项目,访问几个不存在的ID,结果如下图所示:
对于可能存在的数据,测试如图所示:
总结
使用Redis缓存,可以一定程度上减轻数据库的压力,但是面对一些特殊情况,如:恶意攻击。程序如果不做处理,数据库还是会存在危险。
使用布隆过滤器可以高效的过滤掉绝大多数无意义的DB查询,当数据量过大时,可能会存在一定的哈希冲突,布隆过滤器存在一定的容错,但是它依然非常高效。