前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >万字解析Redis的三大主流问题及解决方案

万字解析Redis的三大主流问题及解决方案

作者头像
萌萌哒的瓤瓤
发布2021-01-13 17:54:14
7110
发布2021-01-13 17:54:14
举报

查询数据的流程

要了解下面的内容,我们首先需要了解我们完整的查询操作是一个什么样点的流程.

我们通过下面的图来让大家更加清晰的了解:

在这里插入图片描述
在这里插入图片描述

了解完这个基本的数据流程之后,我们就可以继续来了解下面的内容了.

1.Redis的常见问题:

我们首先先来了解一下这三者分别代表了什么意思.

1.1-缓存穿透

在这里插入图片描述
在这里插入图片描述

缓存穿透指的是用户持续访问了一个数据库中根本就没有的数据,使得大量这样的访问直接怼到了数据库上,使得数据库最后直接崩掉.

这时候可能有朋友要问了,既然没有没有那为啥要查数据库呢?

在这里插入图片描述
在这里插入图片描述

别笑,可能还真有朋友会问,至少刚接触计算机时候的我可能还真会问.

我们是怎么知道数据库没有这条数据的呢,很明显是我们已经查询数据库之后才知道的,并且一般我们的查询都是对数据库中的数据进行全表查询之后再返回结果的,这种查询是特别消耗时间和性能的.

那么有人又要问了,既然都知道数据库里面没有这条数据了,那之后的请求为什么还要去查询数据库呢?

在这里插入图片描述
在这里插入图片描述

这里主要是因为第一次查询的时候,数据库里面没有这条数据,所以我们无法将数据填充到缓存中,缓存中没有,那么就只能再去数据库里面找了,主要问题就是出在下面红框内的步骤:

在这里插入图片描述
在这里插入图片描述

想想我们之前的操作,很明显都是数据库里面查出相应的数据之后,我们才把相应的数据存储到缓存中,那样我们之后才能直接读取缓存,然后返回我们的结果,但是数据库中都没有的数据显然缓存中肯定也没有,所以之后的请求就全部都怼到数据库上导致数据库的崩溃.

我们也可以通过下面的图来理解:

在这里插入图片描述
在这里插入图片描述

缓存穿透一般是黑客或不法分子利用Redis与数据库的数据漏洞进行 集中一点,连续攻击 ,从而使得我们的数据库服务直接崩溃的异常.

1.2-缓存击穿

在这里插入图片描述
在这里插入图片描述

缓存击穿指的是,由于各种原因导致Redis中的一个热点Key ( 目前访问人数较多的数据可以理解成 微博热搜 ) 失效了,这样突然大量的访问就直接又怼到了数据库上,导致数据库也是直接就崩掉了.

我们也可以通过下面的图来理解:

在这里插入图片描述
在这里插入图片描述

这种情况发生的原因有很多种,有可能是网络的原因,有可能是服务器本身的原因,也有可能是Redis本身服务的原因,反正原因多种多样.

其实这种问题可能是离我们生活最近的,就比方说 微博又炸了:

在这里插入图片描述
在这里插入图片描述

这种情况一般就是由于各种各样的原因,缓存中关于热搜的数据没了,没了没事, 只要现在访问该热搜的请求数量一般或者当前分批次的将这些请求分发过来也就没事了 ,但是想一想微博热搜的访问一般都是直接百万级别的,关键是这种请求又基本是 同一时间点怼到数据库上.

百万级别的请求直接怼到数据库上,这就好比马保国跟普通群众比赛一样,很明显就只有一个结果:

在这里插入图片描述
在这里插入图片描述

那肯定就是当场就歇逼了呗.

1.3-缓存雪崩

在这里插入图片描述
在这里插入图片描述

缓存雪崩指的是缓存中的 大量数据在同一个时间段内失效 ,导致大量对于这部分数据的访问直接怼到了数据库上,导致数据库直接就崩掉了.

我们也可以通过下面的图来理解:

在这里插入图片描述
在这里插入图片描述

这种情况一般都是因为设置的Redis中的缓存数据的过期时间是一样的,导致同一时间点大部分的缓存数据直接过期,这样对于这部分的数据访问肯定又是直接怼到数据库上了.数据库又崩了.

数据库只能说我好难,为什么都欺负我,嘤嘤嘤.

在这里插入图片描述
在这里插入图片描述

了解完三者的概念之后,我们可以横向对比一下三者:

在这里插入图片描述
在这里插入图片描述

2.三种问题相应的解决方案

2.1-缓存穿透解决方案

了解完上述关于缓存穿透的概念之后我们就知道了只要问题就出在数据库无法将不存在的数据存储到Redis中,导致Redis中一直没有该数据,使得关于该数据的访问全部都是直接怼到数据库上,最后导致数据库崩溃.

