请选择 进入手机版 | 继续访问电脑版

Redis中缓存击穿 缓存穿透 缓存雪崩解决方案

[复制链接]
丁翼 发表于 2021-1-1 18:34:51 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
 

目次
 
1.缓存击穿
2.缓存穿透
3.缓存雪崩
1.缓存击穿

       指的是单个key在缓存中查不到,去数据库查询,这样如果数据量不大或者并发不大的话是没有什么问题的
     如果数据库数据量大而且是高并发的情况下那么就大概会造成数据库压力过大而瓦解
     执行流程图
  

 举个栗子
业务场景
假设有个获取文章接口,背景发布一篇新文章,文章为置顶推荐文章,这时有2000用户在同一秒钟同时访问这篇文章详情
如果这篇文章没有缓存,会导致数据压力过大!
代码如下
  1. /**     * 获取文章详情 01-- 防止缓存击穿     *     * @date: 2020/12/2 14:10     * @return: Content     */    @GetMapping("getContentDetail")    public Content getContentDetail(@RequestParam(value = "contentId") Long contentId) {        log.info("getContentDetail.req contentId={}", contentId);        Content content;        String detail = CONTENT + ":" + DETAIL + ":" + contentId;        String value = redisService.get(detail);        if (StringUtils.isNotEmpty(value)) {            log.info("从缓存获取数据.....");            return JSON.parseObject(value, Content.class);        }        content = getData(contentId);        // 查询文章内容不空设置缓存为10min        if (Objects.nonNull(content)) {            redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 10);        }        return content;    }
复制代码
  1. /**     * 模仿数据库查询数据     *     * @param contentId 文章ID     * @date: 2020/12/30 10:50     * @return: com.zlp.entity.Content     */    private Content getData(Long contentId) {        log.info("getData查询数据库获取数据count={}.....", count.incrementAndGet());        try {            Thread.sleep(40);            Optional con = contentList.stream().filter(content ->                    content.getContentId().equals(contentId)).findFirst();            if (con.isPresent()) {                return con.get();            }        } catch (InterruptedException e) {            e.printStackTrace();        }        return null;    }
复制代码
构建一个contentList数据容器
  1. /**    * 构建一个contentList数据容器      */    private static List contentList = new ArrayList();    static {        contentList.add(new Content(1L, "说好不哭", "没有联结,厥后的生活!", 1));        contentList.add(new Content(2L, "告别气球", "塞拉河滨,左岸的咖啡", 1));        contentList.add(new Content(3L, "等你下课", "你住的学校旁,我租了一间公寓", 1));    }
复制代码
Jmeter测试如下
jmeter参数设置如下:
线程数:1000
循环次数:10
任务执行时间 :5
假设用1000用户,对这个接口举行访问,我们用AtomicInteger纪录需要查询数据库次数!
控制台打印日志

结果
发现在同一时刻访问接口,设置缓存没有启动作用,怎样只能查询数据一次,其它到查询缓存数据,就会减轻数据库压力
这时我们可以添加分布式锁,只允许一个线程查询数据库,让其它线程自旋调用该方法,在查询缓存数据
代码如下
  1. @GetMapping("getContentDetail02") public Content getContentDetail02(@RequestParam(value = "contentId") Long contentId) {        log.info("getContentDetail.req contentId={}", contentId);        Content content;        String detail = CONTENT + ":" + DETAIL + ":" + contentId;        String conLock = CONTENT + ":" + LOCK + ":" + contentId;        String lock = "";        String value = redisService.get(detail);        if (StringUtils.isNotEmpty(value)) {            log.info("从缓存获取数据.....");            return JSON.parseObject(value, Content.class);        }        try {            lock = redisService.getLock(conLock, 10);            if (StringUtils.isNotEmpty(lock)) {                content = getData(contentId);                // 查询文章内容不空设置缓存为10min                if (Objects.nonNull(content)) {                    redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 10);                }                // 查询文章内容为空设置缓存为1min,制止缓存穿透                redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 1);                return content;            }            // 休眠重新尝试调用方法            Thread.sleep(300);            getContentDetail(contentId);        } catch (Exception e) {            e.printStackTrace();            redisService.unLock(detail, lock);        } finally {            redisService.unLock(detail, lock);        }        return null;    }
复制代码
 
2.缓存穿透

  一般是出现这种情况是因为恶意频仍查询才会对系统造成很大的问题: key缓存而且数据库不存
在所以每次查询都会查询数据库从而导致数据库瓦解。
执行流程图
  

办理思路:
从DB中查询出来数据为空,也举行空数据的缓存,制止DB数据为空也每次都举行数据库查询,逾期时间设置短一些
  1. // 查询文章内容不空设置缓存为10min if (Objects.nonNull(content)) {       redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 10); } // 查询文章内容为空设置缓存为1min,制止缓存穿透 redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 1);
复制代码
使用布隆过滤器,但是会增加一定的复杂度及存在一定的误判率(判断不存在肯定是不存在,判断存在大概会不存在)
布隆过滤器原理:
  布隆过滤器(Bloom Filter)的焦点实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k

 

以上图为例,详细的操作流程:假设聚集内里有3个元素{x, y, z},哈希函数的个数为3。首先将位数组举行初始化,将内里每个位都设置位0。对于聚集内里的每一个元素,将元素依次通过3个哈希函数举行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在聚集中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在聚集中。反之,如果3个点都为1,则该元素大概存在聚集中。注意:此处不能判断该元素是否一定存在聚集中,大概存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很显着这3个点是差异元素颠末哈希得到的位置,因此这种情况说明元素虽然不在聚集中,也大概对应的都是1,这是误判率存在的原因。
布隆过滤器添加元素


  • 将要添加的元素给k个哈希函数
  • 得到对应于位数组上的k个位置
  • 将这k个位置设为1
布隆过滤器查询元素


  • 将要查询的元素给k个哈希函数
  • 得到对应于位数组上的k个位置
  • 如果k个位置有一个为0,则肯定不在聚集中
  • 如果k个位置全部为1,则大概在聚集中
布隆过滤器实现
我们借助Redisson来实现布隆顾虑器
首先我们初始化布隆过滤器数据
  1. /**     * 初始化布隆过滤器数据     *     * @date: 2020/12/30 13:34     * @return: void     */    @GetMapping("initContentBloomData")    public void initContentBloomData() {        log.info("初始化布隆过滤器数据==>initContentBloomData...");        RBloomFilter bloomFilter = redissonClient.getBloomFilter("CONTENT_PREFIX");        //初始化布隆过滤器,var1表现容量大小,var3表现容错率        bloomFilter.tryInit(1000L, 0.0001);        for (int i = 1; i < 1000; i++) {            bloomFilter.add(1);        }        log.info("CONTENT_PREFIX:1 是否存在:" + bloomFilter.contains(1));        log.info("CONTENT_PREFIX:2 是否存在:" + bloomFilter.contains(1002));        log.info("预计插入数量:" + bloomFilter.getExpectedInsertions());        log.info("容错率:" + bloomFilter.getFalseProbability());        log.info("hash函数的个数:" + bloomFilter.getHashIterations());        log.info("插入对象的个数:" + bloomFilter.count());    }
复制代码


 
  1.    /**     * 获取文章详情 -- 防止缓存穿透     *     * @date: 2020/12/2 14:10     * @return: Content     */    @GetMapping("getContentDetail03")    public Content getContentDetail03(@RequestParam(value = "contentId") Long contentId) {        log.info("getContentDetail.req contentId={}", contentId);        Content content;        String detail = CONTENT + ":" + DETAIL + ":" + contentId;        String conLock = CONTENT + ":" + LOCK + ":" + contentId;        RBloomFilter bloomFilter = redissonClient.getBloomFilter(BloomFilterConstants.CONTENT_PREFIX);        if (Boolean.FALSE.equals(bloomFilter.contains(contentId))) {            log.info("bloomFilter.contentId={},非法的文章ID", detail);            return null;        }        String lock = "";        String value = redisService.get(detail);        if (StringUtils.isNotEmpty(value)) {            log.info("从缓存获取数据.....");            return JSON.parseObject(value, Content.class);        }        try {            lock = redisService.getLock(conLock, 10);            if (StringUtils.isNotEmpty(lock)) {                content = getData(contentId);                // 查询文章内容不空设置缓存为10min                if (Objects.nonNull(content)) {                    redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 10);                }                // 查询文章内容为空设置缓存为1min,制止缓存穿透                redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 1);                return content;            }            // 休眠重新尝试调用方法            Thread.sleep(300);            getContentDetail(contentId);        } catch (Exception e) {            e.printStackTrace();            redisService.unLock(detail, lock);        } finally {            redisService.unLock(detail, lock);        }        return null;    }
复制代码
测试
我们首先初始化数据,再调用getContentDetail03接口如下
我们做两次测试
 
第一次contentId=5
控制台输出

第二次contentId=1003
控制台输出

 
 

3.缓存雪崩

  雪崩指的是多个key查询而且出现高并发,缓存中失效或者查不到,然后都去db查询,从而导致db压力突然飙升
从而瓦解。出现原因: 1 key同时失效, 2 redis自己瓦解了
办理办法
 

  • 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(跟击穿的第一个方案类似,但是这样是制止不了其它key去查数据库,只能淘汰查询的次数)
  • 实现redis高可用
  • 差异的key,设置差异的逾期时间,详细值可以根据业务决定,让缓存失效的时间点只管均匀
 

来源:https://blog.csdn.net/zouliping123456/article/details/111995888
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

发布主题

专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )