关于Redis缓存穿透和缓存击穿的一些见解
本文最后更新于7 天前,其中的信息可能已经过时,如有错误请发送邮件到big_fw@foxmail.com

众所周知

Redis是款高性能的非关系数据库中间件,具备缓存读写的强大能力

但是归根结底还是以身后的数据库为托底。

然而在高并发或有坏比攻击的情况下,可能会干碎你的缓存系统,导致请求直接穿透至数据库进而引发严重问题。常见的失效场景比如有缓存穿透缓存击穿缓存雪崩

我们这来逐一列举一下这些问题的根源和解决方案


1.缓存穿透

根本原因:查询的数据在缓存和数据库中根本不存在,导致每次请求都会直达数据库,而造成数据库压力。

解决方案:核心思路就是即使是不存在的数据,也在缓存中留下记录而防止重复查询数据库,也就是缓存空对象或者布隆过滤器。

缓存空对象:即使数据库查询结果为NULL,也把这个空结果(例如空字符串或特定标识)写入缓存,并设置一个较短的过期时间(例如5分钟)。这样后续请求在有效期内将直接命中缓存中的空值,从而保护数据库。

以下是一种代码实现方案(缓存空对象):

    
//R为泛型参数类型,ID是传入对象的类型, 后面的R返回值类型
public  <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit) {
        //Function<传入参数,返回值>                      //定义一个有参数有返回值的泛型方法
        String key=keyPrefix+id;
        String json=stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(json)){
            R bean = JSONUtil.toBean(json, type);
            return bean;
        }
        if(json!=null){//判断是否为空值
            return null;
        }
        R r= dbFallback.apply(id);//这里就比如调用MP的查询方法
//Function<ID,R>调用时只需要传入一个ID参数,意为输入为ID,输出为R
        if(r==null){
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//注意值为“ ”
            return null;
        }
        this.set(key,r,time,unit);//这个this相当于调用set方法的实例化对象
         |
//       |-> stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(r),time,unit);
//这里是我事先写好了set的工具方法,所以直接this.set,set工具方法里封装的就是这个
        return r;
    }

代码说明

  • 关键点在于区分前两个if到底是查询的有效值还是。redisTemplate.get(key) 对于不存在的 key 返回 null。redisTemplate.set对于存入 "" 的 key 返回空字符串。

2.缓存击穿:

定义:某个热点Key在缓存中Key过期的瞬间,海量的并发请求同时无法命中缓存,并去数据库查询数据并载入缓存。这个瞬间的并发压力可能会压垮数据库。

特别是页面首页这种高并发的地方最容易有缓存击穿的问题

方案一:设置逻辑时间过期:

核心思想:

手动设置一个TLL而不依赖Redis的TTL(自动物理过期时间),而是在缓存值中额外储存一个手动设置的逻辑过期时间. 当发现数据逻辑过期时,异步更新缓存,当前请求返回旧数据。

代码如下:(逻辑过期)

    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
   
    public  <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){
        String key=keyPrefix+id;
        String JSON = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(JSON)) {return null;}
        RedisData redisData = JSONUtil.toBean(JSON, RedisData.class);//1.若命中,需要先把JSON反序列化为对象
        //data封装了封装进这个属性的类的所有属性
        JSONObject data = (JSONObject) redisData.getData();//这里要用JSONObject接收,我也不知道为什么,用Object会报错
        R r=JSONUtil.toBean(data,type);//再把JSONObject转成对应类型对象
        LocalDateTime expireTime=redisData.getExpireTime();
        //2.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){return r;}//2.1 若未过期,直接返回店铺信息
        //2.2 若过期,需要缓存重建
        //3.缓存重建
        //3.1获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
        boolean isLock=tryLock(lockKey);
        if(isLock){//3.2判断是否获取锁成功
            CACHE_REBUILD_EXECUTOR.submit(()->{//3.3 若成功,开启独立线程,实现缓存重建
                try {//重建缓存
                    R r1=dbFallback.apply(id);//函数式编程,传入ID到数据库进行查找来更新数据
                    this.setWithLogicalExpire(key,r1,time,unit);//更新缓存,这个r1只是做到更新缓存的作用,用完就废了,因为逻辑过期时间是TLL到了之后不删缓存而返回旧数据
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    unlock(lockKey);
                }
            });
        }
        //3.4返回过期的商铺信息
        return r;
    }

设置互斥锁:

并发线程一个一个等待锁的开启释放,就不会同时请求并发直击数据库

但带来的问题就是性能的浪费,甚至于导致死锁

以下是代码实例(互斥锁):

public <R,ID> R queryWithMutex(String keyPrefix,ID id,Class<R> type, Function<ID,R> dbFallback,Long time,TimeUnit unit){
        //从Redis中查询缓存
        String key=keyPrefix+id;
        String JSON = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(JSON)){
            //此处要转存json格式才能被前端正确的接受
            R r= JSONUtil.toBean(JSON,type);
            return r;
        }
        //判断是否为空字符串  (三种情况:有值,无值,空字符串,下面有一个判断为null的了)
        if(JSON!=null){
            return null;
        }
        /**
         * ————————————若数据未命中————————————
         */
        //实现缓存重建
        // 1.获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
        R r=null;
        try {
            boolean isLock=tryLock(lockKey);
            //2.判断是否获取互斥锁
            if(!isLock){
                //3.若失败,则进入短期休眠状态
                Thread.sleep(50);
                return queryWithMutex(keyPrefix,id,type,dbFallback,time,unit);// 递归,休眠时间到后重新尝试加锁
            }
            //4.若加锁成功,则根据id查询数据库
            r = dbFallback.apply(id);
            //5.若不存在则返回失败
            if(r==null){
                //这是返回redis的空内容缓存,用于防止无限访问导致的缓存穿透
                stringRedisTemplate.opsForValue().set("cache:shop+id","",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //6.存在,写入redis
            stringRedisTemplate.opsForValue().set("cache:shop+id",JSONUtil.toJsonStr(r),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //7.释放互斥锁
            unlock(lockKey);
        }
        //8.返回
        return r;
    }

    private boolean tryLock(String key){//添加锁
        Boolean flag=stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);//用这个布尔工具类更好一点,可以防止拆箱导致的空指针
    }

其实是先设置了一个LOCK:SHOP+ID的表层级
然后把表层级传入tryLock方法(加锁操作)

tryLock方法中的关键字setIfAbsent的作用是如果不存在就set
setIfAbsent(key,”1″,10,TimeUnit.SECONDS); 会设置值(value)为“1”

并且设置10秒的TTL时间以防止死锁
这个”1“相当于标志,存在时阻止同名缓存
tryLock方法是Boolean类型的

所以当boolean isLock=tryLock(lockKey);时

会去Redis中设置一个新的锁,

然后会被IfAbsent卡住进而返回false,加锁失败
后续对isLock是True还是False进行判断即可
若为false就sleep以下然后在return递归的调用函数本身
必须要是return,是返回到上一层级。不加return只会继续执行if内的语句然后报错
最后释放互斥锁

3. 缓存雪崩

  • 定义:缓存中大量的数据在同一时间点或极短时间内集中过期,或者缓存服务(如Redis集群)直接宕机。导致所有对这些数据的请求全部转向数据库,数据库无法承受瞬间的巨大压力而崩溃,进而导致整个系统瘫痪。

这种的解决方式主要是一种思想习惯而不是代码层面能直接解决的,有错开过期时间、构建高可用的缓存集群、提前预热等手段,这里就不展开细说。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