前面在介紹MyBatis二級緩衝的時候簡單的介紹了ehcache,以及ehcache如何配置為Mybatis的二級緩衝等。這篇文章介紹ehcache在更為上層的應用。
儘快使用ehcache的二級緩衝可以最佳化Mybatis的查詢效率,但這個有幾個限制。
1. 只能在【只有單表操作】的表上使用緩衝
不只是要保證這個表在整個系統中只有單表操作,而且和該表有關的全部操作必須全部在一個namespace下。
2. 在可以保證查詢遠遠大於insert,update,delete操作的情況下使用緩衝
這一點不需要多說,所有人都應該清楚。記住,這一點需要保證在1的前提下才可以。
在實際中其實應該避免使用二級緩衝,mybatis下的二級緩衝具有如下幾點特點:
>* 緩衝是以namespace為單位的,不同namespace下的操作互不影響。>* insert,update,delete操作會清空所在namespace下的全部緩衝。>* 通常使用MyBatis Generator產生的程式碼中,都是各個表獨立的,每個表都有自己的namespace。
為什麼避免使用二級緩衝
在符合最開始說的二級緩衝的限制時,並沒有什麼影響。
其他情況就會有很多危害了。
針對一個表的某些操作不在他獨立的namespace下進行。
例如在UserMapper.xml中有大多數針對user表的操作。但是在一個XXXMapper.xml中,還有針對user單表的操作。
這會導致user在兩個命名空間下的資料不一致。如果在UserMapper.xml中做了重新整理緩衝的操作,在XXXMapper.xml中緩衝仍然有效,如果有針對user的單表查詢,使用緩衝的結果可能會不正確。
更危險的情況是在XXXMapper.xml做了insert,update,delete操作時(此時會清空所有的緩衝),會導致UserMapper.xml中的各種操作充滿未知和風險。
有關這樣單表的操作可能不常見。但是你也許想到了一種常見的情況。
多表操作一定不能使用緩衝
為什麼不能。
首先不管多表操作寫到那個namespace下,都會存在某個表不在這個namespace下的情況。
例如兩個表:role和user_role,如果我想查詢出某個使用者的全部角色role,就一定會涉及到多表的操作。
<select id="selectUserRoles" resultType="UserRoleVO"> select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}</select>
像上面這個查詢,你會寫到那個xml中呢。。
不管是寫到RoleMapper.xml還是UserRoleMapper.xml,或者是一個獨立的XxxMapper.xml中。如果使用了二級緩衝,都會導致上面這個查詢結果可能不正確。
如果你正好修改了這個使用者的角色,上面這個查詢使用緩衝的時候結果就是錯的。
這點應該很容易理解。
在我看來,就以MyBatis目前的緩衝方式來看是無解的。多表操作根本不能緩衝。
如果你讓他們都使用同一個namespace(通過<cache-ref>)來避免髒資料,那就失去了緩衝的意義。
挽救二級緩衝。
想更高效率的使用二級緩衝是解決不了了。
但是解決多表操作避免髒資料還是有法解決的。解決思路就是通過攔截器判斷執行的sql涉及到那些表(可以用jsqlparser解析),然後把相關表的緩衝自動清空。但是這種方式對緩衝的使用效率是很低的。
設計這樣一個外掛程式是相當複雜的,既然我沒想著去實現,就不廢話了。
最後還是建議,放棄二級緩衝,在業務層使用可控制的緩衝代替更好。
業務層使用ehcache實戰:
需要添加如下jar包到lib目錄下
ehcache-core-2.5.2.jarehcache-web-2.0.4.jar //主要針對頁面緩衝
3、 當前工程的src目錄中加入設定檔
ehcache.xmlehcache.xsd
這些設定檔在ehcache-core這個jar包中可以找到
Ehcache基本用法
CacheManager cacheManager = CacheManager.create();// 或者cacheManager = CacheManager.getInstance();// 或者cacheManager = CacheManager.create("/config/ehcache.xml");// 或者cacheManager = CacheManager.create("http://localhost:8080/test/ehcache.xml");cacheManager = CacheManager.newInstance("/config/ehcache.xml");// .......// 擷取ehcache設定檔中的一個cacheCache sample = cacheManager.getCache("sample");// 擷取頁面緩衝BlockingCache cache = new BlockingCache(cacheManager.getEhcache("SimplePageCachingFilter"));// 添加資料到緩衝中Element element = new Element("key", "val");sample.put(element);// 擷取緩衝中的對象,注意添加到cache中對象要序列化 實現Serializable介面Element result = sample.get("key");// 刪除緩衝sample.remove("key");sample.removeAll();// 擷取緩衝管理器中的緩衝配置名稱for (String cacheName : cacheManager.getCacheNames()) { System.out.println(cacheName);}// 擷取所有的緩衝對象for (Object key : cache.getKeys()) { System.out.println(key);}// 得到緩衝中的對象數cache.getSize();// 得到緩衝對象佔用記憶體的大小cache.getMemoryStoreSize();// 得到緩衝讀取的叫用次數cache.getStatistics().getCacheHits();// 得到緩衝讀取的錯失次數cache.getStatistics().getCacheMisses();
三、頁面緩衝
頁面緩衝主要用Filter過濾器對請求的url進行過濾,如果該url在緩衝中出現。那麼頁面資料就從緩衝對象中擷取,並以gzip壓縮後返回。其速度是沒有壓縮緩衝時速度的3-5倍,效率相當之高。其中頁面緩衝的過濾器有CachingFilter,一般要擴充filter或是自訂Filter都繼承該CachingFilter。
CachingFilter功能可以對HTTP響應的內容進行緩衝。這種方式快取資料的粒度比較粗,例如緩衝整張頁面。它的優點是使用簡單、效率高,缺點是不夠靈活,可重用程度不高。
EHCache使用SimplePageCachingFilter類實現Filter緩衝。該類繼承自CachingFilter,有預設產生cache key的calculateKey()方法,該方法使用HTTP請求的URI和查詢條件來組成key。也可以自己實現一個Filter,同樣繼承CachingFilter類,然後覆寫calculateKey()方法,產生自訂的key。
CachingFilter輸出的資料會根據瀏覽器發送的Accept-Encoding頭資訊進行Gzip壓縮。
在使用Gzip壓縮時,需注意兩個問題: Filter在進行Gzip壓縮時,採用系統預設編碼,對於使用GBK編碼的中文網頁來說,需要將作業系統的語言設定為:zh_CN.GBK,否則會出現亂碼的問題。 預設情況下CachingFilter會根據瀏覽器發送的要求標頭部所包含的Accept-Encoding參數值來判斷是否進行Gzip壓縮。雖然IE6/7瀏覽器是支援Gzip壓縮的,但是在發送請求的時候卻不帶該參數。為了對IE6/7也能進行Gzip壓縮,可以通過繼承CachingFilter,實現自己的Filter,然後在具體的實現中覆寫方法acceptsGzipEncoding。
具體實現參考:
protected boolean acceptsGzipEncoding(HttpServletRequest request) {boolean ie6 = headerContains(request, "User-Agent", "MSIE 6.0");boolean ie7 = headerContains(request, "User-Agent", "MSIE 7.0");return acceptsEncoding(request, "gzip") || ie6 || ie7;}
ehcache.xml中的配置如下:
<?xml version="1.0" encoding="gbk"?><ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd"> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="30" timeToLiveSeconds="30" overflowToDisk="false"/> <!-- 配置自訂緩衝 maxElementsInMemory:緩衝中允許建立的最大對象數 eternal:緩衝中對象是否為永久的,如果是,逾時設定將被忽略,對象從不到期。 timeToIdleSeconds:快取資料的鈍化時間,也就是在一個元素消亡之前, 兩次訪問時間的最大時間間隔值,這隻能在元素不是永久駐留時有效, 如果該值是 0 就意味著元素可以停頓無窮長的時間。 timeToLiveSeconds:快取資料的存留時間,也就是一個元素從構建到消亡的最大時間間隔值, 這隻能在元素不是永久駐留時有效,如果該值是0就意味著元素可以停頓無窮長的時間。 overflowToDisk:記憶體不足時,是否啟用磁碟緩衝。 memoryStoreEvictionPolicy:緩衝滿了之後的淘汰演算法。 注意:一個ehcache.xml中不是只能寫一個cache標籤,可以根據緩衝的對象不同而寫多個cache,其他name為其標識位。 --> <cache name="SimplePageCachingFilter" maxElementsInMemory="10000" eternal="false" overflowToDisk="false" timeToIdleSeconds="900" timeToLiveSeconds="1800" memoryStoreEvictionPolicy="LFU" /></ehcache>
在web.xml中加入如下配置
<!-- ehcache --> <filter> <filter-name>SimplePageCachingFilter</filter-name> <filter-class>net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter </filter-class> </filter> <!-- This is a filter chain. They are executed in the order below. Do not change the order. --> <filter-mapping> <filter-name>SimplePageCachingFilter</filter-name> <url-pattern>*.do</url-pattern> </filter-mapping>
注意: ehcache.xml檔案中配置的緩衝名稱 必須和web.xml配置的緩衝filter名字一致 不然會拋出找不到配置的異常 。
當第一次請求這些頁面後,這些頁面就會被添加到緩衝中,以後請求這些頁面將會從緩衝中擷取。你可以在cache.jsp頁面中用小指令碼來測試該頁面是否被緩衝。<%=new Date()%>如果時間是變動的,則表示該頁面沒有被緩衝或是緩衝已經到期,否則則是在緩衝狀態了。
對象緩衝
對象緩衝就是將查詢的資料,添加到緩衝中,下次重新查詢的時候直接從緩衝中擷取,而不去資料庫中查詢。
對象緩衝一般是針對方法、類而來的,結合Spring的Aop對象、方法緩衝就很簡單。這裡需要用到切面編程,用到了Spring的MethodInterceptor或是用@Aspect。
public class MethodCacheInterceptor implements MethodInterceptor, InitializingBean { private static final Logger log = Logger.getLogger(MethodCacheInterceptor.class); private Cache cache; public void setCache(Cache cache) { this.cache = cache; } public void afterPropertiesSet() throws Exception { log.info(cache + " A cache is required. Use setCache(Cache) to provide one."); } 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) { log.info(cacheKey + "加入到緩衝: " + cache.getName()); // 調用實際的方法 result = invocation.proceed(); element = new Element(cacheKey, (Serializable) result); cache.put(element); } else { log.info(cacheKey + "使用緩衝: " + cache.getName()); } } return element.getValue(); } /** * <b>function:</b> 返回具體的方法全路徑名稱 參數 * @param targetName 全路徑 * @param methodName 方法名稱 * @param arguments 參數 * @return 完整方法名稱 */ 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(); }}
這裡的方法攔截器主要是對你要攔截的類的方法進行攔截,然後判斷該方法的類路徑+方法名稱+參數值組合的cache key在緩衝cache中是否存在。如果存在就從緩衝中取出該對象,轉換成我們要的傳回型別。沒有的話就把該方法返回的對象添加到緩衝中即可。值得主意的是當前方法的參數和傳回值的物件類型需要序列化。
我們需要在src目錄下添加applicationContext.xml完成對MethodCacheInterceptor攔截器的配置,該配置主要是注入我們的cache對象,哪個cache來管理對象緩衝,然後哪些類、方法參與該攔截器的掃描。
<!-- 配置eh緩衝管理器 --><bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/><!-- 配置一個簡單的緩衝工廠bean對象 --><bean id="simpleCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <property name="cacheManager" ref="cacheManager" /> <!-- 使用緩衝 關聯ehcache.xml中的緩衝配置 --> <property name="cacheName" value="mobileCache" /></bean><!-- 配置一個緩衝攔截器對象,處理具體的緩衝業務 --><bean id="methodCacheInterceptor" class="com.common.interceptor.MethodCacheInterceptor"> <property name="cache" ref="simpleCache"/></bean><!-- 參與緩衝的切入點對象 (切入點對象,確定何時何地調用攔截器) --><bean id="methodCachePointCut" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <!-- 配置緩衝aop切面 --> <property name="advice" ref="methodCacheInterceptor" /> <!-- 配置哪些方法參與緩衝策略 --> <!-- .表示符合任何單一字元 ### +表示符合前一個字元一次或多次 ### *表示符合前一個字元零次或多次 ### \Escape任何Regular expression使用到的符號 --> <!-- .*表示前面的首碼(包括包名) 表示print方法--> <property name="patterns"> <list> <value>com.hoo.rest.*RestService*\.*get.*</value> <value>com.hoo.rest.*RestService*\.*search.*</value> </list> </property></bean>
在ehcache.xml中添加如下cache配置
<cache name="mobileCache" maxElementsInMemory="10000" eternal="false" overflowToDisk="true" timeToIdleSeconds="1800" timeToLiveSeconds="3600" memoryStoreEvictionPolicy="LFU" />
參考: http://blog.csdn.net/isea533/article/details/44566257
參考: http://www.cnblogs.com/hoojo/archive/2012/07/12/2587556.html