Java緩衝架構 EhCache EhCache 是一個純Java的進程內緩衝架構,具有快速、精乾等特點,是Hibernate中預設的CacheProvider。
主要的特性有:
1. 快速.
2. 簡單.
3. 多種緩衝策略
4. 快取資料有兩級:記憶體和磁碟,因此無需擔心容量問題
5. 快取資料會在虛擬機器重啟的過程中寫入磁碟
6. 可以通過RMI、可插入API等方式進行分布式緩衝
7. 具有緩衝和緩衝管理器的偵聽介面
8. 支援多緩衝管理器執行個體,以及一個執行個體的多個快取區域
9. 提供Hibernate的緩衝實現
10. 等等
EHCache是來自sourceforge(http://ehcache.sourceforge.net/)的開源項目,也是純Java實現的簡單、快速的Cache組件。EHCache支援記憶體和磁碟的緩衝,支援LRU、LFU和FIFO多種淘汰演算法,支援分布式的Cache,可以作為Hibernate的快取區外掛程式。同時它也能提供基於Filter的Cache,該Filter可以緩衝響應的內容並採用Gzip壓縮提高響應速度。
EHCache API的基本用法
首先介紹CacheManager類。它主要負責讀取設定檔,預設讀取CLASSPATH下的ehcache.xml,根據設定檔建立並管理Cache對象。
// 使用預設設定檔建立CacheManager
CacheManager manager = CacheManager.create();
// 通過manager可以產生指定名稱的Cache對象
Cache cache = cache = manager.getCache("demoCache");
// 使用manager移除指定名稱的Cache對象
manager.removeCache("demoCache");
可以通過調用manager.removalAll()來移除所有的Cache。通過調用manager的shutdown()方法可以關閉CacheManager。
有了Cache對象之後就可以進行一些基本的Cache操作,例如:
//往cache中添加元素
Element element = new Element("key", "value");
cache.put(element);
//從cache中取回元素
Element element = cache.get("key");
element.getValue();
//從Cache中移除一個元素
cache.remove("key");
可以直接使用上面的API進行資料對象的緩衝,這裡需要注意的是對於緩衝的對象都是必須可序列化的。在下面的篇幅中筆者還會介紹EHCache和Spring、Hibernate的整合使用。
設定檔
設定檔ehcache.xml中命名為demoCache的緩衝配置:
<cache name="demoCache"
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LFU" />
各配置參數的含義:
maxElementsInMemory:緩衝中允許建立的最大對象數
eternal:緩衝中對象是否為永久的,如果是,逾時設定將被忽略,對象從不到期。
timeToIdleSeconds:快取資料的鈍化時間,也就是在一個元素消亡之前,兩次訪問時間的最大時間間隔值, 這隻能在元素不是永久駐留時有效,如果該值是 0 就意味著元素可以停頓無窮長的時間。
timeToLiveSeconds:快取資料的存留時間,也就是一個元素從構建到消亡的最大時間間隔值,這隻能在元素不是永久駐留時有效,如果該值是0就意味著元素可以停頓無窮長的時間。
overflowToDisk:記憶體不足時,是否啟用磁碟緩衝。
memoryStoreEvictionPolicy:緩衝滿了之後的淘汰演算法。LRU和FIFO演算法這裡就不做介紹。LFU演算法直接淘汰使用比較少的對象,在記憶體保留的都是一些經常訪問的對象。對於大部分網站項目,該演算法比較適用。
如果應用需要配置多個不同命名並採用不同參數的Cache,可以相應修改設定檔,增加需要的Cache配置即可。
利用Spring APO整合EHCache
首先,在CLASSPATH下面放置ehcache.xml設定檔。在Spring的設定檔中先添加如下cacheManager配置:
<bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
</bean>
配置demoCache:
<bean id="demoCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager" ref="cacheManager" />
<property name="cacheName">
<value>demoCache</value>
</property>
</bean>
接下來,寫一個實現org.aopalliance.intercept.MethodInterceptor介面的攔截器類。有了攔截器就可以有選擇性的配置想要緩衝的 bean 方法。如果被調用的方法配置為可緩衝,攔截器將為該方法產生 cache key 並檢查該方法返回的結果是否已緩衝。如果已緩衝,就返回緩衝的結果,否則再次執行被攔截的方法,並緩衝結果供下次調用。具體代碼如下:
public class MethodCacheInterceptor implements MethodInterceptor,
InitializingBean {
private Cache cache;
/**
* 設定緩衝名
*/
public void setCache(Cache cache) {
this.cache = cache;
}
/**
* 檢查是否提供必要參數。
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(cache,
"A cache is required. Use setCache(Cache) to provide one.");
}
/**
*
* 如果某方法可被緩衝就緩衝其結果,方法的返回結果必須是可序列化的(Serializable)
*/
public Object invoke(MethodInvocation invocation) throws Throwable {
String targetName = invocation.getThis().getClass().getName();
String methodName = invocation.getMethod().getName();
Object[] arguments = invocation.getArguments();
Object result;
String cacheKey = getCacheKey(targetName, methodName, arguments);
Element element = null;
synchronized (this){
element = cache.get(cacheKey);
if (element == null) {
//調用實際的方法
result = invocation.proceed();
element = new Element(cacheKey, (Serializable) result);
cache.put(element);
}
}
return element.getValue();
}
/**
* 產生CacheKey: targetName.methodName.argument0.argument1...
*/
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();
}
}
synchronized (this)這段代碼實現了同步功能。為什麼一定要同步?Cache對象本身的get和put操作是同步的。如果我們緩衝的資料來自資料庫查詢,在沒有這段同步代碼時,當key不存在或者key對應的對象已經到期時,在多線程並發訪問的情況下,許多線程都會重新執行該方法,由於對資料庫進行重新查詢代價是比較昂貴的,而在瞬間大量的並發查詢,會對資料庫伺服器造成非常大的壓力。所以這裡的同步代碼是很重要的。
繼續完成攔截器和Bean的配置:
<bean id="methodCacheInterceptor" class="com.xiebing.utils.interceptor.MethodCacheInterceptor">
<property name="cache">
<ref local="demoCache" />
</property>
</bean>
<bean id="methodCachePointCut" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref local="methodCacheInterceptor" />
</property>
<property name="patterns">
<list>
<value>.*myMethod</value>
</list>
</property>
</bean>
<bean id="myServiceBean"
class="com.xiebing.ehcache.spring.MyServiceBean">
</bean>
<bean id="myService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref local="myServiceBean" />
</property>
<property name="interceptorNames">
<list>
<value>methodCachePointCut</value>
</list>
</property>
</bean>
其中myServiceBean是實現了商務邏輯的Bean,裡面的方法myMethod()的返回結果需要被緩衝。這樣每次對myServiceBean的myMethod()方法進行調用,都會首先從緩衝中尋找,其次才會查詢資料庫。使用AOP的方式極大地提高了系統的靈活性,通過修改設定檔就可以實現對方法結果的緩衝,所有的對Cache的操作都封裝在了攔截器的實現中。
CachingFilter功能
使用Spring的AOP進行整合,可以靈活的對方法的的返回結果對象進行緩衝。CachingFilter功能可以對HTTP響應的內容進行緩衝。這種方式快取資料的粒度比較粗,例如緩衝整張頁面。它的優點是使用簡單、效率高,缺點是不夠靈活,可重用程度不高。
EHCache使用SimplePageCachingFilter類實現Filter緩衝。該類繼承自CachingFilter,有預設產生cache key的calculateKey()方法,該方法使用HTTP請求的URI和查詢條件來組成key。也可以自己實現一個Filter,同樣繼承CachingFilter類,然後覆寫calculateKey()方法,產生自訂的key。
在筆者參與的項目中很多頁面都使用AJAX,為保證JS請求的資料不被瀏覽器緩衝,每次請求都會帶有一個隨機數參數i。如果使用SimplePageCachingFilter,那麼每次產生的key都不一樣,緩衝就沒有意義了。這種情況下,我們就會覆寫calculateKey()方法。
要使用SimplePageCachingFilter,首先在設定檔ehcache.xml中,增加下面的配置:
<cache name="SimplePageCachingFilter" maxElementsInMemory="10000" eternal="false"
overflowToDisk="false" timeToIdleSeconds="300" timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LFU" />
其中name屬性必須為SimplePageCachingFilter,修改web.xml檔案,增加一個Filter的配置:
<filter>
<filter-name>SimplePageCachingFilter</filter-name>
<filter-class>net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SimplePageCachingFilter</filter-name>
<url-pattern>/test.jsp</url-pattern>
</filter-mapping>
下面我們寫一個簡單的test.jsp檔案進行測試,緩衝後的頁面每次重新整理,在600秒內顯示的時間都不會發生變化的。代碼如下:
<%
out.println(new Date());
%>
CachingFilter輸出的資料會根據瀏覽器發送的Accept-Encoding頭資訊進行Gzip壓縮。經過筆者測試,Gzip壓縮後的資料量是原來的1/4,速度是原來的4-5倍,所以緩衝加上壓縮,效果非常明顯。
在使用Gzip壓縮時,需注意兩個問題:
1. Filter在進行Gzip壓縮時,採用系統預設編碼,對於使用GBK編碼的中文網頁來說,需要將作業系統的語言設定為:zh_CN.GBK,否則會出現亂碼的問題。
2. 預設情況下CachingFilter會根據瀏覽器發送的要求標頭部所包含的Accept-Encoding參數值來判斷是否進行Gzip壓縮。雖然IE6/7瀏覽器是支援Gzip壓縮的,但是在發送請求的時候卻不帶該參數。為了對IE6/7也能進行Gzip壓縮,可以通過繼承CachingFilter,實現自己的Filter,然後在具體的實現中覆寫方法acceptsGzipEncoding。
具體實現參考:
protected boolean acceptsGzipEncoding(HttpServletRequest request) {
final boolean ie6 = headerContains(request, "User-Agent", "MSIE 6.0");
final boolean ie7 = headerContains(request, "User-Agent", "MSIE 7.0");
return acceptsEncoding(request, "gzip") || ie6 || ie7;
}
EHCache在Hibernate中的使用
EHCache可以作為Hibernate的二級緩衝使用。在hibernate.cfg.xml中需增加如下設定:
<prop key="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider
</prop>
然後在Hibernate對應檔的每個需要Cache的Domain中,加入類似如下格式資訊:
<cache usage="read-write|nonstrict-read-write|read-only" />
比如:
<cache usage="read-write" />
最後在設定檔ehcache.xml中增加一段cache的配置,其中name為該domain的類名。
<cache name="domain.class.name"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"
/>
EHCache的監控
對於Cache的使用,除了功能,在實際的系統運營過程中,我們會比較關注每個Cache對象佔用的記憶體大小和Cache的命中率。有了這些資料,我們就可以對Cache的配置參數和系統的配置參數進行最佳化,使系統的效能達到最優。EHCache提供了方便的API供我們調用以擷取監控資料,其中主要的方法有:
//得到緩衝中的對象數
cache.getSize();
//得到緩衝對象佔用記憶體的大小
cache.getMemoryStoreSize();
//得到緩衝讀取的叫用次數
cache.getStatistics().getCacheHits()
//得到緩衝讀取的錯失次數
cache.getStatistics().getCacheMisses()
分布式緩衝
EHCache從1.2版本開始支援分布式緩衝。分布式緩衝主要解決叢集環境中不同的伺服器間的資料的同步問題。具體的配置如下:
在設定檔ehcache.xml中加入
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="peerDiscovery=automatic, multicastGroupAddress=230.0.0.1, multicastGroupPort=4446"/>
<cacheManagerPeerListenerFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"/>
另外,需要在每個cache屬性中加入
<cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>
例如:
<cache name="demoCache"
maxElementsInMemory="10000"
eternal="true"
overflowToDisk="true">
<cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>
</cache>
總結
EHCache是一個非常優秀的基於Java的Cache實現。它簡單、易用,而且功能齊全,並且非常容易與Spring、Hibernate等流行的開源架構進行整合。通過使用EHCache可以減少網站項目中資料庫伺服器的訪問壓力,提高網站的訪問速度,改善使用者的體驗。