超輕量級緩存技術——EhCache

1.技術背景:

    系統緩存是位於應用程序與物理數據源之間,用於臨時存放複製數據的內存區域,目的是爲減少應用程序對物理數據源訪問的次數,從而提高應用程序的運行性能。緩存設想內存是有限的,緩存的時效性也是有限的,所以可以設定內存數量的大小可以執行失效算法,可以在內存滿了的情況下,按照最少訪問等算法將緩存直接移除或切換到硬盤上。

    Ehcache從Hibernate發展而來,逐漸涵蓋了Cache界的全部功能,是目前發展勢頭最好的一個項目,具有快速、簡單、低消耗、擴展性強、支持對象或序列化緩存,支持緩存或元素的失效,提供LRU、LFU和FIFO緩存策略,支持內存緩存和硬盤緩存和分佈式緩存機制等特點。其中Cache的存儲方式爲內存或磁盤(ps:無須擔心容量問題)

2.EhCahe的類層次介紹:

    主要分爲三層,最上層是CacheManager,它是操作Ehcache的入口。可以通過CacheManager.getInstance()獲得一個單子的CacheManager,或者通過CacheManager的構造函數創建一個新的CacheManager。每個CacheManger都管理多個Cache。每個Cache都以一種類Hash的方式,關聯多個Element。Element就是我們用於存放緩存內容的地方。

3.環境搭建:

    很簡單只需要將ehcache-2.1.0-distribution.tar.gz和ehcache-web-2.0.2-distribution.tar.gz擠壓的jar包放入WEB-INF/lib下。

    再創建一個重要的配置文件ehcache.xml,可以從ehcache組件包中拷貝一個,也可以自己建立一個,需要放到classpath下,一般放於/WEB-INF/classes/ehcache.xml;具體的配置文件可以網上搜一下

4.實際運用

    一個網站的首頁估計是被訪問次數最多的,我們可以考慮給首頁做一個頁面緩存;

    緩存策略:應該是某個固定時間之內不變的,比如說2分鐘更新一次,以應用結構page-filter-action-service-dao-db爲例。

    位置:頁面緩存做到儘量靠近客戶的地方,就是在page和filter之間,這樣的優點就是第一個用戶請求後,頁面被緩存,第二個用戶在請求,走到filter這個請求就結束了,需要在走到action-service-dao-db,好處當然是服務器壓力大大降低和客戶端頁面響應速度加快。

    首頁頁面緩存存活時間定爲2分鐘,也就是參數timeToLiveSeconds(緩存的存活時間)應該設置爲120,同時timeToIdleSeconds(多長時間不訪問緩存,就清楚該緩存)最好也設爲2分鐘或者小於2分鐘。



接着我們來看一下SimplePageCachingFilter 的配置,

  1. <filter>  
  2. <filter-name>indexCacheFilterfilter-name>  
  3. <filter-class>  
  4. net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter  
  5. <filter-class>  
  6. <filter>  
  7. <filter-mapping>  
  8. <filter-name>indexCacheFilterfilter-name>  
  9. <url-pattern>*index.actionurl-pattern>  
  10. <filter-mapping>  
<filter>
<filter-name>indexCacheFilterfilter-name>
<filter-class>
net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter
<filter-class>
<filter>
<filter-mapping>
<filter-name>indexCacheFilterfilter-name>
<url-pattern>*index.actionurl-pattern>
<filter-mapping>

將上述代碼加入到web.xml,那麼當打開首頁時,你會發現2分鐘纔會有一堆sql語句出現在控制檯,也可以調整爲5分鐘,總之一切盡在掌控之中。


當然,如果你像緩存首頁的部分內容時,你需要使用SimplePageFragmentCachingFilter這個filter,我看一下:

  1. <filter>  
  2. <filter-name>indexCacheFilterfilter-name>  
  3. <filter-class>  
  4. net.sf.ehcache.constructs.web.filter.SimplePageFragmentCachingFilter  
  5. <filter-class>  
  6. filter>  
  7. <filter-mapping>  
  8. <filter-name>indexCacheFilterfilter-name>  
  9. <url-pattern>*/index_right.jsp<url-pattern>  
  10. <filter-mapping>  
<filter>
<filter-name>indexCacheFilterfilter-name>
<filter-class>
net.sf.ehcache.constructs.web.filter.SimplePageFragmentCachingFilter
<filter-class>
filter>
<filter-mapping>
<filter-name>indexCacheFilterfilter-name>
<url-pattern>*/index_right.jsp<url-pattern>
<filter-mapping>
如此我們將jsp頁面通過jsp:include到其他頁面,這樣就做到了頁面局部緩存的效果,這一點貌似沒有oscache的tag好用。


此外cachefilter中還有一個特性,就是gzip,也就是緩存中的元素是被壓縮過的,如果客戶端瀏覽器支持壓縮的話,filter會直接返回壓縮過的流,這樣節省了帶寬,把解壓的工作交給了客戶端瀏覽即可,當然如果客戶端不支持gzip,那麼filter會把緩存的元素拿出來解壓後在返回給客戶端瀏覽器(大多數爬蟲是不支持gzip的,所以filter也會解壓後在返回流)。

