天天看點

對緩存機制的了解

概述

Spring 3.1 引入了激動人心的基于注釋(annotation)的緩存(cache)技術,它本質上不是一個具體的緩存實作方案(例如 EHCache 或者 OSCache),而是一個對緩存使用的抽象,通過在既有代碼中添加少量它定義的各種 annotation,即能夠達到緩存方法的傳回對象的效果。

Spring 的緩存技術還具備相當的靈活性,不僅能夠使用 SpEL(Spring Expression Language)來定義緩存的 key 和各種 condition,還提供開箱即用的緩存臨時存儲方案,也支援和主流的專業緩存例如 EHCache 內建。

其特點總結如下:

  • 通過少量的配置 annotation 注釋即可使得既有代碼支援緩存
  • 支援開箱即用 Out-Of-The-Box,即不用安裝和部署額外第三方元件即可使用緩存
  • 支援 Spring Express Language,能使用對象的任何屬性或者方法來定義緩存的 key 和 condition
  • 支援 AspectJ,并通過其實作任何方法的緩存支援
  • 支援自定義 key 和自定義緩存管理者,具有相當的靈活性和擴充性

本文将針對上述特點對 Spring cache 進行詳細的介紹,主要通過一個簡單的例子和原理介紹展開,然後我們将一起看一個比較實際的緩存例子,最後會介紹 spring cache 的使用限制和注意事項。OK,Let ’ s begin!

回頁首

原來我們是怎麼做的

這裡先展示一個完全自定義的緩存實作,即不用任何第三方的元件來實作某種對象的記憶體緩存。

場景是:對一個賬号查詢方法做緩存,以賬号名稱為 key,賬号對象為 value,當以相同的賬号名稱查詢賬号的時候,直接從緩存中傳回結果,否則更新緩存。賬号查詢服務還支援 reload 緩存(即清空緩存)。

首先定義一個實體類:賬号類,具備基本的 id 和 name 屬性,且具備 getter 和 setter 方法

清單 1. Account.java
package cacheOfAnno; 

 public class Account { 
   private int id; 
   private String name; 
  
   public Account(String name) { 
     this.name = name; 
   } 
   public int getId() { 
     return id; 
   } 
   public void setId(int id) { 
     this.id = id; 
   } 
   public String getName() { 
     return name; 
   } 
   public void setName(String name) { 
     this.name = name; 
   } 
 }      

然後定義一個緩存管理器,這個管理器負責實作緩存邏輯,支援對象的增加、修改和删除,支援值對象的泛型。如下:

清單 2. MyCacheManager.java
package oldcache; 

 import java.util.Map; 
 import java.util.concurrent.ConcurrentHashMap; 

 public class MyCacheManager<T> { 
   private Map<String,T> cache = 
       new ConcurrentHashMap<String,T>(); 
  
   public T getValue(Object key) { 
     return cache.get(key); 
   } 
  
   public void addOrUpdateCache(String key,T value) { 
     cache.put(key, value); 
   } 
  
   public void evictCache(String key) {// 根據 key 來删除緩存中的一條記錄
     if(cache.containsKey(key)) { 
       cache.remove(key); 
     } 
   } 
  
   public void evictCache() {// 清空緩存中的所有記錄
     cache.clear(); 
   } 
 }      

好,現在我們有了實體類和一個緩存管理器,還需要一個提供賬号查詢的服務類,此服務類使用緩存管理器來支援賬号查詢緩存,如下:

清單 3. MyAccountService.java
package oldcache; 

 import cacheOfAnno.Account; 

 public class MyAccountService { 
   private MyCacheManager<Account> cacheManager; 
  
   public MyAccountService() { 
     cacheManager = new MyCacheManager<Account>();// 構造一個緩存管理器
   } 
  
   public Account getAccountByName(String acctName) { 
     Account result = cacheManager.getValue(acctName);// 首先查詢緩存
     if(result!=null) { 
       System.out.println("get from cache..."+acctName); 
       return result;// 如果在緩存中,則直接傳回緩存的結果
     } 
     result = getFromDB(acctName);// 否則到資料庫中查詢
     if(result!=null) {// 将資料庫查詢的結果更新到緩存中
       cacheManager.addOrUpdateCache(acctName, result); 
     } 
     return result; 
   } 
  
   public void reload() { 
     cacheManager.evictCache(); 
   } 
  
   private Account getFromDB(String acctName) { 
     System.out.println("real querying db..."+acctName); 
     return new Account(acctName); 
   } 
 }      

現在我們開始寫一個測試類,用于測試剛才的緩存是否有效

