小黄学redis|redis——缓存穿透、缓存击穿、缓存雪崩、分布式锁

redis应用问题解决 缓存穿透
什么是缓存穿透?
可以参考下图,当客户端发送读的请求过来时,会先访问缓存中的数据,如果不存在则直接去访问MySQL服务器中的数据。这时候如果MySQL服务器中并不存在他请求对应的信息,请求就会反反复复一直访问MySQL服务器,黑客利用此漏洞进行攻击可能压垮数据库。
小黄学redis|redis——缓存穿透、缓存击穿、缓存雪崩、分布式锁
文章图片

解决方案

  • 方案一:缓存空值
    如果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. 【小黄学redis|redis——缓存穿透、缓存击穿、缓存雪崩、分布式锁】具体命令
    # 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 = https://www.it610.com/article/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值,防止误删除
小黄学redis|redis——缓存穿透、缓存击穿、缓存雪崩、分布式锁
文章图片

@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 = https://www.it610.com/article/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脚本保证删除的原子性
小黄学redis|redis——缓存穿透、缓存击穿、缓存雪崩、分布式锁
文章图片

@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 = https://www.it610.com/article/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 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(); } } }

    推荐阅读