Redis缓存三大问题总结

目录

学习目标

学习内容

缓存穿透

缓存雪崩

缓存击穿


学习目标

学习并解决redis缓存三类问题


学习内容

在我们在查询数据时,在没有使用类似redis缓存的情况下,一般都是直接去数据库中查询,但是直接查询数据库的效率比使用redis缓存来查询数据的效率要低,所以我们需要增加缓存,但是使用redis缓存就存在一些问题,这里是对使用redis缓存的三类问题的总结。

缓存穿透

问题描述

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

解决方案

  • 缓存空对象

当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库。为防止这个问题的发生,对于访问的这个数据在数据库中即使不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据。

 public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }
public <R,ID> R queryWithPassThrough(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time,TimeUnit unit){
        //1.从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        //2.判断是否存在
        if(StrUtil.isNotBlank(json)){
            //3.存在,直接返回
            return  JSONUtil.toBean(json, type);
        }
        //判断命中的是否为空值
        if(json!=null){
            //这个是用来反馈信息的
            return null;
        }
        //4.不存在,根据id查询数据库
        //交给调用者
        R r = dbFallback.apply(id);
        //5.不存在返回错误
        if(r==null){
            //空值写入redis
            stringRedisTemplate.opsForValue().set(keyPrefix+id,"",time,unit);
            return null;
        }
        //6.存在写入redis
       this.set(keyPrefix+id,r,time,unit);
        //7.返回
        return r;
    }
  • 布隆过滤

布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,

假设布隆过滤器判断这个数据不存在,则直接返回

这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲

这个暂时没有实现代码。。。。

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值

  • 给缓存业务添加降级限流策略

目前暂时只知道第一种方案就挺好的了,就是在导入缓存key的时候,给key设置不同的过期时间就行了。

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:

  • 互斥锁

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行。对于锁的实现可以通过redis中的setnx来实现。

当第一个人访问时,缓存并未命中,第一个人就需要拿到锁并从数据库中取出数据在redis中重新缓存数据,在第一个没有释放锁时,其他人的访问请求只能等待,直到第一个人释放锁后,在redis中可以查询到数据。

 //redis加锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);//直接返回会发生拆箱,有可能会返回null
    }

    //释放锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }
   /**
     * 缓存击穿,互斥锁处理
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id){
        //1.从redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断命中的是否为空值
        if(shopJson!=null){
            //这个是用来反馈信息的
            return null;
        }
        //4.不存在,根据id查询数据库
        //实现缓存重建
        //获取锁
        Shop shop= null;
        String lockKey="lock:shop:0"+id;
        try {

            boolean isLock=tryLock(lockKey);
            //判断是否获取成功
            if(!isLock){
                // 失败,休眠并重试
                Thread.sleep(50);
                queryWithMutex(id);
            }
            //获取锁成功之后应对redis缓存在做一次判断,防止其余等待的线程重复查询数据库
            //1.从redis查询缓存
            shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
            //2.判断是否存在
            if(StrUtil.isNotBlank(shopJson)){
                //3.存在,直接返回
                shop = JSONUtil.toBean(shopJson, Shop.class);
                return shop;
            }
            shop = getById(id);
            //5.不存在返回错误
            if(shop==null){
                //空值写入redis
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //6.存在写入redis
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL, TimeUnit.MINUTES);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放互斥锁
            unLock(lockKey);
        }
        //7.返回
        return shop;
    }
  • 逻辑过期

我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据就可以一直占用我们内存了。我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。

假设第一个人去查询缓存,然后从value中判断出来当前的数据已经过期了,第一个人获取互斥锁,获得了锁的线程他会开启一个 线程去进行 更新缓存的数据,直到新开的线程完成这个逻辑后,才释放锁。在更新数据的期间,其他人来访问数据时,直接返回redis中未更新的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。

//设置逻辑过期时间
public void setWithExpire(String key,Object value,Long time,TimeUnit unit){
        //设置逻辑过期
        RedisData redisData=new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        //写入reids
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }
//创建线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);

    /**
     * 缓存击穿逻辑过期处理
     * @param id
     * @return
     */
    public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R> dbFallback,Long time,TimeUnit unit){
        //1.从redis查询缓存
        String Json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
        //2.判断是否存在
        if(StrUtil.isBlank(Json)){
            //3.存在,直接返回
            return null;
        }
        //命中,需要把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R r=JSONUtil.toBean(data,type);
        LocalDateTime expireTime=redisData.getExpireTime();
        //判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //未过期,直接返回店铺对象
            return r;
        }
        //已过期,开始缓存重建
        //获取锁
        String lockKey=keyPrefix+id;
        boolean flag = tryLock(lockKey);
        //判断获取是否成功
        if(flag){
            //此处应对redis缓存再做一次判断
            //TODO 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                //查询数据
                R r1= dbFallback.apply(id);
                //写入redis
                this.setWithExpire(keyPrefix+id,r1,time,unit);
                //释放锁
                unLock(lockKey);
            });
        }
        //失败返回商铺信息
        return r;
    }

到此为止,上述内容就是这段时间我所学习的reids缓存相关的知识。

此文章仅供本人学习使用,如有问题,欢迎各位大佬指正与交流。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值