还不懂缓存穿透?Redis缓存穿透深度剖析

🎈个人公众号:🎈 :✨✨✨ 可为编程✨ 🍟🍟
🔑个人信条:🔑 知足知不足 有为有不为 为与不为皆为可为🌵
🍉本篇简介:🍉本篇记录Redis缓存穿透深度剖析命令操作,如有出入还望指正。

当系统中引入redis缓存后,一个请求进来后,会先从redis缓存中查询,缓存有就直接返回,缓存中没有就去db中查询,db中如果有就会将其丢到缓存中,但是有些key对应更多数据在db中并不存在,或者缓存大批量失效了,每次针对此次key的请求从缓存中取不到,请求都会压到db,从而可能压垮db。因此本篇就针对Redis缓存使用中存在的问题进行梳理,针对问题按照代码模拟现实场景并给出解决方案。

概述

当系统中引入redis缓存后,一个请求进来后,会先从redis缓存中查询,缓存有就直接返回,缓存中没有就去db中查询,db中如果有就会将其丢到缓存中,但是有些key对应更多数据在db中并不存在,或者缓存大批量失效了,每次针对此次key的请求从缓存中取不到,请求都会压到db,从而可能压垮db。因此本篇就针对Redis缓存使用中存在的问题进行梳理,针对问题按照代码模拟现实场景并给出解决方案。

关注公众号【可为编程】回复【加群】进入微信交流群一起学习!!!

缓存穿透

缓存穿透定义

穿透,顾名思义穿透缓存肯定是到数据库了,肯定是查询数据库不存在的数据,因为如果数据库中存在,查询一遍就存入到缓存了,就不会再次和数据库进行IO操作了。正因为数据库没有数据,导致每次请求都要到数据库中,失去了缓存的意义。总结一下造成缓存穿透的条件有:
1、数据库中没有符合请求条件的数据
2、请求穿透缓存频繁请求数据库
3、每次查询的值都不在redis中

缓存穿透场景

Redis起到保护数据库的作用,提升查询效率,如果连数据库都不存在对应数据,同时也就不会写入到Redis中,频繁查询就会和数据库进行频繁IO操作。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用大量此类攻击可能压垮数据库。那么有人会想了,可不可以做个校验呢?如果缓存中不存在用户信息,那么就存一下该用户信息,保证不到数据库不就不会压垮数据库了嘛,确实是这样。
关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

缓存穿透场景模拟

下面我根据现实的场景模拟一下,首先先去查询缓存,如果缓存不存在就去检索数据库,存在即返回。
Controller

@GetMapping("list")
    public String list(@Param("id") Integer id) {
        String re = redisUtils.get("test");
        //2. 缓存中没有数据,查询数据库
        System.out.println("缓存命中...");
        if (StringUtils.isEmpty(re)) {
            //2. 缓存中没有数据,查询数据款
            System.out.println("缓存不命中...查询数据库");
            Test test = testService.queryTest(id);
            return JSON.toJSONString(test);
        }
        System.out.println(re);
        return re;
    }

service

 public Test queryTest(Integer id) {
        Test row = testMapper.queryTest(id);
        System.out.println("查询数据库");
        return row;
    }

我们请求参数id传一个数据库不存在该条数据的id,肯定直接打到数据库。
在这里插入图片描述
数据库中也没有该数据,如果处于高并发情况下这种场景直接造成数据库宕机,因此我们可以将查询出来的null结果存入到缓存,只需要第一次查询的时候检索数据库,后面直接命中缓存返回结果。修改service。

public Test queryTest(Integer id) {
        Test row = testMapper.queryTest(id);
        System.out.println("查询数据库");
        //查询数据库后将对象存入缓存
        redisUtils.set("test", JSON.toJSONString(row));
        redisUtils.expire("test", 30, TimeUnit.MINUTES);
        return row;
    }