清單 4. Main.java
package oldcache; 

 public class Main { 

   public static void main(String[] args) { 
     MyAccountService s = new MyAccountService(); 
     // 開始查詢賬号
     s.getAccountByName("somebody");// 第一次查詢,應該是資料庫查詢
     s.getAccountByName("somebody");// 第二次查詢,應該直接從緩存傳回
    
     s.reload();// 重置緩存
     System.out.println("after reload..."); 
    
     s.getAccountByName("somebody");// 應該是資料庫查詢
     s.getAccountByName("somebody");// 第二次查詢,應該直接從緩存傳回
    
   } 

 }      

按照分析,執行結果應該是:首先從資料庫查詢,然後直接傳回緩存中的結果,重置緩存後,應該先從資料庫查詢,然後傳回緩存中的結果,實際的執行結果如下:

清單 5. 運作結果
real querying db...somebody// 第一次從資料庫加載
 get from cache...somebody// 第二次從緩存加載
 after reload...// 清空緩存
 real querying db...somebody// 又從資料庫加載
 get from cache...somebody// 從緩存加載      

可以看出我們的緩存起效了,但是這種自定義的緩存方案有如下劣勢:

  • 緩存代碼和業務代碼耦合度太高,如上面的例子,AccountService 中的 getAccountByName()方法中有了太多緩存的邏輯,不便于維護和變更
  • 不靈活,這種緩存方案不支援按照某種條件的緩存,比如隻有某種類型的賬号才需要緩存,這種需求會導緻代碼的變更
  • 緩存的存儲這塊寫的比較死,不能靈活的切換為使用第三方的緩存子產品

如果你的代碼中有上述代碼的影子,那麼你可以考慮按照下面的介紹來優化一下你的代碼結構了,也可以說是簡化,你會發現,你的代碼會變得優雅的多!

回頁首

Hello World,注釋驅動的 Spring Cache

Hello World 的實作目标

本 Hello World 類似于其他任何的 Hello World 程式,從最簡單實用的角度展現 spring cache 的魅力,它基于剛才自定義緩存方案的實體類 Account.java,重新定義了 AccountService.java 和測試類 Main.java(注意這個例子不用自己定義緩存管理器,因為 spring 已經提供了預設實作)

需要的 jar 包

為了實用 spring cache 緩存方案,在工程的 classpath 必須具備下列 jar 包。

圖 1. 工程依賴的 jar 包圖
對緩存機制的了解

注意這裡我引入的是最新的 spring 3.2.0.M1 版本 jar 包,其實隻要是 spring 3.1 以上,都支援 spring cache。其中 spring-context-*.jar 包含了 cache 需要的類。

定義實體類、服務類和相關配置檔案

實體類就是上面自定義緩存方案定義的 Account.java,這裡重新定義了服務類,如下:

清單 6. AccountService.java
package cacheOfAnno; 

 import org.springframework.cache.annotation.CacheEvict; 
 import org.springframework.cache.annotation.Cacheable; 

 public class AccountService { 
   @Cacheable(value="accountCache")// 使用了一個緩存名叫 accountCache 
   public Account getAccountByName(String userName) { 
     // 方法内部實作不考慮緩存邏輯,直接實作業務
     System.out.println("real query account."+userName); 
     return getFromDB(userName); 
   } 
  
   private Account getFromDB(String acctName) { 
     System.out.println("real querying db..."+acctName); 
     return new Account(acctName); 
   } 
 }      

注意,此類的 getAccountByName 方法上有一個注釋 annotation,即 @Cacheable(value=”accountCache”),這個注釋的意思是,當調用這個方法的時候,會從一個名叫 accountCache 的緩存中查詢,如果沒有,則執行實際的方法(即查詢資料庫),并将執行的結果存入緩存中,否則傳回緩存中的對象。這裡的緩存中的 key 就是參數 userName,value 就是 Account 對象。“accountCache”緩存是在 spring*.xml 中定義的名稱。

好,因為加入了 spring,是以我們還需要一個 spring 的配置檔案來支援基于注釋的緩存

清單 7. Spring-cache-anno.xml
<beans xmlns="http://www.springframework.org/schema/beans" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:cache="http://www.springframework.org/schema/cache"
	xmlns:p="http://www.springframework.org/schema/p"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
   http://www.springframework.org/schema/beans/spring-beans.xsd 
     http://www.springframework.org/schema/cache 
     http://www.springframework.org/schema/cache/spring-cache.xsd"> 
    
   <cache:annotation-driven />

   <bean id="accountServiceBean" class="cacheOfAnno.AccountService"/> 
 
    <!-- generic cache manager --> 
   <bean id="cacheManager" 
   class="org.springframework.cache.support.SimpleCacheManager">
     <property name="caches"> 
       <set> 
         <bean 
           class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
           p:name="default" /> 
        
         <bean 
           class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
           p:name="accountCache" /> 
       </set> 
     </property> 
   </bean> 
 </beans>      

