Redis缓存穿透与缓存击穿

Redis缓存穿透与缓存击穿

缓存穿透

在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。

解决方案

1)缓存空对象
简单的来说,就是请求之后,发现数据不存在,就将null值打入Redis中。

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

思路分析:
当我们客户端访问不存在的数据时,先请求 redis,但是此时 redis 中没有数据,
此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,
我们都知道数据库能够承载的并发不如 redis 这么高,如果大量的请求同时过来访问这种不存在的数据,
这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,
我们也把这个数据存入到 redis 中去,这样,下次用户过来访问这个不存在的数据,
那么在 redis 中也能找到这个数据就不会进入到数据库了。

2)布隆过滤
在客户端与Redis之间加了一个布隆过滤器,对请求进行过滤。

 布隆过滤器的大致原理:布隆过滤器中存放二进制位。
           数据库的数据通过hash算法计算其hash值并存放到布隆过滤器中,
           之后判断数据是否存在的时候,就是判断该hash值是0还是1。

           但是这是一种概率上的统计,当其判断不存在的时候就一定是不存在;
            当其判断存在的时候就不一定存在。所以有一定的穿透风险

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

编码解决用户查询的缓存穿透问题

核心思路如下:

在原来的逻辑中,我们如果发现这个数据在 mysql 中不存在,直接就返回 404 了,

这样是会存在缓存穿透问题的

现在的逻辑中:
如果这个数据不存在,我们不会返回 404 ,还是会把这个数据写入到 Redis 中,
并且将 value 设置为空,当再次发起查询时,我们如果发现命中之后,判断这个 value 是否是 null,
如果是 null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据。

@Override
public Result queryById(Long id) {
    // 从redis查询商铺缓存
    String key = CACHE_SHOP_KEY + id;
    String userJson = stringRedisTemplate.opsForValue().get(key);
 
    // 判断是否存在
    if (StrUtil.isNotBlank(userJson )) {
        // 存在,直接返回
        User user = JSONUtil.toBean(userJson , User.class);
        return Result.ok(user );
    }
 
    // 1.检查缓存中是否有空值
    if (userJson == null) {
        // 返回一个错误信息
        return Result.fail("用户不存在!");
    }
 
    // 不存在,根据id查询数据库
    User user = getById(id);
 
    // 不存在,返回错误
    if (user == null) {
        // 2.防止穿透问题,将空值写入redis
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        return Result.fail("用户不存在!");
    }
 
    // 存在,写入Redis
    // 把shop转换成为JSON形式写入Redis
    // 同时添加超时时间
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(user), CACHE_SHOP_TTL, TimeUnit.MINUTES);
    return Result.ok(user);
}

缓存击穿

缓存击穿是部分key过期导致的严重后果。

为什么大量key过期会产生问题而少量的key也会有问题?

缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,

无数的请求访问会在瞬间给数据库带来巨大的冲击。

上述:假设此时该热点key的TTL时间到(失效了),则查询缓存未命中,会继续查询数据库,并进行缓存重建工作。但是由于查询SQL逻辑比较复杂、重建缓存的时间较久,并且该key又是热点key,短时间内有大量的线程对其进行访问,所以请求会直接到数据库中,数据库就有可能垮掉!

缓存击穿解决方案

通过互斥锁解决缓存击穿方案
简单的来说:
并不是所有的线程都有 “ 资格 ” 去访问数据库,只有持有锁的线程才可以对其进行操作。
不过该操作有一个很明显的问题,就是会出现相互等待的情况。

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

     如果获取到了锁的线程,再去进行查询,查询后将数据写入 redis,再释放锁,返回数据,

利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿。

public User queryByMutex(Long id) {
    // 1.从redis查询商铺缓存
    String key = CACHE_SHOP_KEY + id;
    String userJson= stringRedisTemplate.opsForValue().get(key);
 
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopString )) {
        return JSONUtil.toBean(shopString, User.class);
    }
 
    // 判断空值
    if (userJson!= null) {
        // 返回一个错误信息
        return null;
    }
 
    String lockKey = "lock:user:" + id;
    User user= null;
    try {
        // 4.实现缓存重建
        // 4.1获取互斥锁
        boolean isLock = tryLock(lockKey);
 
        // 4.2判断是否成功
        if (!isLock) {
            // 4.3失败,则休眠并重试
            Thread.sleep(50);
            // 递归
            return queryByMutex(id);
        }
        // 4.4成功,根据id查询数据库
        user= getById(id);
 
        // 模拟延迟
        Thread.sleep(200);
 
        // 5.不存在,返回错误
        if (user== null) {
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
 
        // 6.存在,写入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user),
        CACHE_SHOP_TTL,TimeUnit.MINUTES);
 
    } catch (InterruptedException ex) {
        throw new RuntimeException(ex);
    } finally {
        // 7.释放锁
        unLock(lockKey);
    }
 
    // 8.返回
    return user;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值