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