最近在做的一個系統涉及到基礎數據的頻繁調用,大量的網絡開銷和數據讀寫給系統帶來了極大的性能壓力,我們決定引入緩存機制來緩解系統壓力。
什麼是緩存
提起緩存機制,大概10個程序員總有5種不同的解釋吧(姑且認爲只有一半的程序員是通過複製粘貼來學習知識的),我也不能免俗的來說說我的理解。
在回答這個問題之前,我們首先要搞清楚爲什麼要用緩存?
歷史唯物主義揭示了社會發展的基本動力是社會基礎矛盾。
運用到軟件領域同樣適用,一種新技術的出現必然是伴隨着特定的矛盾產生的,而緩存的出現正是因爲介質提供的實際處理響應速度和軟件需求之間的矛盾,最終緩存機制的提出大大的緩解了這個矛盾,同時也印證了一句計算機領域的名言:
Any problem in computer science can be solved by anther layer of indirection.
緩存示意圖
結合上圖我們可以看出緩存從某種意義上來說是一種代理,通過自身某一方面的優勢彌補實際響應的侷限性,理論上來說還是時間和空間的取捨權衡。
下面列舉幾種常見的緩存
1, 數據庫緩存
通過將查詢語句緩存到內存中來減少文件系統的讀寫次數和程序響應時間
2, 應用緩存
將應用常用數據緩存到內存中來減少數據庫訪問,通過緩存減少了連接創建銷燬的時間
3, 用戶端緩存
通過一些用戶端技術如瀏覽器和本地cookie等將用戶常用數據進行緩存,減少網絡連接的創建銷燬,同時避免了網絡傳輸的消耗
Spring中的緩存
Spring從3.1版本開始就引入了基於註解的緩存支持,到現在已經發展的相當穩定了。Spring主要提供的是基於JSR107的抽象,對於緩存的具體實現可以是EhCache也可以是Redis。下面簡單搬運一下幾種註解的定義:
@Cacheable 緩存的入口,首先檢查緩存如果沒有命中則執行方法並將方法結果緩存
@CacheEvict 緩存回收,清空對應的緩存數據
@CachePut 緩存更新,執行方法並將方法執行結果更新到緩存中
@Caching 組合多個緩存操作
@CacheConfig 類級別的公共配置
原文鏈接:
https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache
實際系統中的應用
在瞭解了緩存的一些基礎知識和框架的支持情況後,我們開始付諸實施,我們使用Redis作爲緩存的具體實現。
項目基於spring boot <version>2.0.0.RC1</version>,maven的主要配置信息如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RC1</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>1.5.7.RELEASE</version> </dependency> </dependencies>
首先明確緩存的位置,緩存的參與方可能在下面四層
a) 客戶端
b) 接口層
c) 服務層
d) 數據層
在選擇位置的時候出現的分歧是離客戶端更近一些還是離緩存所有方更近,具體到我們系統中就是緩存放在a還是b,各有優劣。
放在客戶端可以降低網絡消耗,放在服務端可以明確管理職責,最終我們選擇了放在b犧牲一部分的性能消耗來保證數據的完整性和一致性。
下面通過兩個場景來說明緩存的維護
1, 緩存創建(接口層@Cacheable)
2, 緩存更新(服務層@CacheEvict, @Caching)
注:考慮配置數據的修改頻率較低,並且配置數據的緩存結構比較複雜,每次數據修改和新增會刪除相應的緩存,再由接口層調用來重新加載緩存
接下來就是實現了,
首先需要開啓緩存功能,在主程序上加上@EnableCaching註解即可
然後是相關注解的代碼:
@Cacheable(value="icare_region",key="('c_').concat(#companyId)") public List<Region> loadRegionByCompIdRest(@RequestParam("companyId") Integer companyId){ List<Region> regions = regionService.selectRegionsByCompId(companyId); return regions; } @CacheEvict(cacheNames="icare_region", key="('c_').concat(#region.companyId)") public void saveRegion(Region region) { regionMapper.insert(region); } @Caching(evict = { @CacheEvict(cacheNames="icare_region", key="('r_').concat(#region.regionId)"), @CacheEvict(cacheNames="icare_region", key="('c_').concat(#region.companyId)") }) public void updateRegion(Region region) { Region existRegion = regionMapper.selectByPrimaryKey(region.getRegionId()); region.setStatus(existRegion.getStatus()); region.setCreateTime(existRegion.getCreateTime()); region.setUpdateTime(new Date()); regionMapper.updateByPrimaryKey(region); }
最後就是測試了
在如何確定程序按照我們的意圖走到了緩存而非原來的數據庫調用的時候,我們使用了druid的sql監控功能,如圖直接觀察sql的執行次數就可以:
問題和擴展
先說個碰到的具體問題,我們在使用Redis的時候選擇從網上拷貝了一個RedisConfig的文件來擴展KeyGenerator,RedisTemplate和CacheManager。但是當我們再引入了spring boot的dev-tool的時候,上面的緩存實現會報錯提示ClassCast Exception。
最終在官網找到答案:在老版本的CacheManager中沒有考慮序列化和反序列化的ClassLoader問題,導致序列化和反序列化的ClassLoader不一致;最新的修復就是指定了CacheManager使用的ClassLoader。而網上現在流傳的都是老版本的CacheManager,反而把最新版本的修復覆蓋掉了…
問題鏈接:https://github.com/spring-projects/spring-boot/issues/11822
此外,我們現在實現的這種緩存還有諸多限制,也是我們要擴展的方向
1, 無法設置失效時間
Redis是支持設置失效時間的,但是spring 抽象中沒有提供相關支持。
2, 無法統計命中率等指標
無法統計命中率就沒有辦法判定緩存的失效和替換,當然這些都是在緩存變大的情況下需要考慮的