手写布隆过滤器防止缓存穿透

什么是布隆过滤器?

布隆过滤器(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查询,当数据量过大时,可能会存在一定的哈希冲突,布隆过滤器存在一定的容错,但是它依然非常高效。

发布了100 篇原创文章 · 获赞 23 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_32099833/article/details/103844890