前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >还不懂缓存穿透?Redis缓存穿透深度剖析

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

原创
作者头像
可为编程
修改2023-11-27 13:43:57
1940
修改2023-11-27 13:43:57
举报

?个人公众号:? :??? 可为编程? ?? ?个人信条:? 知足知不足 有为有不为 为与不为皆为可为? ?本篇简介:?本篇记录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

代码语言:java
复制
@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

代码语言:java
复制
 public Test queryTest(Integer id) {
        Test row = testMapper.queryTest(id);
        System.out.println("查询数据库");
        return row;
    }

我们请求参数id传一个数据库不存在该条数据的id,肯定直接打到数据库。

数据库中也没有该数据,如果处于高并发情况下这种场景直接造成数据库宕机,因此我们可以将查询出来的null结果存入到缓存,只需要第一次查询的时候检索数据库,后面直接命中缓存返回结果。修改service。

代码语言:java
复制
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方法实现

代码语言:java
复制
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;
        }
    }

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

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

多次执行的结果是不一样的,线程的优先级和时间片分配可能影响线程的执行顺序和时长,进而影响多线程程序的结果。在实际的生产环境中我们主要是采用消息中间件来接受并发请求,按顺序逐一进行处理,同时引入多线程提高任务执行效率。

关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

缓存穿透解决方案

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

代码语言:java
复制
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黑名单)

今天写太慢了,明天争取将缓存雪崩和缓存击穿一起写出来,重在学习,重在消化。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 缓存穿透
    • 缓存穿透定义
      • 缓存穿透场景
        • 缓存穿透场景模拟
          • 缓存穿透解决方案
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
          http://www.vxiaotou.com