在这里插入图片描述
第一次查询数据库并存入缓存,第二次直接查询缓存,看似没有问题,逻辑很合理,但是在高并发的场景下就会出问题了,我们采用Jmater进行压力测试,模拟100个并发请求同时请求查询接口。
在这里插入图片描述
因为多线程场景下存在线程抢占机制,都在查询缓存然后查询数据库,第一个线程来了,看到缓存没有,就去查询数据库,第二个线程来了发现缓存还没有,继续查询数据库,当在查询出来数据与存入缓存环节的空隙时间内,多个请求已经打到数据库了,所以我们要保证并发情况下的操作原子性。由于springboot所有的组件都是单例的,即使有批量请求也让他访问查询和存入缓存的操作是使用同一把锁,所以可以使用synchronized (this)来加锁,第一个请求来时获取锁,查询数据库,在查询之前再次确认下缓存中是否有数据,如果没有则从数据库中查询,获取到数据后存入缓存中。
修改service方法实现

public Test queryTest(Integer id) {
        //只要是同一把锁就可以锁住所有的线程
        //1. synchronized (this): springboot所有的组件都是单例的,即使有批量请求也是使用同一把锁
        synchronized (this) {
            //关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!
            //得到锁以后,应该先去缓存中确定一次,如果没有再进行查询
            String test = redisUtils.get("test");
            if (!StringUtils.isEmpty(test)) {
                //缓存不为空直接返回
                return (Test) JSON.toJSON(test);
            }
            //将数据库的多次查询变为一次
            Test row = testMapper.queryTest(id);
            System.out.println("查询数据库");
            //查询数据库后将对象存入缓存
            redisUtils.set("test", JSON.toJSONString(row));
            redisUtils.expire("test", 30, TimeUnit.MINUTES);
            return row;
        }
    }

我们再次执行之后就发现不会出现刚才那种场景了,只查询了一次数据库,其他请求都没有打到数据库上面。
在这里插入图片描述

缓存不命中...
缓存不命中...
关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!
==> Parameters: 5(Integer)
从缓存中获取数据时出现异常,key:test,value:null
java.lang.NullPointerException: null
<==      Total: 0
查询数据库
缓存命中...
null
缓存不命中...
缓存不命中...
缓存不命中...
缓存不命中...
缓存命中...
null
缓存命中...
null

多次执行的结果是不一样的,线程的优先级和时间片分配可能影响线程的执行顺序和时长,进而影响多线程程序的结果。在实际的生产环境中我们主要是采用消息中间件来接受并发请求,按顺序逐一进行处理,同时引入多线程提高任务执行效率。
关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

缓存穿透解决方案

可以在缓存中存一个空字符串,或者其他特殊字符串用于标识该条为空的数据,然后当应用拿到这个特殊字符串的时候表示数据库没有值,就没必要再去查询数据库了。但是存特殊字符的办法只适用于重复查询同一个不存在的值的情况,如果每次请求,ID都是可变的,并假设ID符合规则,但是每次变化的值都不存在于数据库中,那请求还是会打到数据库中。伪代码如下:

while(true){
  where id = random();
}

所以总结一下几个比较好的解决方案:
关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!
1、对空值缓存
如果一个查询返回的数据为空(不管数据库是否存在),我们仍然把这个结果(null)进行缓存,给其设置一个很短的过期时间,最长不超过五分钟。不然新增了这条数据后,查询还是查不到,保证在后续新增之后不会影响数据查询。
2、设置可访问的名单(白名单)
使用redis中的bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问的id不在bitmaps里面,则进行拦截,不允许访问

(3)采用布隆过滤器
布隆过滤器(Bloom Filter)是1970年由布隆提出的,它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。

布隆过滤器可以用于检测一个元素是否在一个集合中,它的优点是空间效率和查询的时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。后面会单独对其进行介绍。

(4)进行实时监控
当发现redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制对其提供服务(比如:IP黑名单)

今天写太慢了,明天争取将缓存雪崩和缓存击穿一起写出来,重在学习,重在消化。
在这里插入图片描述

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

可为编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值