• Redis缓存穿透问题的解决思路


    1.什么是缓存穿透

    缓存穿透是指客户端请求的数据在缓冲中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库中。

    2.解决方案

    常见的解决方案有两种。

    • 缓存空对象
      • 优点:
        • 实现简单,维护方便。
      • 缺点:
        • 额外的内存消耗
        • 可能造成短期的不一致
    • 布隆过滤器
      • 优点:内存占用少,没有多余的key
      • 缺点:实现复杂,存在误判可能。

    2.1 缓存空对象解决方案

    缓存空对象思路分析: 当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到数据库了。

    2.2 布隆过滤器解决方案

    布隆过滤: 布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,
    假设布隆过滤器判断这个数据不存在,则直接返回
    这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突。

    2.3 图解

    在这里插入图片描述

    3. 实际应用

    在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的

    现在的逻辑中:如果这个数据不存在,我们不会返回404 ,还是会把这个数据写入到Redis中,并且将value设置为空,欧当再次发起查询时,我们如果发现命中之后,判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据。
    在这里插入图片描述

    3.1 代码实现

    工具类:

    	/**
         * 缓存空字符 解决缓存穿透
         * @param keyPrefix
         * @param id
         * @param type
         * @param dbFallback
         * @param time
         * @param unit
         * @param 
         * @param 
         * @return
         */
        public <R,ID> R queryWithPassThrough(
                String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
            String key = keyPrefix + id;
            // 1.从redis查询商铺缓存
            String json = stringRedisTemplate.opsForValue().get(key);
            // 2.判断是否存在
            if (StrUtil.isNotBlank(json)) {
                // 3.存在,直接返回
                return JSONUtil.toBean(json, type);
            }
            // 判断命中的是否是空值
            // ""->表示数据库没有 缓存了空字符串  null->表示:缓存中没有,数据库中不一定有没有,需要去查询数据库 ,
            // 所以 json !=null 也就是 = "" 表示 缓存了空字符串 返回null 表示数据库也没有。
            if (json != null) {
                // 返回一个错误信息
                return null;
            }
    
            // 4.不存在,根据id查询数据库
            R r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            this.set(key, r, time, unit);
            return r;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    调用:

        public Result queryShopById(Long id) {
    //        String key = CACHE_SHOP_KEY+id;
    //        // 1.从redis查询商铺缓存
    //        String shopJson = stringRedisTemplate.opsForValue().get(key);
    //        // 2.判断是否存在
    //        if(StrUtil.isNotBlank(shopJson)){
    //            //3.存在直接返回
    //            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
    //            return Result.ok(shop);
    //        }
    //        // 4, 不存在,根据id查询数据库
    //        Shop shop = getById(id);
    //        // 5.不存在 返回错误
    //        if(shop == null ){
    //            return Result.fail("店铺不存在!");
    //        }
    //        //6.存在 写入redis
    //        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
    //        //7.返回
    //        return Result.ok(shop);
    
    
            // -- 升级
    
            // 解决缓存穿透
            Shop shop = cacheClient
                    .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
            if (shop == null) {
                return Result.fail("店铺不存在!");
            }
            // 7.返回
            return Result.ok(shop);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    代码没有贴全,大概思路是这样。

    4. 小总结

    缓存穿透产生的原因是什么?

    • 用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力

    缓存穿透的解决方案有哪些?

    • 缓存null值
    • 布隆过滤
    • 增强id的复杂度,避免被猜测id规律
    • 做好数据的基础格式校验
    • 加强用户权限校验
    • 做好热点参数的限流

    参考资料:
    黑马Redis资料。

    若有错误,希望大佬指出。
    对你有帮助给点个👍再走呗。

  • 相关阅读:
    # nest笔记八:使用apifox导入swagger
    java计算机毕业设计水质监测数据采集系统源码+系统+数据库+lw文档+mybatis+运行部署
    上周热点回顾(2.28-3.6)
    大数据课程L7——网站流量项目的操作步骤
    MySQL按小时分组统计日志记录数量
    JVM Metaspace内存溢出问题
    SpringBoot整合任务系统(quartz和SpringTask)
    VMware 虚拟机图文安装和配置 AlmaLinux OS 8.6 教程
    C#实现最大公约数和最小公倍数
    【ARM 安全系列介绍 3.7 -- SM4 对称加密算】
  • 原文地址:https://blog.csdn.net/weixin_44254243/article/details/126498554