Spring緩存穿透問題修復
來自專欄網易雲社區9 人贊了文章
本文來自網易雲社區。
本劇情純屬真實,猶如雷同實乃緣分。
發生
事情的發生在某天早上,天氣怎樣反正是忘了,只記得當時監控平台大量的資料庫錯誤報警。 作為後端開發,當看到日誌中大量的db連接獲取失敗,心情是複雜的。
看了下配置和實際連接數,竟然。。。沒滿。恩,可能是突發流量。然而沒多久,一大波報警又襲來,感覺事情沒那麼簡單。
常規措施無果,連續數次如此,看日誌發現時間有點奇怪,都是間隔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->
初步分析
沒錯,換配置item_volume過期時間5分鐘,也就是說,當緩存過期後,此時如果有大量請求,那麼這些請求都會因為緩存失效而請求資料庫。 看起來情形是這樣的:
如果假設成立,那就是spring在處理緩存的時候,如果沒有命中,直接穿透執行實際操作(db查詢),也就是說,中間是不加鎖的。
這樣就解釋通了,但這是bug嗎,還是spring認為是個feature, 這是個問題。
發展
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緩存穿透問題修復
了解 網易雲 :網易雲官網:https://www.163yun.com
網易雲社區:https://sq.163yun.com/blog
新用戶大禮包:https://www.163yun.com/gift
更多網易研發、產品、運營經驗分享請訪問網易雲社區。
推薦閱讀:
※New faces for Spring Festival Gala
※Struts2.1.6與Spring2.5.6框架整合
※spring(構造函數注入)
※SpringAOP的增強順序
※Spring配置DBCP連接池