標籤:Spring Cache Redis 緩衝
最近在做的一個系統涉及到基礎資料的頻繁調用,大量的網路開銷和資料讀寫給系統帶來了極大的效能壓力,我們決定引入緩衝機制來緩解系統壓力。
什麼是緩衝
提起緩衝機制,大概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, 無法統計命中率等指標
無法統計命中率就沒有辦法判定緩衝的失效和替換,當然這些都是在緩衝變大的情況下需要考慮的
Spring Boot下的Redis緩衝實戰