缓存穿透,缓存击穿,缓存雪崩区别

发布时间 2023-09-19 22:20:05作者: 不会敲代码的程序猿!

缓存穿透,缓存击穿,缓存雪崩区别


发现自己有时候明明已经做过了,可能是缺少回顾总结,过了几天这部分知识就会忘的一干二净,一点有记不住,还有这三个概念有时候确实不太能把他分清楚,面试的时候也有让自己说清楚,说的一塌糊涂,所以总结一下

一、缓存穿透,缓存击穿,缓存雪崩简单的区别

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

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

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

二、缓存穿透

缓存穿透常见的解决方案有两种:

1.缓存空对象

  • 优点:实现简单,维护简单
  • 缺点:额外的内存消耗,可能造成短期的不一致

2.布隆过滤

  • 优点:内存占用较少,没有多余的key
  • 缺点:实现复杂,存在误判的可能

3.解决缓存穿透的代码实现

  public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallBack,Long time ,TimeUnit unit){
        //1.查询缓存
        String key = keyPrefix + id;
        String Json = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否存在
        if(StrUtil.isNotBlank(Json)){
            //2.1 如果存在则直接返回数据给用户
            return JSONUtil.toBean(Json,type);
        }
        if(Json != null){
            return null;
        }

        //3.缓存不存在,查询数据库
        R r = dbFallBack.apply(id);
        //3.1如果数据库中也不存在,则返回错误信息
        if(r == null){
            //将一个“”空字符串直接存到redis中
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        //4.将数据库中的数据存入redis
       this.set(key,r,time,unit);
        //5.返回
        return r;
    }

三、缓存击穿

缓存击穿有两种常见的解决方法:

1.互斥锁

2.逻辑过期

3.两者的优缺点

4.互斥锁的代码实现

流程图

代码

  public <R,ID>R queryWithMutex(
            String keyprefix,String lockKeyPrefix,ID id,Class<R>type,Function<ID,R>dbFallBack,Long time ,TimeUnit unit)  {
        //1.查询缓存
        String key = keyprefix + id;
        String Json = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否存在
        if(StrUtil.isNotBlank(Json)){
            //2.1 如果存在则直接返回数据给用户
            return JSONUtil.toBean(Json,type);
        }

        if(Json != null){
            //当缓存的内容不是空字符串""的时候
            return null;
        }

        //2.2 缓存不是空字符串"",而是不存在时进行缓存重建
        //3.缓存重建
        //3.1获取互斥锁
        String lockKey = lockKeyPrefix +id;
        R r = null;
        try {
            boolean flag = tryLock(lockKey);
            //3.2判断是否获取互斥锁成功
            if(!flag){
                //获取互斥锁失败
                //3.3失败则休眠并且重试
                Thread.sleep(50);
                return queryWithMutex(keyprefix,lockKeyPrefix,id,type,dbFallBack,time,unit);
            }
            //3.4成功则进行查询数据库
            //4.缓存不存在,查询数据库
            r = dbFallBack.apply(id);


            //模拟重建的延时
            Thread.sleep(200);

            //4.1如果数据库中也不存在,则返回错误信息
            if(r == null){
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //5.将数据库中的数据存入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r),time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //6.释放互斥锁
            unLock(lockKey);
        }

        //7.返回
        return r;
    }
/**
     * 开启锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 解锁
     * @param key
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

5.逻辑过期的代码实现

流程图

代码

		public <R,ID>R queryWithLogicalExpire(
            String keyprefix,String lockKeyPrefix,ID id,Class<R>type,Function<ID,R>dbFallBack,Long time ,TimeUnit unit){
        //1.查询缓存
        String key = keyprefix + id;
        String Json = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否为空
        if(StrUtil.isBlank(Json)){
            //Json为空,直接返回给用户null
            return null;
        }
        //2.1命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //2.2判断逻辑时间是否已经过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //expireTime在当前时间之后,说明还没过期
            return r;
        }
        //3当逻辑时间已经过期,需要缓存重建
        //3.1获取互斥锁
        String lockKey = lockKeyPrefix + id;
        boolean flag = tryLock(lockKey);
        //判断是否获取锁成功
        if( flag){
            //开启缓存重建,开启独立线程重建
            CACHE_REBULID_EXECUTOR.submit(()->{
                try {
                    //先查数据库
                    R r1 = dbFallBack.apply(id);
                    //2.封装逻辑过期时间
                    RedisData redisData1=new RedisData();
                    redisData1.setData(r1);
                    redisData1.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
                    //再写入Redis
                    stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));

                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }
        //4.返回
        return r;
    }
	/**
     * 开启锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 解锁
     * @param key
     */
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }