本文來自網易雲社區。java
本劇情純屬真實,猶如雷同實乃緣分。linux
事情的發生在某天早上,天氣怎樣反正是忘了,只記得當時監控平臺大量的數據庫錯誤報警。 做爲後端開發,當看到日誌中大量的db鏈接獲取失敗,心情是複雜的。spring
看了下配置和實際鏈接數,居然。。。沒滿。恩,多是突發流量。然而沒多久,一大波報警又襲來,感受事情沒那麼簡單。數據庫
常規措施無果,連續數次如此,看日誌發現時間有點奇怪,都是間隔5分鐘, 難道。。。緩存失效了? 看業務代碼和緩存配置,頗有可能。後端
業務代碼表示緩存
@Cacheable(value = "item_volume", key = "'item_' + #gid", unless = "#result == null")
public Item queryiItem(long gid) {
Optional<Item> optional = itemService.getItem(null, gid);
return optional.orNull();
}
注: cache的實現用的是spring-> app
沒錯,換配置item_volume過時時間5分鐘,也就是說,當緩存過時後,此時若是有大量請求,那麼這些請求都會由於緩存失效而請求數據庫。 看起來情形是這樣的:less
若是假設成立,那就是spring在處理緩存的時候,若是沒有命中,直接穿透執行實際操做(db查詢),也就是說,中間是不加鎖的。ide
這樣就解釋通了,但這是bug嗎,仍是spring認爲是個feature, 這是個問題。post
Talk is cheap, show me the code. --linux之爹
關鍵是,code在哪。又得上套路了:套路: 既然是AOP,找找Advice。 最直接能想到,就是在spring中找全部Advice接口的繼承樹,然而數量太多,逐個尋找驗證明在是耗時。
熟悉spring事務的同窗應該能想到@Transactional的Advice是TransactionInterceptor,那麼cache是否對應對一個CacheInterceptor呢。一看,還真有,那就好辦了,並且看起就是要找的。
修改代碼
順着CacheInterceptor的invoke方法,定位到CacheAspectSupport.execute,看代碼實現,確實沒加鎖,那就加個鎖唄:
private Lock lock = new ReentrantLock();
//execute中部分代碼 lock.lock();
try {
result = findCachedItem(contexts.get(CacheableOperation.class));
if (result == null) {
result = new SimpleValueWrapper(invokeOperation(invoker));
}
collectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests);
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(result.get());
}
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, result.get());
} finally {
lock.unlock();
}
其中,lock相關爲新加部分。
代碼是改好了,怎麼生效呢。還得回頭看看CacheInterceptor是如何注入的,也不難找到:
那就寫個類 MyCacheAspectSupport.java.txt 來代替CacheInterceptor,而後注入。這裏又會用到一些 套路:bean覆蓋 、套路:beanname規則 等。
<bean id="errorHandler"
class="org.springframework.cache.interceptor.SimpleCacheErrorHandler"/>
<bean name="org.springframework.cache.interceptor.CacheInterceptor#0"
class="org.springframework.cache.interceptor.MyCacheAspectSupport"
p:errorHandler-ref="errorHandler"
p:cacheManager-ref="cacheManager"/>
驗證下,能夠工做,然而總以爲哪裏不對,恩,若是有多個bean。。。
<bean id="errorHandler"
class="org.springframework.cache.interceptor.SimpleCacheErrorHandler"/>
<bean name="myCacheAdvice"
class="org.springframework.cache.interceptor.MyCacheAspectSupport"
p:errorHandler-ref="errorHandler"
p:cacheManager-ref="cacheManager"/>
<bean id="annotationCacheOperationSource"
class="org.springframework.cache.annotation.AnnotationCacheOperationSource"/>
<bean name="org.springframework.cache.config.internalCacheAdvisor"
class="org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor" p:adviceBeanName="myCacheAdvice"
p:cacheOperationSource-ref="annotationCacheOperationSource" />
驗證下,能夠工做。其實還能夠有3:
@Autowired private MyCacheAspectSupport mycacheAdvice;
@Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (CacheInterceptor.class.isAssignableFrom(bean.getClass())) {
return mycacheAdvice;
}
return bean;
}
驗證下,能夠工做。
好了,問題解決,測了下性能也沒太大降低(<1%,場景不一樣,僅供參考),終於又能夠愉快的使用Cacheable等註解了。
spring和相關衍生擁有至關大的代碼量,好在有不少套路都是通用的,利用這些套路能讓咱們解決問題事半功倍。
注: 文中spring版本爲4.2.6.RELEASE
本文來自網易雲社區,經做者王大喜受權發佈。