緩存在很多程序里,讀取數(shù)據(jù)的頻率比寫入要高得多。比如RoadRantz,訪問站點來查看帖子的人比張貼帖子的人要多。雖然帖子列表會隨著時間不斷增長,但其增長速度比不上被查看的速度。 更進一步說,RoadRantz所展示的數(shù)據(jù)對于實時性要求并不高。如果用戶在訪問站點時看到了稍微過時一點的帖子列表,并不會產(chǎn)生太多負面影響,他們會稍后再返回站點來查看更新的帖子列表,這樣做并不會有太大問題。 盡管如此,DAO每次收到關(guān)于帖子列表的請求時,都會訪問數(shù)據(jù)庫來獲得最新的數(shù)據(jù)(經(jīng)常會得到與上次請求一樣的數(shù)據(jù))。 數(shù)據(jù)庫操作通常都是程序性能的最大瓶頸。對于負載很大的程序來說,針對高度優(yōu)化的數(shù)據(jù)源進行的最簡單查詢都可能會產(chǎn)生性能問題。 均衡考慮數(shù)據(jù)變化的頻率以及查詢數(shù)據(jù)庫所付出的性能代價,總是從數(shù)據(jù)庫獲取最新數(shù)據(jù)似乎并不明智,而對頻繁訪問(但不頻繁更新)的數(shù)據(jù)進行緩存則顯得更加合理。 從表面上看,緩存似乎相當(dāng)簡單:在獲取一些信息之后,把它保存到本地(和更便于訪問的)位置,從而便于下次需要時使用。但是,手工實現(xiàn)緩存是很麻煩的。以HibernateRantDao的getRantsForDay()方法為例:
public List<Rant> getRantsForDay(Date day) { return getHibernateTemplate().find("from " + RANT +" where postedDate = ?", day); } 這個方法就非常適合緩存。我們不可能讓時間倒轉(zhuǎn),到過去的某一天來添加帖子。只要被查詢的不是今天,對其他任意一天進行查詢而返回的帖子列表都是一樣的,也就沒有必要總是對數(shù)據(jù)庫進行操作來返回過去某一天的帖子列表。我們只需查詢數(shù)據(jù)庫一次,然后就可以記住結(jié)果,以備下次查詢時使用。 下面我們來修改getRantsForDay(),使用某種自制形式的緩存:
這個版本的getRantsForDay()很不好用。這個方法的實際作用是查詢指定日期的帖子,但其中大量代碼都被用于處理緩存了。而且,它還沒有直接處理緩存的一些復(fù)雜情況,比如緩存過期、刷新或溢出。
如圖5.13所示,Spring Modules對于緩存的支持涉及到一個代理,它攔截對Spring管理的Bean的一個或多個方法的調(diào)用。當(dāng)一個被代理的方法被調(diào)用時,Spring Modules Cache首先查閱一個緩存來判斷這個方法是否已經(jīng)被使用同樣參數(shù)調(diào)用過,如果是,它會返回緩存里的值,實際的方法并不會被調(diào)用;否則,實際方法會被調(diào)用,其返回值會被保存到緩存里,以備方法下一次被調(diào)用時使用。 在這一小節(jié)里,我們將使用Spring Modules Cache為RoadRantz的DAO層添加緩存功能,這樣會讓程序具有更好的性能,讓繁忙的數(shù)據(jù)庫輕松一些。 5.7.1 配置緩存方案雖然Spring Modules會提供一個代理來攔截方法并把結(jié)果保存到緩存,它并沒有提供一個實際的緩存解決方案,而是要依賴于第三方的緩存方案。可以使用的方案有多個,包括: n EHCache n GigaSpaces n JBoss Cache n JCS n OpenSymphony的OSCache n Tangosol的Coherence 我們?yōu)镽oadRantz程序選擇EHCache,主要是因為我以前使用它的經(jīng)驗及能夠從www.ibibio.org的Maven倉庫輕易獲得。無論使用哪個緩存方案,對于Spring Modules Cache的配置基本上都是一樣的。 首先要做的是新建一個Spring配置文件來聲明緩存。雖然可以把Spring Modules Cache配置放到RoadRantz程序加載的任意一個Spring上下文配置文件里,但最好還是把它們分開,所以我們要創(chuàng)建roadrantz-cache.xml來保存緩存的配置。 與Spring上下文配置文件一樣,roadrantz-cache.xml也以<beans>元素為根。但為了利用Spring Modules對EHCache的支持,我們要讓<beans>元素能夠識別ehcache命名空間: <beans xmlns="http://www./schema/beans" 我們?yōu)镽oadRantz程序選擇的是EHCache,如果想使用其他緩存方案,需要把Spring Modules命名究竟和規(guī)劃聲明修改為相應(yīng)的內(nèi)容。表5.6列出了每個命名空間及其URI和規(guī)劃URI。
表5.6 Spring Modules所支持的緩存方案的命名空間及規(guī)劃
無論選擇哪種緩存,都可以使用一些Spring配置元素在Spring里對緩存進行配置。表5.7列出了這些元素。
表5.7 Spring Modules的配置元素
在使用EHCache作為緩存方案時,需要告訴Spring到哪里尋找EHCache配置文件[5],這正是<ehcache:config>元素的用途所在: <ehcache:config configLocation="classpath:ehcache.xml" /> 在此對configLocation屬性的設(shè)置告訴Spring從程序類路徑的根位置加載EHCache的配置。 配置EHCache我們已經(jīng)配置了ehcache.xml文件,如程序清單5.12所示。 程序清單5.12 在ehcache.xml里配置EHCache。 在這段代碼里,我們配置了兩個緩存讓EHCache進行管理。<defulatCache>元素是必須有的,描述了在沒有找到其他緩存情況下所使用的緩存。<cache>元素定義了另一個緩存,可以在ehcache.xml里出現(xiàn)0次或多次(每次針對定義的一個緩存)。在此,我們只定義了rantzCache作為惟一的非默認緩存。 <defaultCache>和<cache>里指定的屬性描述了緩存的行為。表5.8列出在配置EHCache緩存時可以使用的屬性。
表5.8 EHCache的緩存配置屬性
對于RoadRantz程序,我們配置了一個默認緩存(這是EHCache要求的),還配置了一個名為rantzCache的緩存作為主緩存。兩個緩存都設(shè)置為最多可以容納500個元素(不過期),訪問頻率最低的元素會被踢出,不允許磁盤溢出[6]。 在Spring程序上下文里配置的EHCache之后,就可以聲明哪個Bean和方法應(yīng)該對結(jié)果進行緩存。首先,我們來聲明一個代理來緩存RoadRantz DAO層里方法的返回值。 5.7.2 緩存的代理Bean我們已經(jīng)知道HibernateRantDao里的getRantsForDay()方法很適合進行緩存。再回到Spring上下文定義,我們要使用<ehcache:proxy>元素把一個代理包裹到HibernateRantDao,從而緩存從getRantsForDay()返回的全部內(nèi)容: <ehcache:proxy id="rantDao" refId="rantDaoTarget"> <ehcache:caching>元素聲明哪個方法要被攔截、其返回值要保存到哪個緩存。本例中,methodName被設(shè)置為getRantsForDay(),要使用的緩存是rantzCache。 我們可以根據(jù)需要在<ehcache:proxy>里聲明多個<ehcache:cacing>來描述Bean方法的緩存。我們可以讓一個<ehcache:caching>用于所有被緩存的方法,也可以使用通配符為一個<ehcache:caching>元素指定多個方法。比如下面的<ehcache:caching>元素會代理緩存全部名稱由get開頭的方法: <ehcache:caching methodName="get*" cacheName="rantzCache" /> 把數(shù)據(jù)放到緩存里只完成了一半的工作。在經(jīng)過一段時間之后,緩存里一定會包含大量數(shù)據(jù),其中很多已經(jīng)沒有意義了。最后,這些數(shù)據(jù)應(yīng)該被清出緩存,數(shù)據(jù)緩存周期重新開始。下面我們來看一看如何在方法調(diào)用時刷新緩存。 刷新緩存<ehcache:caching>元素聲明的是要向緩存中添加數(shù)據(jù)的方法,而<ehcache:flushing>元素聲明了會清空緩存的方法。舉例來說,假設(shè)我們想在saveRant()方法被調(diào)用時清空rantzCache緩存,那么就應(yīng)該使用如下的<ehcache:flushing>元素: <ehcache:flushing methodName="saveRant" cacheName="rantzCache" /> 在默認情況下,cacheName屬性里指定的緩存會在methodName被調(diào)用之后清空,但利用when屬性可以指定清空的時機: <ehcache:flushing methodName="saveRant" cacheName="rantzCache" when="before" /> 把when屬性設(shè)置為before可以讓緩存在saveRant()被調(diào)用之前清空。 聲明一個被代理的內(nèi)部Bean注意<ehcache:proxy>的id和refId屬性。由<ehcache:proxy>生成的代理的id是rantDao,然而這是HibernateRantDao Bean的id,因此,我們需要把這個真正的Bean重命名為rantDaoTarget(由refId屬性指定)。(這與傳統(tǒng)Spring AOP代理及其目標(biāo)的命名方式是一樣的,詳情請見4.2.3小節(jié)。) 如果覺得id/refId組合有些奇怪,我們還可以把目標(biāo)Bean聲明為<ehcache:proxy>的內(nèi)部Bean。舉例來說,下面就是把HibernateRantDao配置為一個內(nèi)部Bean的<ehcache:proxy>: <ehcache:proxy id="rantDao"> 即使使用了內(nèi)部Bean,我們?nèi)匀恍枰獮槊總€要代理的Bean聲明一個<ehcache:proxy>元素,為方法聲明一個或多個<ehcache:caching>元素。對于簡單程序來說,這樣做不會有什么問題,但隨著代理緩存Bean和方法的數(shù)量不斷增加,這將意味著Spring配置里越來越多的XML。 如果對內(nèi)部Bean的方法仍然感到不快,或是需要代理多個要緩存的Bean,我們可以考慮使用Spring Modules對注解聲明緩存的支持。接下來,讓我們忘記<ehcache:proxy>,看一看Spring Modules如何支持注解驅(qū)動的緩存。 5.7.3 注解驅(qū)動的緩存除了前面介紹的基于XML的緩存配置,Spring Modules還支持使用代碼級元數(shù)據(jù)聲明緩存。這種支持有兩種形式: Java 5注解:如果目標(biāo)環(huán)境是Java 5平臺,這就是很理想的解決方案。 Jakarta公共屬性:如果目標(biāo)環(huán)境是Java 5以前的平臺,就應(yīng)該選擇它。 對于RoadRantz程序來說,其目標(biāo)環(huán)境是Java 5,所以我們要使用Java 5注解來聲明DAO層的緩存。對于緩存,Spring Modules提供了兩個注解: @Cacheable:聲明一個方法的返回值應(yīng)該被緩存。 @CacheFlush:聲明一個方法是清空緩存的觸發(fā)器。 利用@Cacheable注解,我們可以像下面這樣把getRantsForDay()聲明為要被緩存的: @Cacheable(modelId="rantzCacheModel") modelId屬性指定用于緩存方法返回值的模型,稍后我們介紹說明如何定義緩存模型,現(xiàn)在先來看一看如何使用@CacheFlush來指定saveRant()被調(diào)用時的緩存清空操作: @CacheFlush(modelId="rantzFlushModel") modelId屬性指定的刷新模型會在saveRant()方法被調(diào)用時被清空。 既然說到緩存模型和刷新模型,那么它們是從何而來的呢?<ehcache:annotateions>元素被用于啟動Spring Modules對注解的支持,我們會在roadrantzcache.xml文件里像下面這樣配置它: <ehcache:annotations> 在<ehcache:annotateions>元素里,必須配置至少一個<ehcache:caching>元素,它就定義了一個緩存模型。簡單來說,緩存模型基本上就是對ehcache.xml里配置的一個緩存的引用。本例中,我們把rantzCacheModel與名為rantzCache的緩存關(guān)聯(lián)起來,這樣一來,任何modelId是rantzCacheModel的@Cacheable都會使用名為rantzCache的緩存。 刷新模型與緩存模型相當(dāng)類似,只是它引用的是要被刷新的緩存。下面使用<ehcache:flushing>元素創(chuàng)建一個名為rantzFlushModel的刷新模型: <ehcache:annotations> 設(shè)置緩存模型與刷新模型的不同之處在于,刷新模型不僅決定要清空哪個緩存,還決定了何時清空。在默認情況下,緩存是在@CacheFlush注解的方法被調(diào)用之后清空的,但我們可以通過指定<ehcache:flushing>的when屬性來改變: <ehcache:annotations> 把when屬性設(shè)置為before之后,緩存就會@CacheFlush注解的方法被調(diào)用之前清空。 |
|