spring?cache注解@Cacheable緩存穿透詳解
最近發(fā)現(xiàn)線上監(jiān)控有個(gè)SQL調(diào)用量很大,但是方法的調(diào)用量不是很大,查看接口實(shí)現(xiàn),發(fā)現(xiàn)接口是做了緩存操作的,使用Spring cache緩存注解結(jié)合tair實(shí)現(xiàn)緩存操作。
但是為啥SQL調(diào)用量這么大,難道緩存沒有生效。測(cè)試發(fā)現(xiàn)緩存是正常的,分析了代碼發(fā)現(xiàn),代碼存在緩存穿透的風(fēng)險(xiǎn)。
具體注解是這樣的
@Cacheable(value = "storeDeliveryCoverage", key = "#sellerId + '|' + #cityCode", unless = "#result == null")
unless = "#result == null"表明接口返回值不為空的時(shí)候才緩存,如果線上有大量不合法的請(qǐng)求參數(shù)過(guò)來(lái),由于為空的不會(huì)緩存起來(lái),每次請(qǐng)求都打到DB上,導(dǎo)致DB的sql調(diào)用量巨大,給了黑客可乘之機(jī),風(fēng)險(xiǎn)還是很大的。
找到原因之后就修改,查詢結(jié)果為空的時(shí)候兜底一個(gè)null,把這句unless = "#result == null"條件去掉測(cè)試了一下,發(fā)現(xiàn)為空的話還是不會(huì)緩存。于是debug分析了一波源碼,終于發(fā)現(xiàn)原來(lái)是tair的問(wèn)題。
由于tair自身的特性,無(wú)法緩存null。既然無(wú)法緩存null,那我們就兜底一個(gè)空對(duì)象進(jìn)去,取出來(lái)的時(shí)候把空對(duì)象轉(zhuǎn)化為null。
基于這個(gè)思路我把Cache的實(shí)現(xiàn)改造了一下
@Override
public void put(Object key, Object value) {
if (value == null) {
// 為空的話,兜底一個(gè)空對(duì)象,防止緩存穿透(由于tair自身特性不允許緩存null對(duì)象的原因,這里緩存一個(gè)空對(duì)象)
value = new Nil();
}
if (value instanceof Serializable) {
final String tairKey = String.format("%s:%s", this.name, key);
final ResultCode resultCode = this.tairManager.put(
this.namespace,
tairKey,
(Serializable) value,
0,
this.timeout
);
if (resultCode != ResultCode.SUCCESS) {
TairSpringCache.log.error(
String.format(
"[CachePut]: unable to put %s => %s into tair due to: %s",
key,
value,
resultCode.getMessage()
)
);
}
} else {
throw new RuntimeException(
String.format(
"[CachePut]: value %s is not Serializable",
value
)
);
}
}
Nil類默認(rèn)是一個(gè)空對(duì)象,這里給了個(gè)內(nèi)部類:
static class Nil implements Serializable {
private static final long serialVersionUID = -9138993336039047508L;
}
取緩存的get方法實(shí)現(xiàn)
@Override
public ValueWrapper get(Object key) {
final String tairKey = String.format("%s:%s", this.name, key);
final Result<DataEntry> result = this.tairManager.get(this.namespace, tairKey);
if (result.isSuccess() && (result.getRc() == ResultCode.SUCCESS)) {
final Object obj = result.getValue().getValue();
// 緩存為空兜底的是Nil對(duì)象,這里返回的時(shí)候需要轉(zhuǎn)為null
if (obj instanceof Nil) {
return null;
}
return () -> obj;
}
return null;
}
改好了之后,測(cè)試一下,結(jié)果發(fā)現(xiàn)還是沒有生效,緩存沒有兜底,請(qǐng)求都打到DB上了。
debug走一遍,看了下Cache的源碼,終于發(fā)現(xiàn)關(guān)鍵問(wèn)題所在(具體實(shí)現(xiàn)流程參考上一篇:Spring Cache- 緩存攔截器( CacheInterceptor)):
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// Special handling of synchronized invocation
if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {
return wrapCacheValue(method, cache.get(key, new Callable<Object>() {
@Override
public Object call() throws Exception {
return unwrapReturnValue(invokeOperation(invoker));
}
}));
}
catch (Cache.ValueRetrievalException ex) {
// The invoker wraps any Throwable in a ThrowableWrapper instance so we
// can just make sure that one bubbles up the stack.
throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
}
}
else {
// No caching required, only call the underlying method
return invokeOperation(invoker);
}
}
// 處理beforeIntercepte=true的緩存刪除操作
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// 從緩存中查找,是否有匹配@Cacheable的緩存數(shù)據(jù)
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// 如果@Cacheable沒有被緩存,那么就需要將數(shù)據(jù)緩存起來(lái),這里將@Cacheable操作收集成CachePutRequest集合,以便后續(xù)做@CachePut緩存數(shù)據(jù)存放。
List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
//如果沒有@CachePut操作,就使用@Cacheable獲取的結(jié)果(可能也沒有@Cableable,所以result可能為空)。
if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
//如果沒有@CachePut操作,并且cacheHit不為空,說(shuō)明命中緩存了,直接返回緩存結(jié)果
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// 否則執(zhí)行具體方法內(nèi)容,返回緩存的結(jié)果
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
根據(jù)key從緩存中查找,返回的結(jié)果是ValueWrapper,它是返回結(jié)果的包裝器:
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
Object key = generateKey(context, result);
Cache.ValueWrapper cached = findInCaches(context, key);
if (cached != null) {
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
這里判斷緩存是否命中的邏輯是根據(jù)cacheHit是否為空,而cacheHit是ValueWrapper類型,查看ValueWrapper是一個(gè)接口,它的實(shí)現(xiàn)類是SimpleValueWrapper,這是一個(gè)包裝器,將緩存的結(jié)果包裝起來(lái)了。
而我們前面的get方法取緩存的時(shí)候如果為Nil對(duì)象,返回的是null,這樣緩存判斷出來(lái)是沒有命中,即cacheHit==null,就會(huì)去執(zhí)行具體方法朔源。
所以到這里已經(jīng)很清晰了,關(guān)鍵問(wèn)題是get取緩存的結(jié)果如果是兜底的Nil對(duì)象,應(yīng)該返回new SimpleValueWrapper(null)。
應(yīng)該返回包裝器,包裝的是緩存的對(duì)象為null。
測(cè)試了一下,發(fā)現(xiàn)ok了
具體源碼如下:
/**
* 基于tair的緩存,適配spring緩存框架
*/
public class TairSpringCache implements Cache {
private static final Logger log = LoggerFactory.getLogger(TairSpringCache.class);
private TairManager tairManager;
private final String name;
private int namespace;
private int timeout;
public TairSpringCache(String name, TairManager tairManager, int namespace) {
this(name, tairManager, namespace, 0);
}
public TairSpringCache(String name, TairManager tairManager, int namespace, int timeout) {
this.name = name;
this.tairManager = tairManager;
this.namespace = namespace;
this.timeout = timeout;
}
@Override
public String getName() {
return this.name;
}
@Override
public Object getNativeCache() {
return this.tairManager;
}
@Override
public ValueWrapper get(Object key) {
final String tairKey = String.format("%s:%s", this.name, key);
final Result<DataEntry> result = this.tairManager.get(this.namespace, tairKey);
if (result.isSuccess() && (result.getRc() == ResultCode.SUCCESS)) {
final Object obj = result.getValue().getValue();
// 緩存為空兜底的是Nil對(duì)象,這里返回的時(shí)候需要轉(zhuǎn)為null
if (obj instanceof Nil) {
return () -> null;
}
return () -> obj;
}
return null;
}
@Override
public <T> T get(Object key, Class<T> type) {
return (T) this.get(key).get();
}
public <T> T get(Object o, Callable<T> callable) {
return null;
}
@Override
public void put(Object key, Object value) {
if (value == null) {
// 為空的話,兜底一個(gè)空對(duì)象,防止緩存穿透(由于tair自身特性不允許緩存null對(duì)象的原因,這里緩存一個(gè)空對(duì)象)
value = new Nil();
}
if (value instanceof Serializable) {
final String tairKey = String.format("%s:%s", this.name, key);
final ResultCode resultCode = this.tairManager.put(
this.namespace,
tairKey,
(Serializable) value,
0,
this.timeout
);
if (resultCode != ResultCode.SUCCESS) {
TairSpringCache.log.error(
String.format(
"[CachePut]: unable to put %s => %s into tair due to: %s",
key,
value,
resultCode.getMessage()
)
);
}
} else {
throw new RuntimeException(
String.format(
"[CachePut]: value %s is not Serializable",
value
)
);
}
}
public ValueWrapper putIfAbsent(Object key, Object value) {
final ValueWrapper vw = this.get(key);
if (vw.get() == null) {
this.put(key, value);
}
return vw;
}
@Override
public void evict(Object key) {
final String tairKey = String.format("%s:%s", this.name, key);
final ResultCode resultCode = this.tairManager.delete(this.namespace, tairKey);
if ((resultCode == ResultCode.SUCCESS)
|| (resultCode == ResultCode.DATANOTEXSITS)
|| (resultCode == ResultCode.DATAEXPIRED)) {
return;
}
else {
final String errMsg = String.format(
"[CacheDelete]: unable to evict key %s, resultCode: %s",
key,
resultCode
);
TairSpringCache.log.error(errMsg);
throw new RuntimeException(errMsg);
}
}
@Override
public void clear() {
//TODO fgz: implement here later
}
public void setTairManager(TairManager tairManager) {
this.tairManager = tairManager;
}
public void setNamespace(int namespace) {
this.namespace = namespace;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
static class Nil implements Serializable {
private static final long serialVersionUID = -9138993336039047508L;
}
}
測(cè)試用例就不貼了。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- SpringBoot使用@Cacheable時(shí)設(shè)置部分緩存的過(guò)期時(shí)間方式
- SpringBoot如何使用@Cacheable進(jìn)行緩存與取值
- 詳解SpringBoot2.0的@Cacheable(Redis)緩存失效時(shí)間解決方案
- 詳解Spring緩存注解@Cacheable,@CachePut , @CacheEvict使用
- spring整合redis緩存并以注解(@Cacheable、@CachePut、@CacheEvict)形式使用
- spring @Cacheable擴(kuò)展實(shí)現(xiàn)緩存自動(dòng)過(guò)期時(shí)間及自動(dòng)刷新功能
相關(guān)文章
聊聊Java 成員變量賦值和構(gòu)造方法誰(shuí)先執(zhí)行的問(wèn)題
這篇文章主要介紹了聊聊Java 成員變量賦值和構(gòu)造方法誰(shuí)先執(zhí)行的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
Kotlin傳遞可變長(zhǎng)參數(shù)給Java可變參數(shù)實(shí)例代碼
這篇文章主要介紹了Kotlin傳遞可變長(zhǎng)參數(shù)給Java可變參數(shù)實(shí)例代碼,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
如何獲取springboot打成jar后的classpath
這篇文章主要介紹了如何獲取springboot打成jar后的classpath問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
SpringBoot基于自定義注解實(shí)現(xiàn)切面編程
這篇文章主要介紹了SpringBoot基于自定義注解實(shí)現(xiàn)切面編程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11
MyBatisPlus 查詢selectOne方法實(shí)現(xiàn)
本文主要介紹了MyBatisPlus 查詢selectOne方法實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
SpringMVC結(jié)合ajaxfileupload.js實(shí)現(xiàn)文件無(wú)刷新上傳
這篇文章主要介紹了SpringMVC結(jié)合ajaxfileupload.js實(shí)現(xiàn)文件無(wú)刷新上傳,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
java http token請(qǐng)求代碼實(shí)例
這篇文章主要介紹了java http token請(qǐng)求,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03

