Redis缓存穿透、击穿、雪崩

Redis目前是非常流行的缓存数据库,缓存穿透、缓存击穿、缓存雪崩是常见的面试题,也是非常重要的问题。

缓存穿透

缓存穿透指的是客户端请求的数据既不在缓存中,也不在数据库中,这样导致请求访问缓存,发现缺失,再去访问数据库时发现也没有数据,当大量的请求到来,数据库的压力就会突然增加。

解决方案:

  • 缓存空对象,对于一些在数据库查找不到记录的,我们将其缓存key的value设置成NULL,设置一个过期时间,这样下次请求访问这个不存在的数据,就不需要再次查询数据库了。

  • 布隆过滤器,布隆过滤器主要用的是哈希的思想,通过一个庞大二进制数和映射函数组成的。

布隆过滤器可能出现判断结果存在的时候不一定存在,但是判断结果为不存在的时候一定不存在,有误判的可能,可以添加元素,不可以删 除元素。

除上面这些外,还可以增强id的复杂度,避免被猜测id规律,做好数据的基础格式校验,热点参数的限流。

缓存雪崩

缓存雪崩指的是同一时间段大量的缓存key失效或者Redis宕机,导致大量请求访问数据库。

解决方案:

  • 均匀设置缓存key的过期时间,可以添加随机值,这样就可以保证数据不会都在同一时间过期。

  • 给Redis集群提高服务的可用性

  • 给缓存服务添加服务熔断或请求限流机制

  • 采用互斥锁,当数据不在Redis,加一个互斥锁,等缓存重新构建完成后再释放锁。

  • 采用双锁策略,一个是主key,设置过期时间,一个是备份key,不会设置过期时间,对value做一个副本。

缓存击穿

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

解决方案:

  • 互斥锁方案,保证在同一时间只有一个线程可以更新缓存,未能获取到互斥锁的请求,需要等待锁的释放,或者返回空值。

  • 给数据设置逻辑过期时间,当知道数据已经过期时,可以通知后台线程更新缓存以及重新设置过期时间。

代码实战部分

缓存击穿实战代码封装

对于需要添加逻辑过期时间,我们需要数据和逻辑过期时间封装到RedisData中,其中在需要重建缓存的数据需要使用到互斥锁来限制只有一个线程进行重建,并且这个线程是新开的线程,返回已经过期的数据,后面的请求访问过来也都是先返回过期数据,直到新线程重建完缓存数据才是一致性,会出现短暂性的缓存和数据的不一致问题。

private static final ExecutorService CACHE_REBUILD_EXECUTOR;
static {
    CACHE_REBUILD_EXECUTOR = newFixedThreadPool(10);
}
public <R, ID> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbCallBack,  Long time, TimeUnit timeUnit) {
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 1. 如果不存在
        if (StrUtil.isBlank(json)) {
            // 2. 不存在,直接返回
            return null;
        }
        // 3. 如果存在, 先把json反序列化为对象
        RedisData data = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) data.getData(), type);
        LocalDateTime expireTime = data.getExpireTime();
        // 4. 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 5.1 未过期,直接返回店铺信息
            return r;
        }
        // 5.2 已过期,开始重建
        // 6. 缓存重建
        // 6.1 尝试获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            // 6.3 成功,开启另外一个线程将进行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    R r1 = dbCallBack.apply(id);
                    this.setLogicalExpire(key, r1, time, timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4 返回过期数据
        return r;
    }

public void setLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit) {
        // 逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

互斥锁解决缓存击穿问题:

在进行重建的时候需要申请一把锁,这里才有的是Redis的setnx命令,只有key不存在的时候才会申请成功,申请失败,我们就重复申请。当然我们也可以使用Lock,或者synchronized关键字来同步代码快,同步代码块的时候锁是字符串的时候,需要锁的对象是字符串的.intern()方法,如果是同步方法的话,效率太低了。

public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, type);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.实现缓存重建
        // 4.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功,根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }

缓存穿透解决

将缓存查询不到和数据库查询不到的数据写入到Redis中。

public <R, ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 如果是空值
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }
        // 判断是否是空值
        if (json != null) {
            return null;
        }
        R r = dbFallback.apply(id);
        if (r == null) {
            stringRedisTemplate.opsForValue().set(key, "" ,RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        this.set(key, r, time, timeUnit);
        return r;
    }

上面是学习Redis课程使用到的一些解决方案,还有很多的方案没有列举出来,布隆过滤器可以使用hutool或者一些开源库的,比较稳定,那就到最后啦,需要大家可以多多支持持…

posted @ 2023-02-01 09:53  Leo哥coding~  阅读(62)  评论(0编辑  收藏  举报  来源