天天看點

Spring Boot下的Redis緩存實戰

最近在做的一個系統涉及到基礎資料的頻繁調用,大量的網絡開銷和資料讀寫給系統帶來了極大的性能壓力,我們決定引入緩存機制來緩解系統壓力。

什麼是緩存

提起緩存機制,大概10個程式員總有5種不同的解釋吧(姑且認為隻有一半的程式員是通過複制粘貼來學習知識的),我也不能免俗的來說說我的了解。

在回答這個問題之前,我們首先要搞清楚為什麼要用緩存?

曆史唯物主義揭示了社會發展的基本動力是社會基礎沖突。

運用到軟體領域同樣适用,一種新技術的出現必然是伴随着特定的沖突産生的,而緩存的出現正是因為媒體提供的實際處理響應速度和軟體需求之間的沖突,最終緩存機制的提出大大的緩解了這個沖突,同時也印證了一句計算機領域的名言:

Any problem in computer science can be solved by anther layer of indirection.

Spring Boot下的Redis緩存實戰

緩存示意圖

結合上圖我們可以看出緩存從某種意義上來說是一種代理,通過自身某一方面的優勢彌補實際響應的局限性,理論上來說還是時間和空間的取舍權衡。

下面列舉幾種常見的緩存

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)

Spring Boot下的Redis緩存實戰

2,   緩存更新(服務層@CacheEvict, @Caching)

Spring Boot下的Redis緩存實戰

注:考慮配置資料的修改頻率較低,并且配置資料的緩存結構比較複雜,每次資料修改和新增會删除相應的緩存,再由接口層調用來重新加載緩存

接下來就是實作了,

首先需要開啟緩存功能,在主程式上加上@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的執行次數就可以:

Spring Boot下的Redis緩存實戰

問題和擴充

先說個碰到的具體問題,我們在使用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, 無法統計命中率等名額

無法統計命中率就沒有辦法判定緩存的失效和替換,當然這些都是在緩存變大的情況下需要考慮的

繼續閱讀