微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Redis学习专栏Redis缓存雪崩、击穿、穿透

一.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,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",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-缓存雪崩、击穿、穿透

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