總之,Ehcache是一個非常輕量級的緩存實現,而且從1.2之後支持了集羣,而且是hibernate默認的緩存provider,本文主要介紹Ehcahe對頁面緩存的支持,但是它的功能遠不止如此,要用好緩存,對J2ee中緩存的原理、適用範圍、適用場景等等都需要比較深刻的理解,這樣才能用好用對緩存。


爲了大家通過實際例子加深瞭解與場景運用,在奉獻一個實例:

*在Spring中運用EhCache

    適用任意一個現有開源Cache Framework,要求可以Cache系統中service或者DAO層的get/find等方法返回結果,如果數據更新(適用了Create/update/delete),則刷新cache中相應的內容。

    根據需求,計劃適用Spring AOP+enCache來實現這個功能,採用ehCache原因之一就是Spring提供了enCache的支持,至於爲何僅僅支持ehcache而不支持oscache和jbosscache就無從得知了。

    AOP少不了攔截器,先創建一個實現了MethodInterceptor接口的攔截器,用來攔截Service/DAO的方法調用,攔截到方法後,搜索該方法的結果在cache中是否存在,如果存在,返回cache中結果,如果不存在返回數據庫查詢結果,並將結果返回到緩存。

  1. public class MethodCacheInterceptor implements MethodInterceptor, InitializingBean  
  2. {  
  3. private static final Log logger = LogFactory.getLog(MethodCacheInterceptor.class);  
  4. private Cache cache;  
  5. public void setCache(Cache cache) {  
  6. this.cache = cache;  
  7. }  
  8. public MethodCacheInterceptor() {  
  9. super();  
  10. }  
  11. /** 
  12. * 攔截Service/DAO 的方法,並查找該結果是否存在,如果存在就返回cache 中的值, 
  13. * 否則,返回數據庫查詢結果,並將查詢結果放入cache 
  14. */  
  15. public Object invoke(MethodInvocation invocation) throws Throwable {  
  16. String targetName = invocation.getThis().getClass().getName();  
  17. String methodName = invocation.getMethod().getName();  
  18. Object[] arguments = invocation.getArguments();  
  19. Object result;  
  20. logger.debug("Find object from cache is " + cache.getName());  
  21. String cacheKey = getCacheKey(targetName, methodName, arguments);  
  22. Element element = cache.get(cacheKey);  
  23. Page 13 of 26  
  24. if (element == null) {  
  25. logger.debug("Hold up method , Get method result and create cache........!");  
  26. result = invocation.proceed();  
  27. element = new Element(cacheKey, (Serializable) result);  
  28. cache.put(element);  
  29. }  
  30. return element.getValue();  
  31. }  
  32. /** 
  33. * 獲得cache key 的方法,cache key 是Cache 中一個Element 的唯一標識 
  34. * cache key 包括包名+類名+方法名,如com.co.cache.service.UserServiceImpl.getAllUser 
  35. */  
  36. private String getCacheKey(String targetName, String methodName, Object[] arguments) {  
  37. StringBuffer sb = new StringBuffer();  
  38. sb.append(targetName).append(".").append(methodName);  
  39. if ((arguments != null) && (arguments.length != 0)) {  
  40. for (int i = 0; i < arguments.length; i++) {  
  41. sb.append(".").append(arguments[i]);  
  42. }  
  43. }  
  44. return sb.toString();  
  45. }  
  46. /** 
  47. * implement InitializingBean,檢查cache 是否爲空 
  48. */  
  49. public void afterPropertiesSet() throws Exception {  
  50. Assert.notNull(cache, "Need a cache. Please use setCache(Cache) create it.");  
  51. }  
  52. }  