注意這個 spring 配置檔案有一個關鍵的支援緩存的配置項:

<cache:annotation-driven />,

這個配置項預設使用了一個名字叫 cacheManager 的緩存管理器,這個緩存管理器有一個 spring 的預設實作,即 org.springframework.cache.support.SimpleCacheManager,這個緩存管理器實作了我們剛剛自定義的緩存管理器的邏輯,它需要配置一個屬性 caches,即此緩存管理器管理的緩存集合,除了預設的名字叫 default 的緩存,我們還自定義了一個名字叫 accountCache 的緩存,使用了預設的記憶體存儲方案 ConcurrentMapCacheFactoryBean,它是基于 java.util.concurrent.ConcurrentHashMap 的一個記憶體緩存實作方案。

OK,現在我們具備了測試條件,測試代碼如下:

清單 8. Main.java
package cacheOfAnno; 

 import org.springframework.context.ApplicationContext; 
 import org.springframework.context.support.ClassPathXmlApplicationContext; 

 public class Main { 
   public static void main(String[] args) { 
     ApplicationContext context = new ClassPathXmlApplicationContext( 
        "spring-cache-anno.xml");// 加載 spring 配置檔案
    
     AccountService s = (AccountService) context.getBean("accountServiceBean"); 
     // 第一次查詢,應該走資料庫
     System.out.print("first query..."); 
     s.getAccountByName("somebody"); 
     // 第二次查詢,應該不查資料庫,直接傳回緩存的值
     System.out.print("second query..."); 
     s.getAccountByName("somebody"); 
     System.out.println(); 
   } 
 }      

上面的測試代碼主要進行了兩次查詢,第一次應該會查詢資料庫,第二次應該傳回緩存,不再查資料庫,我們執行一下,看看結果

清單 9. 執行結果
first query...real query account.somebody// 第一次查詢
 real querying db...somebody// 對資料庫進行了查詢
 second query...// 第二次查詢,沒有列印資料庫查詢日志,直接傳回了緩存中的結果      

可以看出我們設定的基于注釋的緩存起作用了,而在 AccountService.java 的代碼中,我們沒有看到任何的緩存邏輯代碼,隻有一行注釋:@Cacheable(value="accountCache"),就實作了基本的緩存方案,是不是很強大?

如何清空緩存

好,到目前為止,我們的 spring cache 緩存程式已經運作成功了,但是還不完美,因為還缺少一個重要的緩存管理邏輯:清空緩存,當賬号資料發生變更,那麼必須要清空某個緩存,另外還需要定期的清空所有緩存,以保證緩存資料的可靠性。

為了加入清空緩存的邏輯,我們隻要對 AccountService.java 進行修改,從業務邏輯的角度上看,它有兩個需要清空緩存的地方

  • 當外部調用更新了賬号,則我們需要更新此賬号對應的緩存
  • 當外部調用說明重新加載,則我們需要清空所有緩存
清單 10. AccountService.java

點選檢視代碼清單

清單 11. Main.java

點選檢視代碼清單

清單 12. 運作結果
first query...real querying db...somebody 
 second query... 
 start testing clear cache... 
 real querying db...somebody1 
 real querying db...somebody2 
 real update db...somebody1 
 real querying db...somebody1 
 real querying db...somebody1 
 real querying db...somebody2      

