最近在作的一個系統涉及到基礎數據的頻繁調用,大量的網絡開銷和數據讀寫給系統帶來了極大的性能壓力,咱們決定引入緩存機制來緩解系統壓力。html
提起緩存機制,大概10個程序員總有5種不一樣的解釋吧(姑且認爲只有一半的程序員是經過複製粘貼來學習知識的),我也不能免俗的來講說個人理解。java
在回答這個問題以前,咱們首先要搞清楚爲何要用緩存?git
歷史惟物主義揭示了社會發展的基本動力是社會基礎矛盾。程序員
運用到軟件領域一樣適用,一種新技術的出現必然是伴隨着特定的矛盾產生的,而緩存的出現正是由於介質提供的實際處理響應速度和軟件需求之間的矛盾,最終緩存機制的提出大大的緩解了這個矛盾,同時也印證了一句計算機領域的名言:github
Any problem in computer science can be solved by anther layer of indirection.redis
緩存示意圖spring
結合上圖咱們能夠看出緩存從某種意義上來講是一種代理,經過自身某一方面的優點彌補實際響應的侷限性,理論上來講仍是時間和空間的取捨權衡。sql
下面列舉幾種常見的緩存數據庫
1, 數據庫緩存瀏覽器
經過將查詢語句緩存到內存中來減小文件系統的讀寫次數和程序響應時間
2, 應用緩存
將應用經常使用數據緩存到內存中來減小數據庫訪問,經過緩存減小了鏈接建立銷燬的時間
3, 用戶端緩存
經過一些用戶端技術如瀏覽器和本地cookie等將用戶經常使用數據進行緩存,減小網絡鏈接的建立銷燬,同時避免了網絡傳輸的消耗
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, 沒法統計命中率等指標
沒法統計命中率就沒有辦法斷定緩存的失效和替換,固然這些都是在緩存變大的狀況下須要考慮的