缓存,换粗穿透,缓存雪崩,缓存击穿

前言

由于本文讲述的主要是缓存穿透,缓存雪崩和缓存击穿这三个常见的问题,这三个问题是在学习黑马点评时遇到的问题,所以说业务逻辑也是在黑马点评中就已经前提说好了。在这之前,我们肯定首先需要知道什么是缓存,缓存主要用来干什么,那么废话不多说,我们就开始我们今天的解答。

什么是缓存

由于我个人对于计算机的理解也不能说太深,所以我只好通过我个人较为直白的的理解通过mysql和redis这两个数据库去讲述一下什么是缓存,我么知道,当我们需要从"我的电脑"当中读取数据时,是从C盘,D盘这样的磁盘当中去读取的,这就涉及到了IO操作,Mysql中的数据也是基于去存储的,所以说当我们的应用层需要去访问mysql中的数据的时候,就涉及到了大量的IO操作。而Redis就提供了一个缓存,缓存当中也可以对数据进行一系类操作,但是缓存他是基于电脑的运行内存去实现的,我们购买电脑时也会去看电脑的配置信息,其中就包含了运行内存这一个信息,这也就是我刚才所说的运行内存

为什么需要缓存

就像刚才所说的,对于Mysql的一系类操作涉及到了磁盘IO操作,而对于Redis的操作则是涉及到了电脑的运行内存,我们知道基于内存实现内容交互肯定是比磁盘要快很多的,因此如果当有大量的请求直接去我们的数据库查询,那么会很容易的将数据库直接干崩溃。这个时候缓存的作用就出现了。当第一次请求请求过来时,我们可以先去查询缓存,如果命中缓存了,那么直接将缓存中的数据返回即可,但是如果缓存未命中,就说明我们先前没有查询过,那么我们就需要先去查询数据库,再将数据库的信息写入缓存即可

缓存穿透

定义

在进行讨论解决,我们肯定首先需要知道什么是缓存穿透,缓存穿透指的是当一个请求到发送到我们的服务端时,我们需要去查找对应的数据并将这个数据返回给他,可是存在着一种情况,就是需要查询的数据在我们的数据库和缓存都不存在,这样缓存永远都不会生效,这些请求最终都会打到数据库上,造成数据库的崩溃。例如:前端发送请求查询小明的信息,可是小明的信息在我们的缓存和数据库不存在,前端第一次查询发现没有数据返回,那么就会一直持续查询,这样是很不好的

解决方案

缓存空对象

缓存空对象,就是他的字面意思,当请求发送过来时,我们查询缓存未命中,就会去查找我们的数据库,但是如果我们发现数据库中也没有对应的信息,我们可以将一个空对象直接写入缓存,这样以后如果有类似的请求,就会直接从缓存中获取空值,但是我们一般会为这个缓存设置一个有效期

优点:实现简单,维护方便

缺点:额外的内存消耗,可能造成短期的不一致

布隆过滤

当客户端发送请求时,会首先通过我们的布隆过滤器,由布隆过滤器进行判断,只有过滤器允许放行请求才可以继续执行

优点:内存占用较少,没有多余的key

缺点:实现复杂,存在误判功能

缓存雪崩

定义

缓存雪崩指的是同一时间内大量的缓存key同时失效或者是Redis服务宕机,导致客户端的大量请求直接请求到数据库,带来巨大压力

解决方案

1.给不同的key的TTL添加随机值

2.利用Redis集群提高服务的可用性

3.给缓存业务添加降级限流策略

4.给业务添加多级缓存

缓存击穿

定义

缓存击穿问题也叫做热点key问题,就是一个被高并发访问并且缓存重建业务较为复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,常常出现在我们的秒杀场景中

解决方案

互斥锁

当一个线程去查询数据的时候,缓存没有命中,此时需要给这个缓存加上锁,之后再进行查询数据库进行缓存重建的操作,当这个操作执行完成之后再将锁给释放,因此当其他线程在释放锁之前发现查询缓存没有命中的情况,就让当前线程休眠一会,然后再进行操作,直到上一个线程将锁释放之后才会继续执行操作。