結果和我們期望的一緻,是以,我們可以看出,spring cache 清空緩存的方法很簡單,就是通過 @CacheEvict 注釋來标記要清空緩存的方法,當這個方法被調用後,即會清空緩存。注意其中一個 @CacheEvict(value=”accountCache”,key=”#account.getName()”),其中的 Key 是用來指定緩存的 key 的,這裡因為我們儲存的時候用的是 account 對象的 name 字段,是以這裡還需要從參數 account 對象中擷取 name 的值來作為 key,前面的 # 号代表這是一個 SpEL 表達式,此表達式可以周遊方法的參數對象,具體文法可以參考 Spring 的相關文檔手冊。

如何按照條件操作緩存

前面介紹的緩存方法,沒有任何條件,即所有對 accountService 對象的 getAccountByName 方法的調用都會起動緩存效果,不管參數是什麼值,如果有一個需求,就是隻有賬号名稱的長度小于等于 4 的情況下,才做緩存,大于 4 的不使用緩存,那怎麼實作呢?

Spring cache 提供了一個很好的方法,那就是基于 SpEL 表達式的 condition 定義,這個 condition 是 @Cacheable 注釋的一個屬性,下面我來示範一下

清單 13. AccountService.java(getAccountByName 方法修訂,支援條件)
@Cacheable(value="accountCache",condition="#userName.length() <= 4")// 緩存名叫 accountCache 
 public Account getAccountByName(String userName) { 
 // 方法内部實作不考慮緩存邏輯,直接實作業務
 return getFromDB(userName); 
 }      

注意其中的 condition=”#userName.length() <=4”,這裡使用了 SpEL 表達式通路了參數 userName 對象的 length() 方法,條件表達式傳回一個布爾值,true/false,當條件為 true,則進行緩存操作,否則直接調用方法執行的傳回結果。

清單 14. 測試方法
s.getAccountByName("somebody");// 長度大于 4,不會被緩存
 s.getAccountByName("sbd");// 長度小于 4,會被緩存
 s.getAccountByName("somebody");// 還是查詢資料庫
 s.getAccountByName("sbd");// 會從緩存傳回      
清單 15. 運作結果
real querying db...somebody 
 real querying db...sbd 
 real querying db...somebody      

可見對長度大于 4 的賬号名 (somebody) 沒有緩存,每次都查詢資料庫。

如果有多個參數,如何進行 key 的組合

假設 AccountService 現在有一個需求,要求根據賬号名、密碼和是否發送日志查詢賬号資訊,很明顯,這裡我們需要根據賬号名、密碼對賬号對象進行緩存,而第三個參數“是否發送日志”對緩存沒有任何影響。是以,我們可以利用 SpEL 表達式對緩存 key 進行設計

清單 16. Account.java(增加 password 屬性)
private String password; 
 public String getPassword() { 
   return password; 
 } 
 public void setPassword(String password) { 
   this.password = password; 
 }      
清單 17. AccountService.java(增加 getAccount 方法,支援組合 key)
@Cacheable(value="accountCache",key="#userName.concat(#password)") 
 public Account getAccount(String userName,String password,boolean sendLog) { 
   // 方法内部實作不考慮緩存邏輯,直接實作業務
   return getFromDB(userName,password); 
  
 }      

注意上面的 key 屬性,其中引用了方法的兩個參數 userName 和 password,而 sendLog 屬性沒有考慮,因為其對緩存沒有影響。

清單 18. Main.java
public static void main(String[] args) { 
   ApplicationContext context = new ClassPathXmlApplicationContext( 
      "spring-cache-anno.xml");// 加載 spring 配置檔案
  
   AccountService s = (AccountService) context.getBean("accountServiceBean"); 
   s.getAccount("somebody", "123456", true);// 應該查詢資料庫
   s.getAccount("somebody", "123456", true);// 應該走緩存
   s.getAccount("somebody", "123456", false);// 應該走緩存
   s.getAccount("somebody", "654321", true);// 應該查詢資料庫
   s.getAccount("somebody", "654321", true);// 應該走緩存
 }      

上述測試,是采用了相同的賬号,不同的密碼組合進行查詢,那麼一共有兩種組合情況,是以針對資料庫的查詢應該隻有兩次。

清單 19. 運作結果
real querying db...userName=somebody password=123456 
 real querying db...userName=somebody password=654321      

和我們預期的一緻。

如何做到:既要保證方法被調用,又希望結果被緩存

根據前面的例子,我們知道,如果使用了 @Cacheable 注釋,則當重複使用相同參數調用方法的時候,方法本身不會被調用執行,即方法本身被略過了,取而代之的是方法的結果直接從緩存中找到并傳回了。

現實中并不總是如此,有些情況下我們希望方法一定會被調用,因為其除了傳回一個結果,還做了其他事情,例如記錄日志,調用接口等,這個時候,我們可以用 @CachePut 注釋,這個注釋可以確定方法被執行,同時方法的傳回值也被記錄到緩存中。

清單 20. AccountService.java
@Cacheable(value="accountCache")// 使用了一個緩存名叫 accountCache 
 public Account getAccountByName(String userName) { 
   // 方法内部實作不考慮緩存邏輯,直接實作業務
   return getFromDB(userName); 
 } 
 @CachePut(value="accountCache",key="#account.getName()")// 更新 accountCache 緩存
 public Account updateAccount(Account account) { 
   return updateDB(account); 
 } 
 private Account updateDB(Account account) { 
   System.out.println("real updating db..."+account.getName()); 
   return account; 
 }      
清單 21. Main.java
public static void main(String[] args) { 
   ApplicationContext context = new ClassPathXmlApplicationContext( 
      "spring-cache-anno.xml");// 加載 spring 配置檔案
  
   AccountService s = (AccountService) context.getBean("accountServiceBean"); 
  
   Account account = s.getAccountByName("someone"); 
   account.setPassword("123"); 
   s.updateAccount(account); 
   account.setPassword("321"); 
   s.updateAccount(account); 
   account = s.getAccountByName("someone"); 
   System.out.println(account.getPassword()); 
 }      

如上面的代碼所示,我們首先用 getAccountByName 方法查詢一個人 someone 的賬号,這個時候會查詢資料庫一次,但是也記錄到緩存中了。然後我們修改了密碼,調用了 updateAccount 方法,這個時候會執行資料庫的更新操作且記錄到緩存,我們再次修改密碼并調用 updateAccount 方法,然後通過 getAccountByName 方法查詢,這個時候,由于緩存中已經有資料,是以不會查詢資料庫,而是直接傳回最新的資料,是以列印的密碼應該是“321”

清單 22. 運作結果
real querying db...someone 
 real updating db...someone 
 real updating db...someone 
 321      

和分析的一樣,隻查詢了一次資料庫,更新了兩次資料庫,最終的結果是最新的密碼。說明 @CachePut 确實可以保證方法被執行,且結果一定會被緩存。

@Cacheable、@CachePut、@CacheEvict 注釋介紹

通過上面的例子,我們可以看到 spring cache 主要使用兩個注釋标簽,即 @Cacheable、@CachePut 和 @CacheEvict,我們總結一下其作用和配置方法。

表 1. @Cacheable 作用和配置方法
@Cacheable 的作用 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存
@Cacheable 主要的參數
value 緩存的名稱,在 spring 配置檔案中定義,必須指定至少一個

例如:

@Cacheable(value=”mycache”) 或者 

@Cacheable(value={”cache1”,”cache2”}

key 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則預設按照方法的所有參數進行組合

例如:

@Cacheable(value=”testcache”,key=”#userName”)

condition 緩存的條件,可以為空,使用 SpEL 編寫,傳回 true 或者 false,隻有為 true 才進行緩存

例如:

@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

表 2. @CachePut 作用和配置方法
@CachePut 的作用 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存,和 @Cacheable 不同的是,它每次都會觸發真實方法的調用
@CachePut 主要的參數
value 緩存的名稱,在 spring 配置檔案中定義,必須指定至少一個

例如:

@Cacheable(value=”mycache”) 或者 

@Cacheable(value={”cache1”,”cache2”}

key 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則預設按照方法的所有參數進行組合

例如:

@Cacheable(value=”testcache”,key=”#userName”)

condition 緩存的條件,可以為空,使用 SpEL 編寫,傳回 true 或者 false,隻有為 true 才進行緩存

例如:

@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

表 3. @CacheEvict 作用和配置方法
@CachEvict 的作用 主要針對方法配置,能夠根據一定的條件對緩存進行清空
@CacheEvict 主要的參數
value 緩存的名稱,在 spring 配置檔案中定義,必須指定至少一個

例如:

@CachEvict(value=”mycache”) 或者 

@CachEvict(value={”cache1”,”cache2”}

key 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則預設按照方法的所有參數進行組合

例如:

@CachEvict(value=”testcache”,key=”#userName”)

condition 緩存的條件,可以為空,使用 SpEL 編寫,傳回 true 或者 false,隻有為 true 才清空緩存

例如:

@CachEvict(value=”testcache”,

condition=”#userName.length()>2”)

allEntries 是否清空所有緩存内容,預設為 false,如果指定為 true,則方法調用後将立即清空所有緩存

例如:

@CachEvict(value=”testcache”,allEntries=true)

beforeInvocation 是否在方法執行前就清空,預設為 false,如果指定為 true,則在方法還沒有執行的時候就清空緩存,預設情況下,如果方法執行抛出異常,則不會清空緩存

例如:

@CachEvict(value=”testcache”,beforeInvocation=true)

回頁首

基本原理

和 spring 的事務管理類似,spring cache 的關鍵原理就是 spring AOP,通過 spring AOP,其實作了在方法調用前、調用後擷取方法的入參和傳回值,進而實作了緩存的邏輯。我們來看一下下面這個圖:

圖 2. 原始方法調用圖
對緩存機制的了解

上圖顯示,當用戶端“Calling code”調用一個普通類 Plain Object 的 foo() 方法的時候,是直接作用在 pojo 類自身對象上的,用戶端擁有的是被調用者的直接的引用。

而 Spring cache 利用了 Spring AOP 的動态代理技術,即當用戶端嘗試調用 pojo 的 foo()方法的時候,給他的不是 pojo 自身的引用,而是一個動态生成的代理類

圖 3. 動态代理調用圖
對緩存機制的了解

如上圖所示,這個時候,實際用戶端擁有的是一個代理的引用,那麼在調用 foo() 方法的時候,會首先調用 proxy 的 foo() 方法,這個時候 proxy 可以整體控制實際的 pojo.foo() 方法的入參和傳回值,比如緩存結果,比如直接略過執行實際的 foo() 方法等,都是可以輕松做到的。

回頁首

擴充性

直到現在,我們已經學會了如何使用開箱即用的 spring cache,這基本能夠滿足一般應用對緩存的需求,但現實總是很複雜,當你的使用者量上去或者性能跟不上,總需要進行擴充,這個時候你或許對其提供的記憶體緩存不滿意了,因為其不支援高可用性,也不具備持久化資料能力,這個時候,你就需要自定義你的緩存方案了,還好,spring 也想到了這一點。

我們先不考慮如何持久化緩存,畢竟這種第三方的實作方案很多,我們要考慮的是,怎麼利用 spring 提供的擴充點實作我們自己的緩存,且在不改原來已有代碼的情況下進行擴充。

首先,我們需要提供一個 CacheManager 接口的實作,這個接口告訴 spring 有哪些 cache 執行個體,spring 會根據 cache 的名字查找 cache 的執行個體。另外還需要自己實作 Cache 接口,Cache 接口負責實際的緩存邏輯,例如增加鍵值對、存儲、查詢和清空等。利用 Cache 接口,我們可以對接任何第三方的緩存系統,例如 EHCache、OSCache,甚至一些記憶體資料庫例如 memcache 或者 h2db 等。下面我舉一個簡單的例子說明如何做。

清單 23. MyCacheManager
package cacheOfAnno; 

 import java.util.Collection; 

 import org.springframework.cache.support.AbstractCacheManager; 

 public class MyCacheManager extends AbstractCacheManager { 
   private Collection<? extends MyCache> caches; 
  
   /** 
   * Specify the collection of Cache instances to use for this CacheManager. 
   */ 
   public void setCaches(Collection<? extends MyCache> caches) { 
     this.caches = caches; 
   } 

   @Override 
   protected Collection<? extends MyCache> loadCaches() { 
     return this.caches; 
   } 

 }      

上面的自定義的 CacheManager 實際繼承了 spring 内置的 AbstractCacheManager,實際上僅僅管理 MyCache 類的執行個體。

清單 24. MyCache
package cacheOfAnno; 

 import java.util.HashMap; 
 import java.util.Map; 

 import org.springframework.cache.Cache; 
 import org.springframework.cache.support.SimpleValueWrapper; 

 public class MyCache implements Cache { 
   private String name; 
   private Map<String,Account> store = new HashMap<String,Account>();; 
  
   public MyCache() { 
   } 
  
   public MyCache(String name) { 
     this.name = name; 
   } 
  
   @Override 
   public String getName() { 
     return name; 
   } 
  
   public void setName(String name) { 
     this.name = name; 
   } 

   @Override 
   public Object getNativeCache() { 
     return store; 
   } 

   @Override 
   public ValueWrapper get(Object key) { 
     ValueWrapper result = null; 
     Account thevalue = store.get(key); 
     if(thevalue!=null) { 
       thevalue.setPassword("from mycache:"+name); 
       result = new SimpleValueWrapper(thevalue); 
     } 
     return result; 
   } 

   @Override 
   public void put(Object key, Object value) { 
     Account thevalue = (Account)value; 
     store.put((String)key, thevalue); 
   } 

   @Override 
   public void evict(Object key) { 
   } 

   @Override 
   public void clear() { 
   } 
 }      

上面的自定義緩存隻實作了很簡單的邏輯,但這是我們自己做的,也很令人激動是不是,主要看 get 和 put 方法,其中的 get 方法留了一個後門,即所有的從緩存查詢傳回的對象都将其 password 字段設定為一個特殊的值,這樣我們等下就能示範“我們的緩存确實在起作用!”了。

這還不夠,spring 還不知道我們寫了這些東西,需要通過 spring*.xml 配置檔案告訴它

清單 25. Spring-cache-anno.xml
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:cache="http://www.springframework.org/schema/cache"
   xmlns:p="http://www.springframework.org/schema/p"  
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
   http://www.springframework.org/schema/beans/spring-beans.xsd 
     http://www.springframework.org/schema/cache 
     http://www.springframework.org/schema/cache/spring-cache.xsd"> 
    
   <cache:annotation-driven /> 

   <bean id="accountServiceBean" class="cacheOfAnno.AccountService"/> 
 
    <!-- generic cache manager --> 
   <bean id="cacheManager" class="cacheOfAnno.MyCacheManager">
     <property name="caches"> 
       <set> 
         <bean 
           class="cacheOfAnno.MyCache"
           p:name="accountCache" /> 
       </set> 
     </property> 
   </bean> 
  
 </beans>      

注意上面配置檔案的黑體字,這些配置說明了我們的 cacheManager 和我們自己的 cache 執行個體。

好,什麼都不說,測試!

清單 26. Main.java
public static void main(String[] args) { 
   ApplicationContext context = new ClassPathXmlApplicationContext( 
      "spring-cache-anno.xml");// 加載 spring 配置檔案
  
   AccountService s = (AccountService) context.getBean("accountServiceBean"); 
  
   Account account = s.getAccountByName("someone"); 
   System.out.println("passwd="+account.getPassword()); 
   account = s.getAccountByName("someone"); 
   System.out.println("passwd="+account.getPassword()); 
 }      

上面的測試代碼主要是先調用 getAccountByName 進行一次查詢,這會調用資料庫查詢,然後緩存到 mycache 中,然後我列印密碼,應該是空的;下面我再次查詢 someone 的賬号,這個時候會從 mycache 中傳回緩存的執行個體,記得上面的後門麼?我們修改了密碼,是以這個時候列印的密碼應該是一個特殊的值

清單 27. 運作結果
real querying db...someone 
 passwd=null 
 passwd=from mycache:accountCache      

結果符合預期,即第一次查詢資料庫,且密碼為空,第二次列印了一個特殊的密碼。說明我們的 myCache 起作用了。

回頁首

注意和限制

基于 proxy 的 spring aop 帶來的内部調用問題

上面介紹過 spring cache 的原理,即它是基于動态生成的 proxy 代理機制來對方法的調用進行切面,這裡關鍵點是對象的引用問題,如果對象的方法是内部調用(即 this 引用)而不是外部引用,則會導緻 proxy 失效,那麼我們的切面就失效,也就是說上面定義的各種注釋包括 @Cacheable、@CachePut 和 @CacheEvict 都會失效,我們來示範一下。

清單 28. AccountService.java
public Account getAccountByName2(String userName) { 
   return this.getAccountByName(userName); 
 } 

 @Cacheable(value="accountCache")// 使用了一個緩存名叫 accountCache 
 public Account getAccountByName(String userName) { 
   // 方法内部實作不考慮緩存邏輯,直接實作業務
   return getFromDB(userName); 
 }      

上面我們定義了一個新的方法 getAccountByName2,其自身調用了 getAccountByName 方法,這個時候,發生的是内部調用(this),是以沒有走 proxy,導緻 spring cache 失效

清單 29. Main.java
public static void main(String[] args) { 
   ApplicationContext context = new ClassPathXmlApplicationContext( 
      "spring-cache-anno.xml");// 加載 spring 配置檔案
  
   AccountService s = (AccountService) context.getBean("accountServiceBean"); 
  
   s.getAccountByName2("someone"); 
   s.getAccountByName2("someone"); 
   s.getAccountByName2("someone"); 
 }      
清單 30. 運作結果
real querying db...someone 
 real querying db...someone 
 real querying db...someone      

可見,結果是每次都查詢資料庫,緩存沒起作用。要避免這個問題,就是要避免對緩存方法的内部調用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式來解決這個問題。

@CacheEvict 的可靠性問題

我們看到,@CacheEvict 注釋有一個屬性 beforeInvocation,預設為 false,即預設情況下,都是在實際的方法執行完成後,才對緩存進行清空操作。期間如果執行方法出現異常,則會導緻緩存清空不被執行。我們示範一下

清單 31. AccountService.java
@CacheEvict(value="accountCache",allEntries=true)// 清空 accountCache 緩存
 public void reload() { 
   throw new RuntimeException(); 
 }      

注意上面的代碼,我們在 reload 的時候抛出了運作期異常,這會導緻清空緩存失敗。

清單 32. Main.java
public static void main(String[] args) { 
   ApplicationContext context = new ClassPathXmlApplicationContext( 
      "spring-cache-anno.xml");// 加載 spring 配置檔案
  
   AccountService s = (AccountService) context.getBean("accountServiceBean"); 
  
   s.getAccountByName("someone"); 
   s.getAccountByName("someone"); 
   try { 
     s.reload(); 
   } catch (Exception e) { 
   } 
   s.getAccountByName("someone"); 
 }      

上面的測試代碼先查詢了兩次,然後 reload,然後再查詢一次,結果應該是隻有第一次查詢走了資料庫,其他兩次查詢都從緩存,第三次也走緩存因為 reload 失敗了。

清單 33. 運作結果
real querying db...someone      

和預期一樣。那麼我們如何避免這個問題呢?我們可以用 @CacheEvict 注釋提供的 beforeInvocation 屬性,将其設定為 true,這樣,在方法執行前我們的緩存就被清空了。可以確定緩存被清空。

清單 34. AccountService.java
@CacheEvict(value="accountCache",allEntries=true,beforeInvocation=true)
 // 清空 accountCache 緩存
 public void reload() { 
   throw new RuntimeException(); 
 }      

注意上面的代碼,我們在 @CacheEvict 注釋中加了 beforeInvocation 屬性,確定緩存被清空。

執行相同的測試代碼

清單 35. 運作結果
real querying db...someone 
 real querying db...someone      

這樣,第一次和第三次都從資料庫取資料了,緩存清空有效。

非 public 方法問題

和内部調用問題類似,非 public 方法如果想實作基于注釋的緩存,必須采用基于 AspectJ 的 AOP 機制,這裡限于篇幅不再細述。

回頁首

其他技巧

Dummy CacheManager 的配置和作用

有的時候,我們在代碼遷移、調試或者部署的時候,恰好沒有 cache 容器,比如 memcache 還不具備條件,h2db 還沒有裝好等,如果這個時候你想調試代碼,豈不是要瘋掉?這裡有一個辦法,在不具備緩存條件的時候,在不改代碼的情況下,禁用緩存。

方法就是修改 spring*.xml 配置檔案,設定一個找不到緩存就不做任何操作的标志位,如下

清單 36. Spring-cache-anno.xml
<beans xmlns="http://www.springframework.org/schema/beans" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:cache="http://www.springframework.org/schema/cache"
   xmlns:p="http://www.springframework.org/schema/p"  
   xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd 
     http://www.springframework.org/schema/cache 
     http://www.springframework.org/schema/cache/spring-cache.xsd"> 
    
   <cache:annotation-driven /> 

   <bean id="accountServiceBean" class="cacheOfAnno.AccountService"/> 
 
    <!-- generic cache manager --> 
   <bean id="simpleCacheManager" 
   class="org.springframework.cache.support.SimpleCacheManager"> 
     <property name="caches"> 
       <set> 
         <bean 
           class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
           p:name="default" /> 
       </set> 
     </property> 
   </bean> 
  
   <!-- dummy cacheManager  --> 
   <bean id="cacheManager" 
   class="org.springframework.cache.support.CompositeCacheManager">
     <property name="cacheManagers"> 
       <list> 
         <ref bean="simpleCacheManager" /> 
       </list> 
     </property> 
     <property name="fallbackToNoOpCache" value="true" /> 
   </bean> 
  
 </beans>      

注意以前的 cacheManager 變為了 simpleCacheManager,且沒有配置 accountCache 執行個體,後面的 cacheManager 的執行個體是一個 CompositeCacheManager,他利用了前面的 simpleCacheManager 進行查詢,如果查詢不到,則根據标志位 fallbackToNoOpCache 來判斷是否不做任何緩存操作。

清單 37. 運作結果
real querying db...someone 
 real querying db...someone 
 real querying db...someone      

可以看出,緩存失效。每次都查詢資料庫。因為我們沒有配置它需要的 accountCache 執行個體。

如果将上面 xml 配置檔案的 fallbackToNoOpCache 設定為 false,再次運作,則會得到

清單 38. 運作結果
Exception in thread "main" java.lang.IllegalArgumentException: 
   Cannot find cache named [accountCache] for CacheableOperation 
     [public cacheOfAnno.Account 
     cacheOfAnno.AccountService.getAccountByName(java.lang.String)]
     caches=[accountCache] | condition='' | key=''      

可見,在找不到 accountCache,且沒有将 fallbackToNoOpCache 設定為 true 的情況下,系統會抛出異常。

繼續閱讀