• 缓存穿透、缓存击穿、缓存雪崩


    1 缓存穿透

    (1)什么是缓存穿透

    缓存穿透是指缓存和数据库中的没有的数据,而用户不断发起请求,由于缓存时不命中时被动写入的,处于容错考虑,如果从缓存查不到的数据则不写入缓存,这将会导致不存再的数据每次请求都要到存储层(DAO和SQL)去查询,这样缓存就是去意义了。再流量大的时候,SQL数据库的压力就太大了。如果有人利用不存再的key频繁攻击我们的应用,这就是性能漏洞。

    例如发起id为”-1“的数据请求,这些数据时不存在的,用户如果时故意为之,我们的系统就会遭受攻击。

    (2)解决方法

    接口层增加校验,例如:用户权限校验,id做范围校验,id<=0的请求直接拦截。

    从缓存取不到数据时,如果在数据库也没有渠道,这时可以将key-value对写为key-null,缓存有效时间可以设置短一些,如30秒(缓存时间太长会导致正常情况也无法使用),这也是可以方式黑客利用缓存穿透进行攻击的有效手段。

    2 缓存击穿

    2.1 什么是缓存击穿

    一个存在value的key,在缓存创建(或过期)的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大,压力骤增。缓存击穿只会发生在高并发的时候,就是当有10000 个并发进行查询数据的时候,我们一般都会先去redis里面查询进行数据,但是如果redis里面没有这个数据的时候,那么这10000个并发里面就会有很大一部分并发会一下子都去mysql数据库里面进行查询了。这样给数据库带来巨大的压力。

    例如以下示例:

    (1) 业务类代码

        @Override
        public Category queryById(int id) {
            objRedisTemplate.setKeySerializer(RedisSerializer.string());
            Category category = (Category) objRedisTemplate.opsForValue().get("category-"+id);
            if(category==null) {
                //双重检查没有才去SQL数据库查
                System.out.println("从SQL数据库读取...");
                category = categoryMapper.selectById(id);
                objRedisTemplate.opsForValue().set("category-"+id, category, 5, TimeUnit.MINUTES);
            }else{
                System.out.println("从Redis缓存读取...");
            }
            return category;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (2)控制器代码模拟同时1000个并发请求,导致缓存击穿

        //模拟缓存击穿
        @RequestMapping("/breakdown")
        @ResponseBody
        public String breakdown(Integer id){
            //初始化线程池执行器
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            //利用线程池模拟并发读取1000次
            for(int i=0; i<1000; i++){
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        Category category = categoryService.queryById(id);
                    }
                });
            }
            executorService.shutdown();
            return "success";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    印结果打印了很多查询数据库和查询缓存,此时也就说明10000个并发里面有很多去查询了数据库,这就是高并发引起的问题。也就是缓存击穿。我们怎么解决缓存击穿呢,即使10000个并发过来,然后这10000个并发需要的数据在redis里面都没有,那么我们应该第一个线程查询数据里面的数据,然后把这个数据给放到redis里面,然后剩下的9999个线程都到redis里面查询,这样就解决了缓存击穿。

    2.2 使用双重锁检查

    使用锁来解决缓存击穿问题,而且叫做双重检测锁,为什么叫做双重检测锁呢,因为有两个if语句,第一个if语句就是为了减少走同步代码块,因为如果换成里面存在想要的数据,就直接获取,所以有两个if语句。第一次线程查询把数据放到redis缓存之后,剩下的线程当走到下面的同步代码块的时候,需要在查询一下缓存里面的数据就会发现刚刚第一个线程放到redis里面的数据了。

        @Override
        public Category queryById(int id) {
    
            redisTemplate.setKeySerializer(RedisSerializer.string());
            //使用双重检查,防止缓存被击穿
            //第一次检查
            Category category = (Category) redisTemplate.opsForValue().get("category-"+id);
            if(category==null) {
                //加同步锁
                synchronized (this){
                    //加锁后再次检查
                    category = (Category) redisTemplate.opsForValue().get("category-"+id);
                    if(category==null) {
                        //双重检查没有才去SQL数据库查
                        System.out.println("从SQL数据库读取...");
                        category = categoryMapper.selectById(id);
                        redisTemplate.opsForValue().set("category-"+id, category, 5, TimeUnit.MINUTES);
                    }
                }
            }else{
                System.out.println("从Redis缓存读取...");
            }
            return category;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    3 缓存雪崩

    缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

    解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

    4完整代码

    @Override
    public  Category queryOneWithString(int id) {
        redisTemplate.setKeySerializer(RedisSerializer.string());
        Category category = (Category) redisTemplate.opsForValue().get("category");
        /*解决缓存穿透问题,存 null 改变 判断方式*/
        if (redisTemplate.hasKey("category")){   //第一次检查 第一步解决缓存穿透
            /*1 判断 redis中 是否存在数据*/
            System.out.println("从 redis中获取数据");
            return category;
        }else {
            /*解决 缓存 击穿*/
            synchronized (this){
                /*再次 查询 是否 存在数据*/
                if (!redisTemplate.hasKey("category")){    第二次检查  第二步 解决缓存击穿   设置双重锁
                    System.out.println("从mysql中获取数据");
                    category = categoryDao.selectById(id);
                    redisTemplate.opsForValue().set("category",category);
                    if (category == null){
                        redisTemplate.expire("category",30,TimeUnit.SECONDS);//设置空白缓存过期30秒
                    }
                    /*解决缓存雪崩: 设置不同的过期时间 随机*/   //第三步 解决 缓存雪崩 防止大量缓存过期
                    Random random = new Random();
                    int suiji = random.nextInt(10);
                    System.out.println("随机数"+suiji  );
    
                    redisTemplate.expire("category",5+suiji,TimeUnit.MINUTES);
                }else {
                    /*再从redis中获取*/
                    category = (Category) redisTemplate.opsForValue().get("category");
                }
            }
            return  category;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    5 项目代码位置

    E:\09.redis\redis-samplate\001-springboot-redis

    6 笔记代码

    
    import com.powernode.dao.CategoryDao;
    import com.powernode.model.Category;
    import com.powernode.service.CategoryService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    
    /*
    * service:业务逻辑处理,从数据源获取数据,现在有两个数据源
    * mysql:持久化,性能差
    * redis:不擅长持久化,性能极高
    *结合两种数据库的优势
    * */
    @Service
    public class CategoryServiceImpl implements CategoryService {
    
        @Autowired
        private CategoryDao categoryDao;
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /*
        redisTemplate.opsForValue();//操作字符串
    redisTemplate.opsForHash();//操作hash
    redisTemplate.opsForList();//操作list
    redisTemplate.opsForSet();//操作set
    redisTemplate.opsForZSet();//操作有序set
    
    RedisTemplate 和  stringredistemplate
    如果你想使用默认的配置来操作redis,则如果操作的数据是字节数组,
    就是用redistemplate,如果操作的数据是明文,使用stringredistemplate。
    
    opsForHash() 和 boundHashOps
      获取a,然后获取b,然后删除c,对同一个key有多次操作,按照opsForHash()的写法
        每次都是redisTemplate.opsForHash().xxx("key","value")写法很是啰嗦
        int result = (Integer) redisTemplate.opsForHash().get("hash-key","a");
        result = (Integer)redisTemplate.opsForHash().get("hash-key","b");
        redisTemplate.opsForHash().delete("hash-key","c");
    
         * boundHashOps()则是直接将key和boundHashOperations对象进行了绑定,
         * 后续直接通过boundHashOperations对象进行相关操作即可,写法简洁,不需要
         * 每次都显式的将key写出来
        BoundHashOperations boundHashOperations = redisTemplate.boundHashOps("hash-key");
         */
        @Override
        public List<Category> queryAll() {
            redisTemplate.setKeySerializer(RedisSerializer.string());
    
            /*1 判断 redis中 是否存在数据*/
            List<Category> categorys = (List<Category>) redisTemplate.opsForValue().get("categorys");
            /*2存在 则 从redis中 获取数据 返回*/
            if (categorys != null){
                System.out.println("从 redis中获取数据");
                return categorys;
            }else {
                System.out.println("从mysql中获取数据");
                /*3 redis中不存在,1 从mysql中获取数据  2 将数据 存入 redis 用于下次查询时 从redis中获取*/
                List<Category> categories = categoryDao.selectAll();
                redisTemplate.opsForValue().set("categorys",categories,5, TimeUnit.MINUTES);
                return  categories;
            }
        }
    
    
        @Override
        public List<Category> queryAllByHash() {
            redisTemplate.setKeySerializer(RedisSerializer.string());
            /*1 判断 redis中 是否存在数据*/
            List<Category> categorys = (List<Category>) redisTemplate.opsForHash().values("hash:category");
            System.out.println(categorys.size());
            /*2存在 则 从redis中 获取数据 返回*/
            if (categorys.size()>0){
                System.out.println("从 redis中获取数据");
                return categorys;
            }else {
                System.out.println("从mysql中获取数据");
                categorys = categoryDao.selectAll();
                categorys.forEach(x->{
                    redisTemplate.opsForHash().put("hash:category",x.getId(),x);
                });
                redisTemplate.expire("hash:category",5,TimeUnit.MINUTES);
                return  categorys;
            }
        }
    
        @Override
        public Category queryOne(int id) {
            redisTemplate.setKeySerializer(RedisSerializer.string());
            /*1 判断 redis中 是否存在数据*/
            Category category = (Category) redisTemplate.opsForHash().get("hash:category", id);
            /*2存在 则 从redis中 获取数据 返回*/
            if (category != null){
                System.out.println("从 redis中获取数据");
                return category;
            }else {
                System.out.println("从mysql中获取数据");
                category = categoryDao.selectById(id);
                redisTemplate.opsForHash().put("hash:category",category.getId(),category);
                redisTemplate.expire("hash:category",5,TimeUnit.MINUTES);
                return  category;
            }
        }
    
    
        @Override
        public  Category queryOneWithString(int id) {
            redisTemplate.setKeySerializer(RedisSerializer.string());
            Category category = (Category) redisTemplate.opsForValue().get("category");
            /*解决缓存穿透问题,存 null 改变 判断方式*/
            if (redisTemplate.hasKey("category")){   //第一次检查 第一步解决缓存穿透
                /*1 判断 redis中 是否存在数据*/
                System.out.println("从 redis中获取数据");
                return category;
            }else {
                /*解决 缓存 击穿*/
                synchronized (this){
                    /*再次 查询 是否 存在数据*/
                    if (!redisTemplate.hasKey("category")){    第二次检查  第二步 解决缓存击穿   设置双重锁
                        System.out.println("从mysql中获取数据");
                        category = categoryDao.selectById(id);
                        redisTemplate.opsForValue().set("category",category);
                        if (category == null){
                            redisTemplate.expire("category",30,TimeUnit.SECONDS);//设置空白缓存过期30秒
                        }
                        /*解决缓存雪崩: 设置不同的过期时间 随机*/   //第三步 解决 缓存雪崩 防止大量缓存过期
                        Random random = new Random();
                        int suiji = random.nextInt(10);
                        System.out.println("随机数"+suiji  );
    
                        redisTemplate.expire("category",5+suiji,TimeUnit.MINUTES);
                    }else {
                        /*再从redis中获取*/
                        category = (Category) redisTemplate.opsForValue().get("category");
                    }
                }
                return  category;
            }
        }
    
    
        /*增删改  删掉redis数据 实现 数据同步-------------高并发redis双删  1先删redis  2再删mysql 3 延迟删除 redis*/
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
  • 相关阅读:
    水稻育种技术全球领先海外市场巨大 国稻种芯百团计划行动
    C++ 数字
    阿里云PolarDB自研数据库详细介绍_兼容MySQL、PostgreSQL和Oracle语法
    2020年最新最全的Java面试经历整理(一次性查缺补漏个够)
    springMVC02之CRUD和文件上传下载
    【Linux】Ubunt20.04在vscode中使用Fira Code字体【教程】
    蓝桥杯---动态规划(1)
    int、Integer、new Integer和Integer.valueOf()的 ==、equals比较
    Numpy(一)简介与基本使用
    网络运维的重要性
  • 原文地址:https://blog.csdn.net/qq_51307593/article/details/127453897