加上锁之后,当一个线程出发了缓存未命中的状态时,在他进行缓存重写的过程中,如果有其他并发的请求也响应过来,此时其他请求由于没有拿到互斥锁会造成线程的休眠,一直等到拿到互斥锁为止

/*
解决缓存击穿的代码实现
 */
public Shop getShopWithCacheBreakDown(Long id){
    //1.先根据商品id去缓存当中查询
    String shopJSON = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);  //得到shop对象的序列化数据

    //2.1 如果缓存中存在且为非空,则直接将这个信息返回
    if(StrUtil.isNotBlank(shopJSON)){
        //将对象序列化后返回给客户端
        return JSON.parseObject(shopJSON,Shop.class);
    }

    //执行到这就说明shopJSON肯定不为空,所以此时只要shopJSON也不为null就符合""的情况
    //2.2 如果缓存当中存在并且为""的,那么需要返回错误
    if("".equals(shopJSON)){
        return null;
    }
    Shop shop = null;

    try {
        //3.缓存当中不存在,判断是否拿到了互斥锁
        if(!lockShop(id)){
            //4.未拿到互斥锁,则让当前线程休眠
            Thread.sleep(200);

            //休眠后重新执行当前逻辑
            return getShopWithCacheBreakDown(id);
        }

        //5.拿到互斥锁了之后,去数据库当中查找数据
        shop = getById(id);
        Thread.sleep(200);
        //6.数据库当中的数据不存在,将一个空值存入到redis当中
        if( shop == null){
            stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id ,"", RedisConstants.SHOP_NULL_TTL , TimeUnit.MINUTES);
            return null;
        }

        //5.数据库当中的数据存在,则将这个对象添加到缓存当中
        //5.1将对象序列化为JSON类型的数据
        String shopMysqlJSON = JSON.toJSONString(shop);

        //5.2将序列化后的数据添加到redis中
        stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id ,shopMysqlJSON , RedisConstants.SHOP_TTL , TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        //释放锁
        unlockShop(id);
    }

设置逻辑过期时间

如果缓存不存在,就说明当前热点的key已经不存在了,因为热点key只是物理层面有expiretime表示过期时间,他在redis中不会真的消失,如果命中缓存了,才需要对expireTime判断是否过期了,如果未过期,则直接将当前获取到的信息返回就好了,但是如果过期了,则需要开启一个新的线程执行缓存重建的操作,当前线程直接返回这个旧数据就好了

/*
利用逻辑过期解决缓存击穿的问题
 */
public <R,T> R queryWithLogicalExpire(String keyPrefix, T id, Class<R> type, Function<T,R> dbFallBack,Long ttl, TimeUnit timeUnit){
    //1.在缓存中查找数据
    String key = keyPrefix + id;
    String json = stringRedisTemplate.opsForValue().get(key);

    //2.如果缓存未命中,返回null
    if(StrUtil.isBlank(json)){
        return null;
    }

    //3.如果缓存命中,判断逻辑时间是否过期
    RedisData redisData = JSON.parseObject(json, RedisData.class); //先转为RedisData对象
    LocalDateTime expireTime = redisData.getExpireTime();
    R result = JSON.parseObject(redisData.getData().toString(), type);

    //4.如果逻辑没有超时,则直接将旧数据返回
    if(expireTime.isAfter(LocalDateTime.now())){
        return result;
    }

    String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
    //5.如果逻辑没有过期,则去更新逻辑失效时间,首先需要获取互斥锁
    if(lock(lockKey)){
        //6.获取到锁,进行缓存重建
        R apply = dbFallBack.apply(id);
        try {
            //6.2缓存中没有数据再执行缓存重建的操作,开启独立线程,在独立线程中执行缓存重建的操作
            CACHE_REBUILD_EXECUTOR.submit(()->{
               this.setWithLogicalExpire(key,apply,ttl,timeUnit);
            });
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //6.3释放锁
            unlockShop(key);
        }
    }

    //7.直接返回旧数据
    return result;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值