redis——缓存穿透、缓存击穿、缓存雪崩、分布式锁

redis应用问题解决

缓存穿透

什么是缓存穿透?

可以参考下图,当客户端发送读的请求过来时,会先访问缓存中的数据,如果不存在则直接去访问MySQL服务器中的数据。这时候如果MySQL服务器中并不存在他请求对应的信息,请求就会反反复复一直访问MySQL服务器,黑客利用此漏洞进行攻击可能压垮数据库。

在这里插入图片描述

解决方案

  • 方案一:缓存空值

    如果MySQL服务器中不存在相对应的数据,可以将对应的key的value值设置为空,当请求再次访问时可以直接去缓存中读取空值

  • 方案二:布隆过滤器

    在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。

缓存击穿

什么是缓存击穿?

比如微博的热点事件,大家都在同一时间点访问这个请求,就可能造成服务器崩溃的情况,就好像子弹打在墙上一样,始终往一个地方大,迟早打穿这堵墙

解决方案

  • 方案一:提前设置预热数据

    在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长

  • 方案二:使用分布式锁

    类似于Java中的锁机制一样,一个时间只有一个用户可以访问,当这个用户访问完之后,解锁了,其他用户才可以开始访问,这个在后面也会详细展开讲解

缓存雪崩

什么时候缓存雪崩?

举个栗子,在双十一的时候,非常多的用户访问非常多的请求,我们虽然提前做了缓存,但在一定时间后这些缓存同时失效,那就会有一大批的请求直接访问MySQL数据库,给服务器带来巨大的压力

解决方案

  • 方案一:构建多级缓存架构

  • 方案二:设置过期标志更新缓存

    记录缓存过期时间,快到过期时间时触发另外一个线程更新过期时间

  • 方案三:将缓存失效时间分散开

    比如A请求的失效时间为10分钟后,B请求的失效时间可以设置为10分01秒后,依次类推,即使过期了,请求直接访问时也可以错开时间点

分布式锁

业务需求

我们上述说到的解决缓存击穿的方案,可以使用分布式锁,让请求一个一个访问。随着时代的发展,现在我们使用redis已经不是单单的一台服务器了, 我们会建一个集群,但是锁这个东西他是不能横跨服务器的,这种情况redis也提供了解决方案

解决方案

  1. 使用setnx命令

    我们都知道setnx命令如果这个key不存在的话可以对其value进行设置,但key存在时是不允许设置的,我们就可以利用这一点,将此key作为锁。

  2. 设置过期时间

    就好像有一个人去上厕所,外面排着长队,结果这个人上着上着突然睡着了,但是外面的人就无法使用。所以我们要将锁设置一定的过期时间,如果长时间没有完成就要自动释放锁

  3. 具体命令

    # set key value nx ex second : 其中nx等同于setnx,ex设置过期时间单位为秒
    set k2 v2 nx ex 10
    

Java中使用

@GetMapping("/testLock")
public void testLock(){
    
    
    //1.获取锁
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent("kkk", "vvv", 10, TimeUnit.SECONDS);
    //2.操作数据
    if (isLock){
    
    
        //查询key为num的数值
        Object value = redisTemplate.opsForValue().get("num");
        if (StringUtils.isEmpty(value)){
    
    
            return;
        }
        //把值转换为数字并加1
        int i = Integer.parseInt(value + "");
        redisTemplate.opsForValue().set("num",i+1);
        //释放锁
        redisTemplate.delete("kkk");
    }else {
    
    
        //3.获取锁失败,每个0.1秒再获取
        try {
    
    
            Thread.sleep(100);
            testLock();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }
}

设置num的值为0,使用ab工具进行多次访问,如果锁有效的话,1000个请求num的值应该为1000

ab -n 1000 -c 100 http://192.168.0.10:8080/redisTest/testLock
127.0.0.1:6379> get num
"1000"

问题解决

其实上述案例中还是有那么一些些问题的,需要我们来梳理一下

问题一

如下图所示,A请求在执行操作的过程中宕机了,但是key的过期时间已到,B请求获取到了锁并加上了锁,B在执行操作的过程中,A相应过来了,手动释放了锁,这时其他的请求又会一起挤进来,可能会出现两个请求操作一个数据的情况

解决方案:使用UUID作为value值,防止误删除

在这里插入图片描述

@GetMapping("/testLock")
public void testLock(){
    
    
    String uuid = UUID.randomUUID().toString();
    //1.获取锁
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent("kkk", "uuid", 10, TimeUnit.SECONDS);
    //2.操作数据
    if (isLock){
    
    
        //查询key为num的数值
        Object value = redisTemplate.opsForValue().get("num");
        if (StringUtils.isEmpty(value)){
    
    
            return;
        }
        //把值转换为数字并加1
        int i = Integer.parseInt(value + "");
        redisTemplate.opsForValue().set("num",i+1);
        //释放锁
        //判断是否为自己的锁
        String keyForUuid = (String) redisTemplate.opsForValue().get("kkk");
        if (uuid.equals(keyForUuid)){
    
    
            redisTemplate.delete("kkk");
        }
    }else {
    
    
        //3.获取锁失败,每个0.1秒再获取
        try {
    
    
            Thread.sleep(100);
            testLock();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }
}

问题二

如下入、图所示,A在释放锁的过程中,先判断了uuid是相同的,正准备删除时,锁的过期时间到了,自动删除后,B获取到了锁并加上了锁,A再删除了这个锁

解决方案:使用LUA脚本保证删除的原子性

在这里插入图片描述

@GetMapping("/testLock")
public void testLock(){
    
    
    String uuid = UUID.randomUUID().toString();
    //1.获取锁
    Boolean isLock = redisTemplate.opsForValue().setIfAbsent("kkk", "uuid", 10, TimeUnit.SECONDS);
    //2.操作数据
    if (isLock){
    
    
        //查询key为num的数值
        Object value = redisTemplate.opsForValue().get("num");
        if (StringUtils.isEmpty(value)){
    
    
            return;
        }
        //把值转换为数字并加1
        int i = Integer.parseInt(value + "");
        redisTemplate.opsForValue().set("num",i+1);

        // 定义lua 脚本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 使用redis执行lua执行
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        // 设置一下返回值类型 为Long
        // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
        // 那么返回字符串与0 会有发生错误。
        redisScript.setResultType(Long.class);
        // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
        redisTemplate.execute(redisScript, Arrays.asList("kkk"), uuid);
        
    }else {
    
    
        //3.获取锁失败,每个0.1秒再获取
        try {
    
    
            Thread.sleep(100);
            testLock();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }
}

Guess you like

Origin blog.csdn.net/Yellow_Star___/article/details/122496108