讲解Redis缓存穿透,缓存雪崩,缓存击穿以及解决方案

为什么使用缓存?

当我们使用缓存时,目标通常有两个:第一,提升响应效率和并发量;第二,减轻数据库的压力。

主要从“高性能”和“高并发”这两点来看待这个问题,当缓存失效或没有抵挡住流量,流量直接涌入到数据库,在高并发的情况下,可能直接击垮数据库,导致整个系统崩溃。

缓存穿透

大多数情况,缓存可以减少数据库的查询,提升系统性能。

通常流程是:一个请求过来,先查询是否在缓存当中,如果缓存中存在,则直接返回。如果缓存中不存在对应的数据,则检索数据库,如果数据库中存在对应的数据,则更新缓存并返回结果。如果数据库中也不存在对应的数据,则返回空或错误。

缓存穿透(cache penetration)是用户访问的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。

解决方式

在这里插入图片描述

缓存空对象

解决缓存穿透 :缓存NULL值 与布隆过滤器 /添加字段的复杂度/对字段进行格式校验
* 可以将没有在redis 和mysql 中查询到的数据 返回为空字符串,如果之后sql新增数据需要加入redis ,所以空字符串要设置过期时间(短)
* 如果redis 为空字符串直接返回 不查询数据库,
@Autowired
    private StringRedisTemplate redis;

    @Autowired
    private  ShopMapper mapper;

    @Override
    public Shop   getShopById(int id) {

         String shop =(String) redis.opsForHash().get("shop", "" + id);

        if(shop==null){
      //数据库中查找数据
         Shop   byId = getById(id);
     //写人缓存
      if(byId!=null){
         redis.opsForHash().put("shop",""+id,JSONUtil.toJsonStr(byId));
          }else{
// 最好是使用String 类型而不是Hash,因为过期是hash对象过期,而不是单条数据
      redis.opsForHash().put("shop",""+id,"nullobj");
      redis.expire("shop",过期时间,TimeUnit.MINUTES);
           }
          
        }
       if(shop.equals("nullobj")){
         return  null;
        }else{

        return   JSONUtil.toBean(  shop , Shop.class);

   }
    

使用布隆过滤器

在cn.hutool的包下有一个封装好的AbstractFilter,他实现了BloomFilter这个接口,里面包含两个方法,一个判断是否包含指定值的boolean contains(String var1)方法,一个添加值boolean add(String var1);的方法,通过这两个方法,我们就可以对布隆过滤器进行添加id和判断id是否存在。对应的包如下:

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.2</version>
</dependency>
 

缓存雪崩

 缓存雪崩是指在使用缓存系统时,缓存中的大量数据同时失效或者缓存系统发生故障,导致大量的请求直接访问后端存储系统,从而给后端系统造成巨大的负载压力,使得后端系统性能降低甚至瘫痪。

缓存系统通常会设置缓存数据的过期时间,为了提高性能,缓存数据的过期时间可以设置得较短,从而保持缓存中的数据与后端存储系统中数据的一致性。然而,当缓存中大量的数据在同一时间失效,或者缓存系统出现故障而无法提供服务时,所有的请求将直接访问后端存储系统,导致后端系统瞬时承受巨大的负载压力。

当缓存中大量热点缓存采用了相同的实效时间,就会导致缓存在某一个时刻同时实效,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机。从而形成一系列的连锁反应,造成系统崩溃等情况,这就是缓存雪崩(Cache Avalanche)。

解决方案

大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案,就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

  @Autowired
    private StringRedisTemplate redis;

    @Autowired
    private  ShopMapper mapper;

    @Override
    public shop getShopById(int id) {

        String shop =(String) redis.opsForHash().get("shopdata", ""+ id);

        if(shop!=null){
            //字符串->对象
            redisData shopmax = JSONUtil.toBean(shop, redisData.class);
            Shop shop1 = JSONUtil.toBean((JSONObject) shopmax.getObj(), Shop.class);

           return  shop1;
           }
        }else{


//            Shop getshopid = mapper.getshopid(id);
//            if(getshopid==null){
                return  Result.fail("店铺不存在");
//            }else{
//
//                //在写入缓存前判断是否加锁
//
//                //如果有锁休眠一段时间,递归方法
//                //如果没有加锁,查数据库,写入缓存
//                redis.opsForHash().put("shop",""+id,JSONUtil.toJsonStr(getshopid));
//                redis.opsForHash().getOperations().expire("shop",30L,TimeUnit.MINUTES);
//                return Result.ok(getshopid);
//            }
//            // 释放互斥锁
  
        }
    }

    //加锁
    public boolean getlock(String key){
      
            // 获取锁成功,设置锁的过期时间
        Boolean expire = redis.expire(key, 过期时间, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(expire);
    }

// 释放锁
    public void releaseLock(String key) {

        redis.delete("lock"+key);
    }


缓存击穿

从定义上可以看出,缓存击穿和缓存雪崩很类似,只不过是缓存击穿是一个热点key失效,而缓存雪崩是大量热点key失效。因此,可以将缓存击穿看作是缓存雪崩的一个子集。

缓存击穿的解决方案:

使用互斥锁(Mutex Key),只让一个线程构建缓存,其他线程等待构建缓存执行完毕,重新从缓存中获取数据。单机通过synchronized或lock来处理,分布式环境采用分布式锁。

利用逻辑过期。

逻辑过期
 

@Autowired
    private StringRedisTemplate redis;

    @Autowired
    private  ShopMapper mapper;

 public Result getShopById(int id) {

 
        String shop =(String) redis.opsForHash().get("shopdata", ""+ id);

        if(shop!=null){
            //字符串->对象
            redisData shopmax = JSONUtil.toBean(shop, redisData.class);
            Shop shop1 = JSONUtil.toBean((JSONObject) shopmax.getObj(), Shop.class);

            //判断该时间是否过期
           if(shopmax.getDatetime().isAfter(LocalDateTime.now())){

               return  Result.ok(shop1);

           }else{

               //在写入缓存前判断是否加锁

               //如果有锁休眠一段时间,递归方法
               //如果没有加锁,查数据库,写入缓存
               addredis(id);
               // 释放互斥锁
           }
            return  Result.ok(shop1);
        }else{
 
                return  Result.fail("店铺不存在");


        }
    }




//封装数据设置逻辑过期时间
    public void addredis(int id){
             Shop   byId = getById(id);
            redisData data=new redisData();
            data.setObj(byId);
            data.setDatetime(LocalDateTime.now().plusSeconds(300));
            redis.opsForHash().put("shopdata",""+id,JSONUtil.toJsonStr(data));



    }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值