既然这样,我们就将该数据存储到Redis里面,这样对于该数据的访问就又重新怼到Redis上面了,但是我们要注意这条数据既然不存在,那么我们就将该数据定义为空,并且要 给它设置过期时间,并且这种国旗时间不要设置的太长,20-30秒即可,否则这种无用的数据一致存储在Redis里面,也是浪费.

代码语言:javascript
复制
public PmsSkuInfo selectBySkuId(Integer skuId) {
        PmsSkuInfo pmsSkuInfo=new PmsSkuInfo();
        //连接缓存
        Jedis jedis=redisUtil.getJedis();
        //查询缓存
        String skuKey="sku:"+skuId+":info";
        String skuJson=jedis.get(skuKey);
        //缓存不为空
        if(StringUtils.isNotBlank(skuJson)){
            //通过fastjson将我们的字符串转化成我们对应的Sku对象
            pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class);
        }
        else{
            //如果缓存没有,查询mysql
            pmsSkuInfo=selectBySkuIdFromDB(skuId);
            //mysql查询结果存储到Redis
            if(pmsSkuInfo!=null){
                jedis.set("sku:"+skuId+":info", JSON.toJSONString(pmsSkuInfo));
            }
            else{
                //数据库中同样也不存在该数据
                //将空值设置给该Key,并且设置30秒的过期时间
                jedis.setex("sku:"+skuId+":info",30,JSON.toJSONString(""));
            }
        }
        jedis.close();
        return pmsSkuInfo;
    }

2.2-缓存击穿解决方案

缓存击穿的解决方案就比较复杂了,不像缓存穿透和缓存雪崩一样,只需要设置相应的过期时间或者是将数据存进Redis即可解决.

缓存击穿的解决方案相应的就比较多,主要有两种:

  • Redis自身的分布式锁实现
  • 通过redisson框架实现

接下来我们分别讲一下两者的实现方式:

  • Redis自身的分布式锁

缓存击穿的特殊性就在于是一个热点数据突然失效,导致大规模的请求直接怼到数据库上,这其中的重点就是一条热搜数据,大规模的请求同一时间点怼到数据库.

所以分布式锁的思想就是, 每次向Redis请求数据的时候,都在Redis里面给该条数据上锁,一旦锁设置成功,那么就只有当前的进程可以进入到数据库中进行查询,并且查询完成之后就 将该数据重新存到Redis之中,数据存储成功之后 再释放掉该锁,在此之前其他锁没有设置成功的进程就 只能自旋等待锁被释放为止.

因为第一条请求结束之后,Redis中就已经重新有了该热点数据的缓存,所以之前自旋的进程就可以 直接从Redis中拿到该热点数据,不用再去访问数据库了,这样就极大的降低了数据库的压力.

我们也可以通过下面的思维导图来帮助大家理解:

在这里插入图片描述
在这里插入图片描述

下面是一个小的Demo:

代码语言:javascript
复制
public PmsSkuInfo selectBySkuId(Integer skuId,String ip) {
        System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName());
        PmsSkuInfo pmsSkuInfo=new PmsSkuInfo();
        //连接缓存
        Jedis jedis=redisUtil.getJedis();
        //查询缓存
        String skuKey="sku:"+skuId+":info";
        String skuJson=jedis.get(skuKey);
        //缓存不为空
        if(StringUtils.isNotBlank(skuJson)){
            System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"已经成功拿到缓存中的数据");
            //通过fastjson将我们的字符串转化成我们对应的Sku对象
            pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class);
        }
        else{
            System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"开始申请分布式锁:"+"sku:"+skuId+":lock");
            //如果缓存没有,查询mysql
            //设置分布式锁,避免缓存击穿
            String OK=jedis.set("sku:"+skuId+":lock","1","nx","px",10*1000);
            if(StringUtils.isNotBlank(OK)&&OK.equals("OK")){
                System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"已经申请到分布式锁:"+"sku:"+skuId+":lock"+"过期时间为10秒");
                pmsSkuInfo=selectBySkuIdFromDB(skuId);

                try {
                    Thread.sleep(1000*7);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //mysql查询结果存储到Redis
                if(pmsSkuInfo!=null){
                    //过期时间随机避免缓存雪崩
                    jedis.setex("sku:"+skuId+":info", (int) (10*Math.random()*10),JSON.toJSONString(pmsSkuInfo));
                }
                else{
                    //数据库中同样也不存在该数据,也传到Redis中,避免缓存穿透
                    jedis.setex("sku:"+skuId+":info",30,JSON.toJSONString(""));
                }
                System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"使用完毕了,释放了分布式锁:"+"sku:"+skuId+":lock");
                //在访问mysql之后,需要将分布式锁释放掉
                jedis.del("sku:"+skuId+":lock");
            }
            else{
                System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"没有申请到分布式锁:"+"sku:"+skuId+":lock"+"已经开始自旋");
                try {
                    //进程休眠几秒之后,开始自旋
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //开始自旋
                return selectBySkuId(skuId,ip);
            }
        }
        jedis.close();
        return pmsSkuInfo;
    }

