spring?cache注解@Cacheable緩存穿透詳解
最近發(fā)現(xiàn)線上監(jiān)控有個SQL調(diào)用量很大,但是方法的調(diào)用量不是很大,查看接口實(shí)現(xiàn),發(fā)現(xiàn)接口是做了緩存操作的,使用Spring cache緩存注解結(jié)合tair實(shí)現(xiàn)緩存操作。
但是為啥SQL調(diào)用量這么大,難道緩存沒有生效。測試發(fā)現(xiàn)緩存是正常的,分析了代碼發(fā)現(xiàn),代碼存在緩存穿透的風(fēng)險。
具體注解是這樣的
@Cacheable(value = "storeDeliveryCoverage", key = "#sellerId + '|' + #cityCode", unless = "#result == null")
unless = "#result == null"表明接口返回值不為空的時候才緩存,如果線上有大量不合法的請求參數(shù)過來,由于為空的不會緩存起來,每次請求都打到DB上,導(dǎo)致DB的sql調(diào)用量巨大,給了黑客可乘之機(jī),風(fēng)險還是很大的。
找到原因之后就修改,查詢結(jié)果為空的時候兜底一個null,把這句unless = "#result == null"條件去掉測試了一下,發(fā)現(xiàn)為空的話還是不會緩存。于是debug分析了一波源碼,終于發(fā)現(xiàn)原來是tair的問題。
由于tair自身的特性,無法緩存null。既然無法緩存null,那我們就兜底一個空對象進(jìn)去,取出來的時候把空對象轉(zhuǎn)化為null。
基于這個思路我把Cache的實(shí)現(xiàn)改造了一下
@Override public void put(Object key, Object value) { if (value == null) { // 為空的話,兜底一個空對象,防止緩存穿透(由于tair自身特性不允許緩存null對象的原因,這里緩存一個空對象) 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)是一個空對象,這里給了個內(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對象,這里返回的時候需要轉(zhuǎn)為null if (obj instanceof Nil) { return null; } return () -> obj; } return null; }
改好了之后,測試一下,結(jié)果發(fā)現(xiàn)還是沒有生效,緩存沒有兜底,請求都打到DB上了。
debug走一遍,看了下Cache的源碼,終于發(fā)現(xiàn)關(guā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ù)緩存起來,這里將@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不為空,說明命中緩存了,直接返回緩存結(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是一個接口,它的實(shí)現(xiàn)類是SimpleValueWrapper,這是一個包裝器,將緩存的結(jié)果包裝起來了。
而我們前面的get方法取緩存的時候如果為Nil對象,返回的是null,這樣緩存判斷出來是沒有命中,即cacheHit==null,就會去執(zhí)行具體方法朔源。
所以到這里已經(jīng)很清晰了,關(guān)鍵問題是get取緩存的結(jié)果如果是兜底的Nil對象,應(yīng)該返回new SimpleValueWrapper(null)。
應(yīng)該返回包裝器,包裝的是緩存的對象為null。
測試了一下,發(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對象,這里返回的時候需要轉(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) { // 為空的話,兜底一個空對象,防止緩存穿透(由于tair自身特性不允許緩存null對象的原因,這里緩存一個空對象) 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; } }
測試用例就不貼了。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
聊聊Java 成員變量賦值和構(gòu)造方法誰先執(zhí)行的問題
這篇文章主要介紹了聊聊Java 成員變量賦值和構(gòu)造方法誰先執(zhí)行的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10Kotlin傳遞可變長參數(shù)給Java可變參數(shù)實(shí)例代碼
這篇文章主要介紹了Kotlin傳遞可變長參數(shù)給Java可變參數(shù)實(shí)例代碼,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下2018-01-01如何獲取springboot打成jar后的classpath
這篇文章主要介紹了如何獲取springboot打成jar后的classpath問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07SpringBoot基于自定義注解實(shí)現(xiàn)切面編程
這篇文章主要介紹了SpringBoot基于自定義注解實(shí)現(xiàn)切面編程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-11-11MyBatisPlus 查詢selectOne方法實(shí)現(xiàn)
本文主要介紹了MyBatisPlus 查詢selectOne方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01SpringMVC結(jié)合ajaxfileupload.js實(shí)現(xiàn)文件無刷新上傳
這篇文章主要介紹了SpringMVC結(jié)合ajaxfileupload.js實(shí)現(xiàn)文件無刷新上傳,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10