从SpringBoot构建十万博文聊聊缓存穿透

前言

在博客系统中,为了提高响应速度,加入了 Redis 缓存,把文章主键 ID 做为 key 值去缓存查询,若是不存在对应的 value,就去数据库中查找 。这个时候,若是请求的并发量很大,就会对后端的数据库服务形成很大的压力。html

形成缘由

  • 业务自身代码或数据出现问题
  • 恶意攻击、爬虫形成大量空的命中,会对数据库形成很大压力

博客架构

案例分析

因为文章的地址是这样子的:java

https://blog.52itstyle.top/49.html

你们很容易猜出,是否是还有 50、5一、52 甚至是十万+?若是是正儿八经的爬虫,可能会读取你的总页数。可是有些不正经的爬虫或者人,还真觉得你有十万+博文,而后就写了这么一个脚本。git

for num in range(1,1000000):
   //爬死你,开100个线程

解决方案

设置布隆过滤器,预先将全部文章的主键 ID 哈希到一个足够大的 BitMap 中,每次请求都会通过 BitMap 的拦截,若是 Key 不存在,直接返回异常。这样就避免了对 Redis 缓存以及底层数据库的查询压力。spring

这里咱们使用谷歌开源的第三方工具类来实现:数据库

<dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>25.1-jre</version>
</dependency>

编写布隆过滤器:后端

/**
 * 布隆缓存过滤器
 */
@Component
public class BloomCacheFilter {

    public static BloomFilter<Integer> bloomFilter = null;

    @Autowired
    private DynamicQuery dynamicQuery;
    /**
     * 初始化
     */
    @PostConstruct
    public void init(){
        String nativeSql = "SELECT id FROM blog";
        List<Object> list = dynamicQuery.query(nativeSql,new Object[]{});
        bloomFilter = BloomFilter.create(Funnels.integerFunnel(), list.size());
        list.forEach(blog ->bloomFilter.put(Integer.parseInt(blog.toString())));
    }
    /**
     * 判断key是否存在
     * @param key
     * @return
     */
    public static boolean mightContain(long key){
        return bloomFilter.mightContain((int)key);
    }
}

而后,每一次查询以前作一次 Key 值校验:缓存

/**
 * 博文
 */
@RequestMapping("{id}.shtml")
public String page(@PathVariable("id") Long id, ModelMap model) {
     if(BloomCacheFilter.mightContain(id)){
         Blog blog = blogService.getById(id);
         model.addAttribute("blog",blog);
         return  "article";
     }else{
         return  "error";
     }
}

效率

那么,在数据量很大的状况下,效率如何呢?咱们来作个实验,以 100W 为基数。架构

public static void main(String[] args) {
        int capacity = 1000000;
        int key = 6666;
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
        for (int i = 0; i < capacity; i++) {
            bloomFilter.put(i);
        }
        /**返回计算机最精确的时间,单位纳妙 */
        long start = System.nanoTime();
        if (bloomFilter.mightContain(key)) {
            System.out.println("成功过滤到" + key);
        }
        long end = System.nanoTime();
        System.out.println("布隆过滤器消耗时间:" + (end - start));
}

布隆过滤器消耗时间:281299,约等于 0.28 毫秒,匹配速度是否是很快?并发

错判率

万事万物都有所均衡,既然效率如此之高,确定其它方面定有所牺牲,经过测试咱们发现,过滤器有 3% 的错判率,也就是说,原本没有的文章,有可能经过校验被访问到,而后报错!app

public static void main(String[] args) {
        int capacity = 1000000;
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
        for (int i = 0; i < capacity; i++) {
            bloomFilter.put(i);
        }
        int sum = 0;
        for (int i = capacity + 20000; i < capacity + 30000; i++) {
            if (bloomFilter.mightContain(i)) {
                sum ++;
            }
        }
        //0.03
        DecimalFormat df=new DecimalFormat("0.00");//设置保留位数
        System.out.println("错判率为:" + df.format((float)sum/10000));
}

经过源码阅读,发现 3% 的错判率是系统写死的。

public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
        return create(funnel, expectedInsertions, 0.03D);
}

固然咱们也能够经过传参,下降错判率。测试了一下,查询速度稍微有一丢丢下降,但也只是零点几毫秒级的而已。

BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);

那么如何作到零错判率呢?答案是不可能的,布隆过滤器,错判率必须大于零。为了保证文章 100% 的访问率,正常状况下,咱们能够关闭布隆校验,只有才突发状况下开启。好比,能够经过阿里的动态参数配置 Nacos 实现。

@NacosValue(value = "${bloomCache:false}", autoRefreshed = true)
private boolean bloomCache;
//省略部分代码
if(bloomCache||BloomCacheFilter.mightContain(id)){
     Blog blog = blogService.getById(id);
     model.addAttribute("blog",blog);
     return  "article";
}else{
     return  "error";
}

小结

缓存穿透大多数状况下都是恶意攻击致使的空命中率。虽然十万博客尚未被百度收录,天天也就寥寥的几十个IP,可是梦想仍是有的,万一实现了呢?因此,仍是要作好准备的!

源码

https://gitee.com/52itstyle/spring-boot-blog

相关文章
相关标签/搜索