redis缓存穿透,缓存击穿,缓存雪崩区别及解决方案

前言

工作中,我们经常使用缓存来提高查询速度,减轻数据库的压力,不让过多的流量压垮数据库,所以缓存也成了我们系统重要的环节之一,而缓存最常见的问题是缓存穿透、击穿和雪崩,在高并发下,缓存的这三个问题会导致大量的请求落到数据库,导致数据库的最大连接数占满,导致数据库故障,引发我们系统一系列故障。今天我们就来分析一下缓存这三种缓存问题有什么区别?

缓存穿透

缓存穿透指的是数据库查询一个不存在的数据,而我们的程序是只有数据库有数据时,我们才将数据存入缓存,这样会导致如果有人利用这个漏洞频繁访问这个数据值去查询,大量的请求打到数据库上可能会导致数据库挂掉,这就失去了缓存的意义

解决方案

  1. 一种简单的解决方案是:我们可以短暂的将这种不存在的数据缓存起来,来防止这种恶意请求频繁打击数据库,将缓存设置过期时间30秒,或更短,确保改key不会过长的占用内存
  2. 上述方案我们可能会因为大量的这种数据而占用很大的内存,而且如果同时过期的话也会有缓存雪崩的问题,我们还可以利用布隆过滤器来进行优化,这里就不具体阐述布隆过滤器了,感兴趣的小伙伴可以百度一下布隆过滤器

缓存击穿

例子

本人在工作中就碰到缓存击穿的问题,我们电商app的首页往往会有很多的数据信息,这些数据信息从数据库取出来存入缓存中,但是客服突然返回我们的app有那么一段时间首页卡顿,用户体验不好,经过检查,有一段时间缓存失效,导致大量请求访问数据库,造成数据库卡顿。

描述

大家估计从上面的例子也能体会到了什么是缓存击穿,其实就是一个热点数据失效之后,在我们还没有重新加载缓存时,大量请求访问数据库,导致数据库奔溃

解决方案

  1. 将热点数据设置为永不过期
  2. 在缓存即将过期时,或者缓存过期时,加互斥锁访问数据库,保证只有一个线程访问到数据库,下面是分布式加锁代码
        public static final String LOCK_PREFIX = "redis_lock_";
        //加锁失效时间,毫秒
        public static final int LOCK_EXPIRE = 3000; // ms
        public boolean lock(String key){
            String lock = LOCK_PREFIX + key;
            // 利用lambda表达式
            return (Boolean) redisTemplate.execute((RedisCallback) connection -> {
    
                //设置redis锁过期时间
                long expireAt = System.currentTimeMillis() + LOCK_EXPIRE + 1;
                Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt).getBytes());
    
                //获取锁成功
                if (acquire) {
                    return true;
                } else {
    
                    byte[] value = connection.get(lock.getBytes());
    
                    if (Objects.nonNull(value) && value.length > 0) {
    
                        long expireTime = Long.parseLong(new String(value));
                        // 如果锁已经过期
                        if (expireTime < System.currentTimeMillis()) {
                            // 重新加锁,防止死锁,返回之前的值
                            byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(System.currentTimeMillis() + LOCK_EXPIRE + 1).getBytes());
                            //当前时间大于之前锁的时间,则代表获取到锁
                            return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
                        }
                    }
                }
                return false;
            });
        }
    
        /**
         * 删除锁
         *
         * @param key
         */
        public void deleteLock(String key) {
            redisTemplate.delete(LOCK_PREFIX + key);
        }

     设置缓存

        //缓存的key
        private String key = "testBreakdown";
        //设置超时事件2分钟
        public static final long timeout = 120;
        public String textRedisBreakdown(){
            Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
            //小于60秒
            if(expire>0&&expire<60){
                if(lock(key)){
                    System.out.println("预刷新缓存,获取到锁");
                    //获取到锁了
                    try {
                        System.out.println("操作数据库存入缓存");
                        redisTemplate.opsForValue().set(key, "11", timeout, TimeUnit.SECONDS);
                    }catch (Exception e){
                        System.out.println("异常出错");
                    }finally {
                        System.out.println("最终都释放锁");
                        deleteLock(key);
                    }
                }
                //去获取缓存的值
                textRedisBreakdown();
            }else if(expire<0){
                //没有值
                if(lock(key)){
                    System.out.println("获取到锁");
                    //获取到锁了
                    try {
                        System.out.println("操作数据库存入缓存");
                        redisTemplate.opsForValue().set(key, "11", timeout, TimeUnit.SECONDS);
                    }catch (Exception e){
                        System.out.println("异常出错");
                    }finally {
                        System.out.println("最终都释放锁");
                        deleteLock(key);
                    }
                }
                //这里也可以写else使用Thead.sleep()来等待数据库和业务处理的事件,这里各位道友仁者见仁智者见智了
                textRedisBreakdown();
            }
    
            return redisTemplate.opsForValue().get(key).toString();
        }

    测试返回的结果 

    缓存快过期情况
    缓存过期的情况
    缓存未过期的情况

     

缓存雪崩

缓存雪崩指的是大量缓存数据统一时刻失效,当用户访问导致大量不同的接口访问数据库,引起数据库压力增大甚至宕机,像淘宝首页的数据都是缓存在数据库,分很多模块,这些模块的数据都是从缓存获取,如果这些缓存同时失效,导致的负面影响是巨大的

解决方案

  1. 设置缓存过期时间的时候,在原有缓存时间的基础上加上随机值,尽量是每个缓存过期时间分布在不同的时间,避免大批量缓存过期的发生
  2. 对与这些数据设置永不过期,但是我们项目中很少这么做,谁也不能保证这些数据之后还会不会用,如果不用了会导致这些数据一只占用内存,导致内存的浪费

最后,如果上述表述有问题或者有歧义的地方,请大家评论滴滴我,没有问题就不会有进步,希望大家能带着我进步

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值