zoukankan      html  css  js  c++  java
  • 从SpringBoot构建十万博文聊聊缓存穿透

    前言

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

    造成原因

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

    博客架构

    案例分析

    由于文章的地址是这样子的:

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

    大家很容易猜出,是不是还有 50、51、52 甚至是十万+?如果是正儿八经的爬虫,可能会读取你的总页数。但是有些不正经的爬虫或者人,还真以为你有十万+博文,然后就写了这么一个脚本。

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

    解决方案

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

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

    <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% 的错判率,也就是说,本来没有的文章,有可能通过校验被访问到,然后报错!

     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

  • 相关阅读:
    基本排序算法
    Ubuntu下fcitx安装。(ibus不会用)
    sublime搭建Java编译平台及编码问题
    Centos6.5(final)安装gcc和g++,python以及导致问题的解决方法
    如何查询centos查看系统内核版本,系统版本,32位还是64位
    vim插件之SnipMate
    Linux rename命令
    Hadoop安装(Ubuntu Kylin 14.04)
    vim 文字插入
    Checkbox indeterminate属性
  • 原文地址:https://www.cnblogs.com/smallSevens/p/11343674.html
Copyright © 2011-2022 走看看