当前位置 博文首页 > 盛夏温暖流年:Redis学习专栏(Redis缓存雪崩、击穿、穿透)

    盛夏温暖流年:Redis学习专栏(Redis缓存雪崩、击穿、穿透)

    作者:[db:作者] 时间:2021-07-13 19:04

    一.Redis缓存雪崩

    热点数据基本都会去做缓存,一般缓存都是定时任务去刷新,或者是查不到之后去更新的,定时任务刷新就有一个问题:缓存雪崩

    概念

    大量的key设置了相同的过期时间,导致缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

    此时,大数据量的请求直接到达数据库,如果没有做熔断策略,其他访问该数据库的接口都无法正常返回,会造成业务中断且短期内无法恢复。

    缓存正常获取时:
    在这里插入图片描述
    缓存同时失效时:
    在这里插入图片描述

    解决方案

    缓存雪崩有三种解决方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间。

    (1)使用锁或队列

    一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,伪代码如下:

    public object GetProductListNew() {
        int cacheTime = 30;
        String cacheKey = "product_list";
        String lockKey = cacheKey;
    
        String cacheValue = CacheHelper.get(cacheKey);
        if (cacheValue != null) {
            return cacheValue;
        } else {
            synchronized(lockKey) {
                cacheValue = CacheHelper.get(cacheKey);
                if (cacheValue != null) {
                    return cacheValue;
                } else {
                    //这里一般是sql查询数据
                    cacheValue = GetProductListFromDB(); 
                    CacheHelper.Add(cacheKey, cacheValue, cacheTime);
                }
            }
            return cacheValue;
        }
    }
    

    加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。

    假设在高并发下,缓存重建期间key是锁着的,那么1000个请求999个都在阻塞,会导致用户等待超时,用户体验很差。

    而且在分布式环境还存在并发问题,可能还要解决分布式锁的问题;因此,在真正的高并发场景下很少使用。

    (2)设置过期标志更新缓存

    第二种解决方案是:给每一个缓存数据增加相应的缓存标记,记录缓存数据是否失效,如果缓存标记失效,则更新数据缓存,伪代码如下:

    //伪代码
    public object GetProductListNew() {
        int cacheTime = 30;
        String cacheKey = "product_list";
        //缓存标记
        String cacheSign = cacheKey + "_sign";
        String sign = CacheHelper.Get(cacheSign);
        //获取缓存值
        String cacheValue = CacheHelper.Get(cacheKey);
        if (sign != null) {
       	    //未过期,直接返回
            return cacheValue; 
        } else {
            CacheHelper.Add(cacheSign, "1", cacheTime);
            ThreadPool.QueueUserWorkItem((arg) -> {
                //这里一般是sql查询数据
                cacheValue = GetProductListFromDB(); 
                //日期设缓存时间的2倍,用于脏读
                CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);                 
            });
            return cacheValue;
        }
    }
    

    解释说明

    1、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;

    2、缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。

    这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

    (3)为key设置不同的缓存失效时间

    可以给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效。或者设置热点数据永远不过期,有更新操作就直接更新缓存,不设置过期时间。

    二.Redis缓存击穿

    概念

    一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。

    解决方案

    (1)设置热点数据永远不过期;

    (2)加互斥锁;

    多个线程同时去查询数据库,可以在第一个查询数据的线程里用一个互斥锁来上锁,其他线程走到这一步拿不到锁就等着,等第一个线程查询结束了,然后加入缓存,后面的线程进来发现有缓存了就直接走缓存。

    /**
         * 互斥锁 针对缓存击穿方案
         * @param key
         * @return
         * @throws InterruptedException
         */
        String mutex(String key) throws InterruptedException {
            String value = redisTemplate.opsForValue().get(key).toString();
            if (value  == null) {
                // 设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
                if (redisTemplate.opsForValue().setIfAbsent(key+"_mutex","1",3,TimeUnit.MINUTES)) {
                    // 数据库获取值
                    value = db.get(key);
                    redisTemplate.opsForValue().set(key, value);
                    redisTemplate.delete(key+"_mutex");
                } else {
                    //其他线程休息50毫秒后重试
                    Thread.sleep(50);
                    redisTemplate.opsForValue().get(key);
                }
                return value;
            } else {
                return value;
            }
        }
    

    三.Redis缓存穿透

    概念

    用户不断发起请求,请求的数据在缓存和数据库中都没有,比如数据库的 id 都是1开始自增上去的,发起为id值为 -1的数据请求或id为特别大不存在的数据时,该请求会直接访问数据库,从而导致数据库压力过大,严重时甚至会击垮数据库。

    解决方案
    • 接口层增加校验,如id做基础校验,id<=0的直接拦截;

    • 从缓存和数据库中都没有取到的数据,可以将key-value对写为key-null,设置较短的缓存有效时间,如30秒(设置太长会导致正常情况也没法使用),就可以防止攻击用户反复用同一个id进行暴力攻击的情况发生。

    参考博客:
    1.缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级、缓存热点 key
    2.缓存穿透,缓存击穿,缓存雪崩解决方案
    3.缓存穿透、缓存击穿、缓存雪崩概念及解决方案
    4.Redis-缓存雪崩、击穿、穿透

    cs