缓存击穿问题

92 阅读5分钟

什么是缓存击穿?缓存击穿的解决方案...

🚀什么是缓存击穿?

缓存击穿

缓存击穿是指一个缓存中不存在但数据库中存在的数据,在高并发的情况下,多个请求同时访问这个不存在的数据,导致这些请求都穿透缓存直接访问数据库,给数据库造成巨大压力。这通常发生在缓存中的数据过期时,被大量并发请求同时请求,而此时缓存失效,导致这些请求直接打到数据库上。

缓存击穿 的 主要原因

  • 并发访问: 大量并发请求同时访问一个缓存中过期的数据,导致缓存失效,触发大量请求直接访问数据库。
  • 热点数据失效: 缓存中的某个热点数据失效,且该数据被高频访问,导致失效时大量请求穿透缓存。

🚀缓存击穿的解决方案

设置互斥锁

  • 设置互斥锁(Mutex Lock): 在缓存失效的时候,可以使用互斥锁来保证只有一个线程去加载数据,其他线程等待,避免大量请求同时穿透到数据库。这种方式需要谨慎使用,因为过多的锁竞争可能会影响系统性能。
    • 假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

逻辑过期

  • 逻辑过期: 逻辑过期是一种在缓存中设置数据的过期时间 的方法,但是它并非真正的时间戳过期,而是通过一些逻辑判断来确定缓存是否需要刷新。使用逻辑过期可以在缓存失效时,避免大量请求穿透到数据库,减轻数据库压力。
    • 假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个新 线程去进行 以前的重构数据的逻辑 ,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁 ,线程3也直接返回数据 ,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。

互斥锁 VS 逻辑过期

互斥锁方案: 由于保证了互斥性,所以数据一致 ,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能受到影响

逻辑过期方案: 线程读取过程中不需要等待,性能好 ,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据(数据不一致) ,且实现起来麻烦


🚀 互斥锁 解决 缓存击穿

流程与思路

相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取 ,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询。

如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿

⚪setnx实现互斥锁

利用redis的setnx方法来表示获取锁 ,该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false,我们可以通过true 或 false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。

    //获取互斥锁
    private boolean tryLock(String key){
        //setIfAbsent()就是使用redis的setnx,key为键
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag); //返回
    }
​
    //释放互斥锁(删除redis缓存)
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

⚪解决查询数据时的缓存击穿问题

    @Resource
    private StringRedisTemplate stringRedisTemplate;
​
    // 根据id查询商铺信息(缓存空值,避免缓存穿透问题)
    @Override
    public Result queryById(Long id) {
        //解决缓存穿透问题
//        Shop shop = queryWithPassThrough(id);
​
        //互斥锁 解决 缓存击穿问题
        Shop shop = queryWithMutex(id);
​
​
        return Result.ok(shop);
    }
​
    //解决查询数据时,缓存击穿和缓存穿透问题的代码
    public Shop queryWithMutex(Long id){
        // redis缓存的key
        String key = RedisConstants.CACHE_SHOP_KEY + id;
​
        //1. 从redis缓存中获取shop信息
        String shopJSON = stringRedisTemplate.opsForValue().get(key);
​
        //2. 缓存存在,返回
        if(StrUtil.isNotBlank(shopJSON)){
            Shop cacheShop = JSONUtil.toBean(shopJSON, Shop.class);
            return cacheShop;
        }
​
        //3. 避免了缓存穿透,获取到空字符串"",直接返回错误
        if(shopJSON != null){
            return null;
        }
​
        //4. 进行缓存重建
        //4.1 获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        Shop shop = null;
        try{
            boolean lock = tryLock(lockKey);
            //4.2 判断是否获得锁
            if(!lock){
                //4.3 失败,进入休眠,稍后重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //4.4 成功、从数据库查询数据
            shop = this.getById(id);
​
            Thread.sleep(200);//模拟重建延时(可以删除)
​
            if(shop == null){
                //空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.SECONDS);
            }
​
            //5. 数据库中存在,存入redis缓存
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
​
            //6. 返回
            return shop;
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }finally {
            unlock(lockKey);
        }
​
    }