(1)正常的缓存穿透使用场景是,所有的查询请求先经过缓存,当缓存命中后,直接返回缓存中的数据;在缓存未命中的清空下,去数据库查询数据,并写入缓存。缓存的目的是为了尽可能将请求在缓存层处理,避免大量的请求进入缓存层,以达到保护缓存层的效果,如下图所示:
通常我们可以在应用程序中分别统计总调用数,缓存层命中数和存储层命中数。如果发现大量存储层命中,有可能就出现了存储层穿透。造成缓存穿透的原因有以下两点:
应用程序自身的问题,如缓存设计或者数据存储问题。
黑客恶意攻击,已感染网络爬虫等。
(2)下面分析常用的缓存穿透问题的解决方案
1.缓存空对象
如下图(1-1)中,由于存储层大量的请求不能命中,无法填充缓存层,造成了恶性循环。缓存空对象的方式,就是在存储层未命中的情况下,仍然将空对象存储到缓存层中,之后再次访问到这条数据将会在缓存层中命中,有效的保护了缓存层。缓存空对象的架构设计图如下(1-2)所示:
缓存空对象的解决方案有两个问题: 第一个问题是缓存为空会存储更多的空对象,因此缓存层需要更多的存储空间,比较有效的办法是为这类数据设计合理的"过期时间" ,节约缓存层的空间;第二个问题是缓存层和存储层会出现数据一致性问题,即在缓存有效期内,存储层这个数据可能已经被更新,此时可以使用消息队列或者其他当时刷新缓存层的对象。下面是缓存空对象这种解决方案的伪代码:
/**
* 根据key查询对象,缓存空对象
*
***/
public Object get(String key){
// 获取缓存中的数据
Object cacheValue = cache.get(key);
// 缓存为空
if (cacheValue == null) {
// 获取缓存层数据
Object storeValue = db.get(key);
//存储层未命中,设置空对象
if(storeValue ==null){
storeValue = new Blank(); // 构造一个保底的空对象
}
// 存储层数据写入缓存
cache.set(key,storeValue);
// 如果storeValue为空对象Blank 类型,设置超时时间为600s
if(storeValue instanceof Blank){
cache.expire(key,60*10);
}
return storeValue;
}
// 缓存非空直接返回
return cacheValue;
}