public class MethodCacheInterceptor implements MethodInterceptor, InitializingBean
{
private static final Log logger = LogFactory.getLog(MethodCacheInterceptor.class);
private Cache cache;
public void setCache(Cache cache) {
this.cache = cache;
}
public MethodCacheInterceptor() {
super();
}
/**
* 攔截Service/DAO 的方法,並查找該結果是否存在,如果存在就返回cache 中的值,
* 否則,返回數據庫查詢結果,並將查詢結果放入cache
*/
public Object invoke(MethodInvocation invocation) throws Throwable {
String targetName = invocation.getThis().getClass().getName();
String methodName = invocation.getMethod().getName();
Object[] arguments = invocation.getArguments();
Object result;
logger.debug("Find object from cache is " + cache.getName());
String cacheKey = getCacheKey(targetName, methodName, arguments);
Element element = cache.get(cacheKey);
Page 13 of 26
if (element == null) {
logger.debug("Hold up method , Get method result and create cache........!");
result = invocation.proceed();
element = new Element(cacheKey, (Serializable) result);
cache.put(element);
}
return element.getValue();
}
/**
* 獲得cache key 的方法,cache key 是Cache 中一個Element 的唯一標識
* cache key 包括包名+類名+方法名,如com.co.cache.service.UserServiceImpl.getAllUser
*/
private String getCacheKey(String targetName, String methodName, Object[] arguments) {
StringBuffer sb = new StringBuffer();
sb.append(targetName).append(".").append(methodName);
if ((arguments != null) && (arguments.length != 0)) {
for (int i = 0; i < arguments.length; i++) {
sb.append(".").append(arguments[i]);
}
}
return sb.toString();
}
/**
* implement InitializingBean,檢查cache 是否爲空
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(cache, "Need a cache. Please use setCache(Cache) create it.");
}
}

上面的代碼可以看到,在方法invoke中,完成了搜索cache/新建cache的功能

隨後,再建立一個攔截器MethodCacheAfterAdvice,作用是在用戶進行create/update/delete操作時來刷新、remove相關cache內容,這個攔截器需要實現AfterRetruningAdvice接口,將會在所攔截的方法執行後執行在afterReturning(object arg0,Method arg1,Object[] arg2,object arg3)方法中所預定的操作

  1. public class MethodCacheAfterAdvice implements AfterReturningAdvice, InitializingBean  
  2. {  
  3. private static final Log logger = LogFactory.getLog(MethodCacheAfterAdvice.class);  
  4. private Cache cache;  
  5. Page 15 of 26  
  6. public void setCache(Cache cache) {  
  7. this.cache = cache;  
  8. }  
  9. public MethodCacheAfterAdvice() {  
  10. super();  
  11. }  
  12. public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws  
  13. Throwable {  
  14. String className = arg3.getClass().getName();  
  15. List list = cache.getKeys();  
  16. for(int i = 0;i<list.size();i++){  
  17. String cacheKey = String.valueOf(list.get(i));  
  18. if(cacheKey.startsWith(className)){  
  19. cache.remove(cacheKey);  
  20. logger.debug("remove cache " + cacheKey);  
  21. }  
  22. }  
  23. }  
  24. public void afterPropertiesSet() throws Exception {  
  25. Assert.notNull(cache, "Need a cache. Please use setCache(Cache) create it.");  
  26. }  
  27. }  
public class MethodCacheAfterAdvice implements AfterReturningAdvice, InitializingBean
{
private static final Log logger = LogFactory.getLog(MethodCacheAfterAdvice.class);
private Cache cache;
Page 15 of 26
public void setCache(Cache cache) {
this.cache = cache;
}
public MethodCacheAfterAdvice() {
super();
}
public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws
Throwable {
String className = arg3.getClass().getName();
List list = cache.getKeys();
for(int i = 0;i<list.size();i++){
String cacheKey = String.valueOf(list.get(i));
if(cacheKey.startsWith(className)){
cache.remove(cacheKey);
logger.debug("remove cache " + cacheKey);
}
}
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(cache, "Need a cache. Please use setCache(Cache) create it.");
}
}
該方法獲取目標class的全名,如:com.co.cache.test.TestServiceImpl,然後循環cache的key list,刷新/remove cache中所有和該class相關的element。


接着就是配置encache的屬性,如最大緩存數量、cache刷新的時間等等。

  1. <ehcache>  
  2. <diskStore path="c:\\myapp\\cache"/>  
  3. <defaultCache  
  4. maxElementsInMemory="1000"  
  5. eternal="false"  
  6. timeToIdleSeconds="120"  
  7. timeToLiveSeconds="120"  
  8. overflowToDisk="true"  
  9. />  
  10. <cache name="DEFAULT_CACHE"  
  11. maxElementsInMemory="10000"  
  12. eternal="false"  
  13. timeToIdleSeconds="300000"  
  14. timeToLiveSeconds="600000"  
  15. overflowToDisk="true"  
  16. />  
  17. </ehcache>  
<ehcache>
<diskStore path="c:\\myapp\\cache"/>
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
<cache name="DEFAULT_CACHE"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300000"
timeToLiveSeconds="600000"
overflowToDisk="true"
/>
</ehcache>

這裏需要注意的是defaultCache定義了一個默認的cache,這個Cache不能刪除,否則會拋出No default cache is configured異常。另外由於使用攔截器來刷新Cache內容,因此在定義cache生命週期時可以定義較大的數值,timeToIdleSeconds="30000000",timeToLiveSeconds="6000000",好像還不夠大?


然後再將Cache和兩個攔截器配置到Spring的配置文件cache.xml中即可,需要創建兩個“切入點”,分別用於攔截不同方法名的方法。在配置application.xml並且導入cache.xml。這樣一個簡單的Spring+Encache框架就搭建完成。


由於時間關係就介紹到這裏,以後有機會還會介紹Ehcache在分佈式集羣系統中的使用,謝謝大家。



發佈了27 篇原創文章 · 獲贊 10 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章