可以看到这是测试结果:

在这里插入图片描述
在这里插入图片描述
  • redisson框架 引入Redisson框架之后,我们就不用上面使用Redis的分布式锁那么繁琐.直接几行代码就能搞定. 首先我们需要先引入Redisson框架所需要的依赖
代码语言:javascript
复制
<dependency>
	     <groupId>org.redisson</groupId>
	     <artifactId>redisson</artifactId>
	     <version>3.10.5</version>
</dependency>

之后我们就需要配置Redisson框架的配置信息

代码语言:javascript
复制
@Configuration
public class RedissonConfig {
    //读取配置文件中的redis的ip地址.端口号,数据库,密码

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.database}")
    private int database;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        //链式编程
        config.useSingleServer().setAddress("redis://" + host + ":" + port)
                                .setPassword(password)
                                .setDatabase(database);
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

这样我们就已经将Redisson引入到我们的Spring容器之中了

之后我们便来编写代码进行测试:

代码语言:javascript
复制
@Controller
public class RedissonController {
    @Autowired
    RedisUtil redisUtil;

    @Autowired
    RedissonClient redissonClient;

    @RequestMapping("/testRedisson")
    @ResponseBody
    public String testRedisson(){
        Jedis jedis=redisUtil.getJedis();
        RLock lock=redissonClient.getLock("lock");//声明锁
        //上锁
        lock.lock();
        try {
            String v=jedis.get("k");
            if(StringUtils.isBlank(v)){
                v="1";
            }
            System.out.println("---->"+v);
            jedis.set("k",(Integer.parseInt(v)+1)+"");
            jedis.close();
        }
        finally {
            //解锁
            lock.unlock();
        }
        return "success";
    }
}

为了模拟高并发,我们通过Apache来进行压力测试(后续我会单独出一篇博客讲解压力测试,主要因为篇幅已经很长了)

在这里插入图片描述
在这里插入图片描述

之后我们再来分别看看三个程序打印的结果: 8071端口:

在这里插入图片描述
在这里插入图片描述

8072端口:

在这里插入图片描述
在这里插入图片描述

8073端口:

在这里插入图片描述
在这里插入图片描述

可以很明显的看到数据没有重复,的确已经实现了安全性.

2.3-缓存雪崩解决方案

了解完上述的缓存雪崩的概念之后,解决办法就比较简单了,既然是因为数据的过期时间都是一样的才导致数据同时失效,那么我们就可以通过 将数据的过期时间设置成随机的 ,这样就会在极大程度上减少大量数据同时过期的情况.

举个例子,可以通过下面的方法来实现:

代码语言:javascript
复制
public PmsSkuInfo selectBySkuId(Integer skuId) {
        PmsSkuInfo pmsSkuInfo=new PmsSkuInfo();
        //连接缓存
        Jedis jedis=redisUtil.getJedis();
        //查询缓存
        String skuKey="sku:"+skuId+":info";
        String skuJson=jedis.get(skuKey);
        //缓存不为空
        if(StringUtils.isNotBlank(skuJson)){
            //通过fastjson将我们的字符串转化成我们对应的Sku对象
            pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class);
        }
        else{
            //如果缓存没有,查询mysql
            pmsSkuInfo=selectBySkuIdFromDB(skuId);
            //mysql查询结果存储到Redis
            if(pmsSkuInfo!=null){
            //设置随机过期时间
                jedis.setex("sku:"+skuId+":info", (int) (10*Math.random()*10),JSON.toJSONString(pmsSkuInfo));
            }
            else{
                //数据库中同样也不存在该数据
                jedis.setex("sku:"+skuId+":info",30,JSON.toJSONString(""));
            }
        }
        jedis.close();
        return pmsSkuInfo;
    }
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-12-02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 查询数据的流程
  • 1.Redis的常见问题:
    • 1.1-缓存穿透
      • 1.2-缓存击穿
        • 1.3-缓存雪崩
        • 2.三种问题相应的解决方案
          • 2.1-缓存穿透解决方案
            • 2.2-缓存击穿解决方案
              • 2.3-缓存雪崩解决方案
              相关产品与服务
              云数据库 Redis
              腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档