什么是缓存击穿、缓存穿透、缓存雪崩


1.缓存击穿

缓存击穿是指某个热点数据key在缓存中失效,大量的请求同时涌向后台数据库,对数据库造成很大的压力。

解决方案:

  • 逻辑过期:在缓存数据中存储逻辑过期时间(如字段expireTime),实际缓存不设物理过期。当数据过期时,异步线程更新缓存,其他线程仍返回旧数据。
  • 互斥锁:在过期后,通过加锁控制对数据库的访问,从而保证只有一个请求能够去查询数据库并更新缓存。

/**
     * 逻辑过期方案解决缓存击穿问题
     *
     * @param id
     * @return
     */
    public Shop queryWithLogicalExpire(Long id) {
        //1.从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        //2.判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.不存在,直接返回
            return null;
        }
        //4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //5.1未过期,直接返回店铺信息
            return shop;
        }
        //5.2已过期,需要缓存重建
        //6缓存重建
        //6.1获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        //6.2判断是否获取锁成功
        if (isLock) {
            //获取互斥锁成功,再次对redis缓存做DoubleCheck
            shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
            redisData = JSONUtil.toBean(shopJson, RedisData.class);
            data = (JSONObject) redisData.getData();
            shop = JSONUtil.toBean(data, Shop.class);
            expireTime = redisData.getExpireTime();
            if (expireTime.isAfter(LocalDateTime.now())) {
                //6.2.1未过期,直接返回店铺信息
                unLock(lockKey);
                return shop;
            }
            //6.2.2已过期,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unLock(lockKey);
                }

            });
        }
        return shop;
    }

要点:在缓存重建时需要双重检查


2.缓存穿透

缓存穿透是指用户请求的数据在缓存和数据库中都不存在,从而导致每次请求都落到数据库上。

解决方案:

  • 缓存空对象:缓存空结果,但要设置短的过期时间。
  • 布隆过滤器:在访问缓存之前进行一次快速判断,过滤掉不可能存在的数据请求。由于哈希碰撞,会存在一定的误判率,但是这个误判率非常低。
/**
     * 缓存穿透
     *
     * @param id
     * @return
     */
    public Shop queryWithPassThrough(Long id) {
        //1.从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判断命中的是否是空值
        if (shopJson != null) {
            return null;
        }
        //4.不存在,根据id查询数据库
        Shop shop = getById(id);
        //5.数据库也不存在,返回错误
        if (shop == null) {
            //将空值写入redis
            stringRedisTemplate.opsForValue().set("cache:shop:" + id, "", 2L, TimeUnit.MINUTES);
            return null;
        }
        //6.存在,写入redis
        stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
        //7.返回
        return shop;
    }

3.缓存雪崩

缓存雪崩:是指在某一时刻,大量缓存数据共同过期,导致瞬时大流量涌向数据库。或者由于分布式缓存节点故障导致缓存失效。

解决方案:

  • 过期时间设置为随机:避免大量缓存同一时间过期
  • 多级缓存架构:采用本地缓存(如Caffeine)+分布式缓存(如Redis)组合,本地缓存作为二级缓冲层。

文章作者: Fuchanglai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Fuchanglai !
赏
  目录