Redis缓存及缓存更新,缓存穿透,缓存雪崩,缓存击穿具体解决方案

什么是缓存

缓存就是数据交换的缓冲区,是临时储存数据的地方,读写性能高。

项目使用场景

在项目中,我们一般把读写频繁的数据缓存到redis中,以减少数据库的压力,降低后台的负载,提高读写效率,减少响应时间

 

在项目中使用缓存

黑马的点评项目:

对于项目中的获取店铺列表是经常需要请求的,所以我们就以此为例:

缓存店铺列表

1.首先在redis中查找,判断是否命中

        命中,直接返回具体信息

        未命中,查找数据库,将数据保存至redis中一份,同时返回给前端具体信息

缓存更新策略

在项目中,如果我们直接操作了数据库,从而导致数据库中数据和redis中数据不一致,这种问题该如何解决呢?

1.采用redis自己的内存淘汰机制,让他在内存占用多时,自己选择淘汰一部分,下次查询再更新。

2.给redis中键设置过期时间

3.程序员在对数据库操作时,同时更新redis

 折中一下,给键设置过期时间,如果过期时间短的话,出现错误的概率会很小,也容易操作

对于数据的更新,我们是先删缓存再操作数据库呢?,还是先操作数据库,再删除缓存呢?

在高并发环境下,由于操作数据库的时间肯定是比redis操作时间消耗大的,所以,我们为了减少错误,应该先进行数据库的更新,再进行redis中的操作

 缓存穿透

 

 次数一多,这会导致数据库的压力变大

解决方案:

 

1.缓存空对象

        第一次如果未命中,将其缓存到redis中,值为空,这样下一次还请求,就直接redis返回空

优点:简单,容易实现

缺点:造成内存浪费,可能造成短期不一致(我存了个null,但刚好我数据库又多了一个这样的真实值)

 

2.布隆过滤

请求过来,先过布隆过滤,检查数据是否存在,如果不存在,直接拒绝

优点:消耗少,没有多余key

缺点:实现复杂,存在误判

 

在代码中实现:

 

 //将list按String存储
    @Override
    public List<ShopType> getShopType() {
        String key = SHOP_TYPE_LIST;
//        首先判断redis中是否有数据,是否命中
        String s = stringRedisTemplate.opsForValue().get(key);
        if (!StrUtil.isBlank(s)) {//不为空
//            将String转成list
            List<ShopType> shopTypes = JSONUtil.toList(s, ShopType.class);
            return shopTypes;
        }
//        判断命中是否是空值
        if (s != null) {//经过上面判断,不是null,就一定是空字符串
            return null;
        }
      
//        获取成功,查找数据库
            List<ShopType> typeList= this.query().orderByAsc("sort").list();
            if (typeList == null) {
//           存储空对象
                stringRedisTemplate.opsForValue().set(SHOP_TYPE_LIST, "", SHOP_TYPE_NULL, TimeUnit.MINUTES);
                return typeList;
            }
//        保存到redis
            String type = JSONUtil.toJsonStr(typeList);
            stringRedisTemplate.opsForValue().set(SHOP_TYPE_LIST, type, SHOP_TYPE_TIME, TimeUnit.MINUTES);

        return typeList;
    }

 缓存雪崩

 

 解决方案:

缓存击穿

解决方案:
 

1.通过互斥锁

让一个线程得到锁,进行缓存创建,其余等待

 代码:

 

  @Override
    public List<ShopType> getShopType() {
        String key = SHOP_TYPE_LIST;
//        首先判断redis中是否有数据,是否命中
        String s = stringRedisTemplate.opsForValue().get(key);
        if (!StrUtil.isBlank(s)) {//不为空
//            将String转成list
            List<ShopType> shopTypes = JSONUtil.toList(s, ShopType.class);
            return shopTypes;
        }
//        判断命中是否是空值
        if (s != null) {//经过上面判断,不是null,就一定是空字符串
            return null;
        }
        String lockKey = SHOP_TYPE_LOCK;
        List<ShopType> typeList = null;
//        获取lock
        try {
            boolean b = tryLock(lockKey);
            if (!b) {
//            获取失败,休眠,递归尝试再次获取
                Thread.sleep(50);
                return getShopType();
            }
//        获取成功,查找数据库
            typeList= this.query().orderByAsc("sort").list();
            if (typeList == null) {
//           存储空对象
                stringRedisTemplate.opsForValue().set(SHOP_TYPE_LIST, "", SHOP_TYPE_NULL, TimeUnit.MINUTES);
                return typeList;
            }
//        保存到redis
            String type = JSONUtil.toJsonStr(typeList);
            stringRedisTemplate.opsForValue().set(SHOP_TYPE_LIST, type, SHOP_TYPE_TIME, TimeUnit.MINUTES);

        }catch (Exception e){
            return null;
        }finally {
            //        释放锁
            unlock(SHOP_TYPE_LOCK);
        }
        return typeList;
    }

    //尝试获取锁
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);//相当于setnx,当值存在时,不会进行修改
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

2.逻辑过期

永久保存该值,过期时间不设置,而是在保存对象中设置一个过期时间标识,让程序判断是否过期,采用互斥锁,让一个线程再开辟一个线程用作缓存创建,返回数据返回原有逻辑过期数据即可

 代码:

 

//    线程池
    private static final ExecutorService CACHE_REBULD = Executors
        .newFixedThreadPool(10);
//    逻辑删除解决缓存击穿
    public List<ShopType> getShopType2() {
//        1.从redis中查询缓存
        String key = SHOP_TYPE_LIST;
        String lockKey = SHOP_TYPE_LOCK;
//        2.判断是否存在
//        首先判断redis中是否有数据,是否命中
        String s = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(s)) {//为空,直接返回null
            System.out.println("为空");
            return null;
        }
//        3.命中,将redis中redisData对象反序列化出来
        RedisData redisData = JSONUtil.toBean(s, RedisData.class);
//        拿到用户信息
        List<ShopType> shopTypes = JSONUtil.toList((JSONArray)redisData.getData(), ShopType.class);
//        拿到过期时间
        LocalDateTime expireTime = redisData.getExpireTime();
//        4.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){//是否在当前时间之后
//        之后说明已经没有过期过期
            System.out.println("未过期");
//        未过期,直接返回相应信息
            return shopTypes;
        }
//        5.过期,缓存重建
//        获取互斥锁
        boolean b = tryLock(lockKey);
        System.out.println(b);
//        判断是否获取成功
        if(b){
//        成功开启独立线程进行缓存重建
            CACHE_REBULD.submit(()->{
                try {
                    System.out.println("开始重建缓存");
                    this.sendSomeExample();
                }catch (Exception e){
                    throw new RuntimeException("逻辑删除新线程的缓存重建失败");
                }finally {
                    //            释放锁
                    this.unlock(lockKey);
                }
            });
        }
//        获取失败返回过期对象
        return shopTypes;
    }

优缺点:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值