当前位置 博文首页 > 盛夏温暖流年: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, cacheValue, cacheTime * 2);
});
return cacheValue;
}
}
解释说明
1、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
2、缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。
这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
(3)为key设置不同的缓存失效时间
可以给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效。或者设置热点数据永远不过期,有更新操作就直接更新缓存,不设置过期时间。
一个存在的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;
}
}
用户不断发起请求,请求的数据在缓存和数据库中都没有,比如数据库的 id 都是1开始自增上去的,发起为id值为 -1的数据请求或id为特别大不存在的数据时,该请求会直接访问数据库,从而导致数据库压力过大,严重时甚至会击垮数据库。
接口层增加校验,如id做基础校验,id<=0的直接拦截;
从缓存和数据库中都没有取到的数据,可以将key-value对写为key-null,设置较短的缓存有效时间,如30秒(设置太长会导致正常情况也没法使用),就可以防止攻击用户反复用同一个id进行暴力攻击的情况发生。
参考博客:
1.缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级、缓存热点 key
2.缓存穿透,缓存击穿,缓存雪崩解决方案
3.缓存穿透、缓存击穿、缓存雪崩概念及解决方案
4.Redis-缓存雪崩、击穿、穿透