Spring緩存穿透問題修復

 

本文來自網易雲社區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規則 等。

方法1:因爲ProxyCachingConfiguration沒有指定Advice的name,那就用默認的:

<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。。。

方法2: 注意到ProxyCachingConfiguration中Advisor的name了嗎,那就定義Advisor:

<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:

方法3:實現BeanPostProcessor接口

@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

 

 

本文來自網易雲社區,經做者王大喜受權發佈。

原文:Spring緩存穿透問題修復

相關文章
相關標籤/搜索