缓存穿透
定义
查询一个根本不存在的数据, 缓存和DB都不会命中, 白嫖了缓存层和DB 。 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。
缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
原因
通常缓存穿透的原因如下:
- 自身业务代码或者数据出现问题。
- 恶意攻击、 爬虫等造成大量空命中
缓存穿透问题解决方案
缓存空对象
这个也很好理解,需要注意的是对空对象,设置个过期时间,伪代码如下
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key)
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存储数据为空, 需要设置一个过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}
复制
一般也够用了,极限情况:恶意攻击,几千万个不存在的id , 都打到了DB,你也都对这几千万个id 作为key, 存到了redis , 虽然他们的value都是空,而且你也设置了过期时间。
但是你想一下,你这几千万次的DB查询,你也挺难过吧,并且你redis里缓存这几千万个key , 那宝贵的内存资源岂不是白白的浪费掉了。。。。。
所以你需要布隆过滤器。
看场景,取舍。
布隆过滤器
Redis进阶-布隆过滤器
缓存同时失效
由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉。
缓存同时失效解决方案
对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间 。 比如 5到10分钟之间的一个随机时间。
伪代码如下
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
//设置一个过期时间(300到600之间的一个随机数)
int expireTime = new Random().nextInt(300) + 300;
if (storageValue == null) {
cache.expire(key, expireTime);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}
复制
缓存雪崩
缓存雪崩指的是缓存层支撑不住或宕掉后, 流量会像奔逃的野牛一样, 打向后端存储层.
由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会达到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。
缓存雪崩的解决方案
预防和解决缓存雪崩问题, 可以从以下三个方面进行着手。
- 1) 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。
- 2) 依赖隔离组件为后端限流并降级。比如使用Hystrix限流降级组件。
- 3) 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。
缓存击穿 ( 热点缓存key重建优化 )
一般情况下,我们使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。
但是有两个问题如果同时出现, 可能就会对应用造成致命的危害:
- 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大
- 重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。
在缓存失效的瞬间, 有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃 。
再通俗一点来说 :对于一些设置了过期时间的key,假设这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。 如果正好在高并发的时候,这个key过期了。。。。 大量的请求都打到了DB层,造成DB的负载非常大,甚至宕机。
缓存击穿的解决方案 (热点缓存key重建优化)
要解决这个问题主要就是要避免大量线程同时重建缓存。
我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。
伪代码如下
String get(String key) {
// 从Redis中获取数据
String value = redis.get(key);
// 如果value为空, 则开始重构缓存
if (value == null) {
// 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex
String mutexKey = "mutext:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 从数据源获取数据
value = db.get(key);
// 回写Redis, 并设置过期时间
redis.setex(key, timeout, value);13 // 删除key_mutex
redis.delete(mutexKey);
}// 其他线程休息50毫秒后重试
else {
Thread.sleep(50);
get(key);
}
}
return value;
}
复制