范例
適用性
緩存在很多場(chǎng)景下都是相當(dāng)有用的带兜。例如枫笛,計(jì)算或檢索一個(gè)值的代價(jià)很高,并且對(duì)同樣的輸入需要不止一次獲取值的時(shí)候刚照,就應(yīng)當(dāng)考慮使用緩存刑巧。
Guava Cache與ConcurrentMap很相似,但也不完全一樣无畔。最基本的區(qū)別是ConcurrentMap會(huì)一直保存所有添加的元素啊楚,直到顯式地移除。相對(duì)地浑彰,Guava Cache為了限制內(nèi)存占用恭理,通常都設(shè)定為自動(dòng)回收元素。在某些場(chǎng)景下郭变,盡管LoadingCache 不回收元素颜价,它也是很有用的涯保,因?yàn)樗鼤?huì)自動(dòng)加載緩存。
通常來(lái)說(shuō)周伦,Guava Cache適用于:
- 你愿意消耗一些內(nèi)存空間來(lái)提升速度夕春。
- 你預(yù)料到某些鍵會(huì)被查詢(xún)一次以上。
- 緩存中存放的數(shù)據(jù)總量不會(huì)超出內(nèi)存容量专挪。(Guava Cache是單個(gè)應(yīng)用運(yùn)行時(shí)的本地緩存及志。它不把數(shù)據(jù)存放到文件或外部服務(wù)器。如果這不符合你的需求寨腔,請(qǐng)嘗試Memcached這類(lèi)工具)
如果你的場(chǎng)景符合上述的每一條速侈,Guava Cache就適合你。
如同范例代碼展示的一樣脆侮,Cache實(shí)例通過(guò)CacheBuilder生成器模式獲取锌畸,但是自定義你的緩存才是最有趣的部分。
注:如果你不需要Cache中的特性靖避,使用ConcurrentHashMap有更好的內(nèi)存效率——但Cache的大多數(shù)特性都很難基于舊有的ConcurrentMap復(fù)制潭枣,甚至根本不可能做到。
加載
在使用緩存前幻捏,首先問(wèn)自己一個(gè)問(wèn)題:有沒(méi)有合理的默認(rèn)方法來(lái)加載或計(jì)算與鍵關(guān)聯(lián)的值盆犁?如果有的話,你應(yīng)當(dāng)使用CacheLoader篡九。如果沒(méi)有谐岁,或者你想要覆蓋默認(rèn)的加載運(yùn)算,同時(shí)保留"獲取緩存-如果沒(méi)有-則計(jì)算"[get-if-absent-compute]的原子語(yǔ)義榛臼,你應(yīng)該在調(diào)用get時(shí)傳入一個(gè)Callable實(shí)例伊佃。緩存元素也可以通過(guò)Cache.put方法直接插入,但自動(dòng)加載是首選的沛善,因?yàn)樗梢愿菀椎赝茢嗨芯彺鎯?nèi)容的一致性航揉。
CacheLoader
LoadingCache是附帶CacheLoader構(gòu)建而成的緩存實(shí)現(xiàn)。創(chuàng)建自己的CacheLoader通常只需要簡(jiǎn)單地實(shí)現(xiàn)V load(K key) throws Exception方法金刁。例如帅涂,你可以用下面的代碼構(gòu)建LoadingCache:
從LoadingCache查詢(xún)的正規(guī)方式是使用get(K)方法。這個(gè)方法要么返回已經(jīng)緩存的值尤蛮,要么使用CacheLoader向緩存原子地加載新值媳友。由于CacheLoader可能拋出異常,LoadingCache.get(K)也聲明為拋出ExecutionException異常产捞。如果你定義的CacheLoader沒(méi)有聲明任何檢查型異常醇锚,則可以通過(guò)getUnchecked(K)查找緩存;但必須注意轧葛,一旦CacheLoader聲明了檢查型異常搂抒,就不可以調(diào)用getUnchecked(K)艇搀。
getAll(Iterable<? extends K>)方法用來(lái)執(zhí)行批量查詢(xún)尿扯。默認(rèn)情況下求晶,對(duì)每個(gè)不在緩存中的鍵,getAll方法會(huì)單獨(dú)調(diào)用CacheLoader.load來(lái)加載緩存項(xiàng)衷笋。如果批量的加載比多個(gè)單獨(dú)加載更高效芳杏,你可以重載CacheLoader.loadAll來(lái)利用這一點(diǎn)。getAll(Iterable)的性能也會(huì)相應(yīng)提升辟宗。
注:CacheLoader.loadAll的實(shí)現(xiàn)可以為沒(méi)有明確請(qǐng)求的鍵加載緩存值爵赵。例如,為某組中的任意鍵計(jì)算值時(shí)泊脐,能夠獲取該組中的所有鍵值空幻,loadAll方法就可以實(shí)現(xiàn)為在同一時(shí)間獲取該組的其他鍵值。校注:getAll(Iterable<? extends K>)方法會(huì)調(diào)用loadAll容客,但會(huì)篩選結(jié)果秕铛,只會(huì)返回請(qǐng)求的鍵值對(duì)。
Callable
所有類(lèi)型的Guava Cache缩挑,不管有沒(méi)有自動(dòng)加載功能但两,都支持get(K, Callable<V>)方法。這個(gè)方法返回緩存中相應(yīng)的值供置,或者用給定的Callable運(yùn)算并把結(jié)果加入到緩存中谨湘。在整個(gè)加載方法完成前,緩存項(xiàng)相關(guān)的可觀察狀態(tài)都不會(huì)更改芥丧。這個(gè)方法簡(jiǎn)便地實(shí)現(xiàn)了模式"如果有緩存則返回紧阔;否則運(yùn)算、緩存续担、然后返回"擅耽。
顯式插入
使用cache.put(key, value)方法可以直接向緩存中插入值,這會(huì)直接覆蓋掉給定鍵之前映射的值赤拒。使用Cache.asMap()視圖提供的任何方法也能修改緩存秫筏。但請(qǐng)注意,asMap視圖的任何方法都不能保證緩存項(xiàng)被原子地加載到緩存中挎挖。進(jìn)一步說(shuō)这敬,asMap視圖的原子運(yùn)算在Guava Cache的原子加載范疇之外,所以相比于Cache.asMap().putIfAbsent(K,
V)蕉朵,Cache.get(K, Callable<V>) 應(yīng)該總是優(yōu)先使用崔涂。
緩存回收
一個(gè)殘酷的現(xiàn)實(shí)是,我們幾乎一定沒(méi)有足夠的內(nèi)存緩存所有數(shù)據(jù)始衅。你你必須決定:什么時(shí)候某個(gè)緩存項(xiàng)就不值得保留了冷蚂?Guava Cache提供了三種基本的緩存回收方式:基于容量回收缭保、定時(shí)回收和基于引用回收。
基于容量的回收(size-based eviction)
如果要規(guī)定緩存項(xiàng)的數(shù)目不超過(guò)固定值蝙茶,只需使用CacheBuilder.maximumSize(long)艺骂。緩存將嘗試回收最近沒(méi)有使用或總體上很少使用的緩存項(xiàng)÷『唬——警告:在緩存項(xiàng)的數(shù)目達(dá)到限定值之前钳恕,緩存就可能進(jìn)行回收操作——通常來(lái)說(shuō),這種情況發(fā)生在緩存項(xiàng)的數(shù)目逼近限定值時(shí)蹄衷。
另外忧额,不同的緩存項(xiàng)有不同的“權(quán)重”(weights)——例如,如果你的緩存值愧口,占據(jù)完全不同的內(nèi)存空間睦番,你可以使用CacheBuilder.weigher(Weigher)指定一個(gè)權(quán)重函數(shù),并且用CacheBuilder.maximumWeight(long)指定最大總重耍属。在權(quán)重限定場(chǎng)景中托嚣,除了要注意回收也是在重量逼近限定值時(shí)就進(jìn)行了,還要知道重量是在緩存創(chuàng)建時(shí)計(jì)算的恬涧,因此要考慮重量計(jì)算的復(fù)雜度注益。
定時(shí)回收(Timed Eviction)
CacheBuilder提供兩種定時(shí)回收的方法:
- expireAfterAccess(long, TimeUnit):緩存項(xiàng)在給定時(shí)間內(nèi)沒(méi)有被讀/寫(xiě)訪問(wèn),則回收溯捆。請(qǐng)注意這種緩存的回收順序和基于大小回收一樣丑搔。
- expireAfterWrite(long, TimeUnit):緩存項(xiàng)在給定時(shí)間內(nèi)沒(méi)有被寫(xiě)訪問(wèn)(創(chuàng)建或覆蓋),則回收提揍。如果認(rèn)為緩存數(shù)據(jù)總是在固定時(shí)候后變得陳舊不可用啤月,這種回收方式是可取的。
如下文所討論劳跃,定時(shí)回收周期性地在寫(xiě)操作中執(zhí)行谎仲,偶爾在讀操作中執(zhí)行。
測(cè)試定時(shí)回收
對(duì)定時(shí)回收進(jìn)行測(cè)試時(shí)刨仑,不一定非得花費(fèi)兩秒鐘去測(cè)試兩秒的過(guò)期郑诺。你可以使用Ticker接口和CacheBuilder.ticker(Ticker)方法在緩存中自定義一個(gè)時(shí)間源,而不是非得用系統(tǒng)時(shí)鐘杉武。
基于引用的回收(Reference-based Eviction)
通過(guò)使用弱引用的鍵辙诞、或弱引用的值、或軟引用的值轻抱,Guava Cache可以把緩存設(shè)置為允許垃圾回收:
- CacheBuilder.weakKeys():使用弱引用存儲(chǔ)鍵飞涂。當(dāng)鍵沒(méi)有其它(強(qiáng)或軟)引用時(shí),緩存項(xiàng)可以被垃圾回收。因?yàn)槔厥諆H依賴(lài)恒等式(==)较店,使用弱引用鍵的緩存用==而不是equals比較鍵士八。
- CacheBuilder.weakValues():使用弱引用存儲(chǔ)值。當(dāng)值沒(méi)有其它(強(qiáng)或軟)引用時(shí)梁呈,緩存項(xiàng)可以被垃圾回收婚度。因?yàn)槔厥諆H依賴(lài)恒等式(==),使用弱引用值的緩存用==而不是equals比較值捧杉。
- CacheBuilder.softValues():使用軟引用存儲(chǔ)值陕见。軟引用只有在響應(yīng)內(nèi)存需要時(shí)秘血,才按照全局最近最少使用的順序回收味抖。考慮到使用軟引用的性能影響灰粮,我們通常建議使用更有性能預(yù)測(cè)性的緩存大小限定(見(jiàn)上文仔涩,基于容量回收)。使用軟引用值的緩存同樣用==而不是equals比較值粘舟。
顯式清除
任何時(shí)候熔脂,你都可以顯式地清除緩存項(xiàng),而不是等到它被回收:
- 個(gè)別清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有緩存項(xiàng):Cache.invalidateAll()
移除監(jiān)聽(tīng)器
通過(guò)CacheBuilder.removalListener(RemovalListener)柑肴,你可以聲明一個(gè)監(jiān)聽(tīng)器霞揉,以便緩存項(xiàng)被移除時(shí)做一些額外操作。緩存項(xiàng)被移除時(shí)晰骑,RemovalListener會(huì)獲取移除通知RemovalNotification适秩,其中包含移除原因RemovalCause鍵和值。
請(qǐng)注意硕舆,RemovalListener拋出的任何異常都會(huì)在記錄到日志后被丟棄[swallowed]秽荞。
警告:默認(rèn)情況下,監(jiān)聽(tīng)器方法是在移除緩存時(shí)同步調(diào)用的抚官。因?yàn)榫彺娴木S護(hù)和請(qǐng)求響應(yīng)通常是同時(shí)進(jìn)行的扬跋,代價(jià)高昂的監(jiān)聽(tīng)器方法在同步模式下會(huì)拖慢正常的緩存請(qǐng)求。在這種情況下凌节,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把監(jiān)聽(tīng)器裝飾為異步操作钦听。
清理什么時(shí)候發(fā)生?
使用CacheBuilder構(gòu)建的緩存不會(huì)"自動(dòng)"執(zhí)行清理和回收工作倍奢,也不會(huì)在某個(gè)緩存項(xiàng)過(guò)期后馬上清理朴上,也沒(méi)有諸如此類(lèi)的清理機(jī)制。相反娱挨,它會(huì)在寫(xiě)操作時(shí)順帶做少量的維護(hù)工作余指,或者偶爾在讀操作時(shí)做——如果寫(xiě)操作實(shí)在太少的話。
這樣做的原因在于:如果要自動(dòng)地持續(xù)清理緩存,就必須有一個(gè)線程酵镜,這個(gè)線程會(huì)和用戶操作競(jìng)爭(zhēng)共享鎖碉碉。此外,某些環(huán)境下線程創(chuàng)建可能受限制淮韭,這樣CacheBuilder就不可用了垢粮。
相反,我們把選擇權(quán)交到你手里靠粪。如果你的緩存是高吞吐的蜡吧,那就無(wú)需擔(dān)心緩存的維護(hù)和清理等工作。如果你的 緩存只會(huì)偶爾有寫(xiě)操作占键,而你又不想清理工作阻礙了讀操作昔善,那么可以創(chuàng)建自己的維護(hù)線程,以固定的時(shí)間間隔調(diào)用Cache.cleanUp()畔乙。ScheduledExecutorService可以幫助你很好地實(shí)現(xiàn)這樣的定時(shí)調(diào)度君仆。
刷新
刷新和回收不太一樣。正如LoadingCache.refresh(K)所聲明牲距,刷新表示為鍵加載新值返咱,這個(gè)過(guò)程可以是異步的。在刷新操作進(jìn)行時(shí)牍鞠,緩存仍然可以向其他線程返回舊值咖摹,而不像回收操作,讀緩存的線程必須等待新值加載完成难述。
如果刷新過(guò)程拋出異常萤晴,緩存將保留舊值,而異常會(huì)在記錄到日志后被丟棄[swallowed]龄广。
重載CacheLoader.reload(K, V)可以擴(kuò)展刷新時(shí)的行為硫眯,這個(gè)方法允許開(kāi)發(fā)者在計(jì)算新值時(shí)使用舊的值。
CacheBuilder.refreshAfterWrite(long, TimeUnit)可以為緩存增加自動(dòng)定時(shí)刷新功能择同。和expireAfterWrite相反两入,refreshAfterWrite通過(guò)定時(shí)刷新可以讓緩存項(xiàng)保持可用,但請(qǐng)注意:緩存項(xiàng)只有在被檢索時(shí)才會(huì)真正刷新(如果CacheLoader.refresh實(shí)現(xiàn)為異步敲才,那么檢索不會(huì)被刷新拖慢)裹纳。因此,如果你在緩存上同時(shí)聲明expireAfterWrite和refreshAfterWrite紧武,緩存并不會(huì)因?yàn)樗⑿旅つ康囟〞r(shí)重置剃氧,如果緩存項(xiàng)沒(méi)有被檢索,那刷新就不會(huì)真的發(fā)生阻星,緩存項(xiàng)在過(guò)期時(shí)間后也變得可以回收朋鞍。
其他特性
統(tǒng)計(jì)
CacheBuilder.recordStats()用來(lái)開(kāi)啟Guava Cache的統(tǒng)計(jì)功能已添。統(tǒng)計(jì)打開(kāi)后,Cache.stats()方法會(huì)返回
CacheStats對(duì)象以提供如下統(tǒng)計(jì)信息:
hitRate():緩存命中率滥酥;
averageLoadPenalty():加載新值的平均時(shí)間更舞,單位為納秒;
evictionCount():緩存項(xiàng)被回收的總數(shù)坎吻,不包括顯式清除缆蝉。
此外,還有其他很多統(tǒng)計(jì)信息瘦真。這些統(tǒng)計(jì)信息對(duì)于調(diào)整緩存設(shè)置是至關(guān)重要的刊头,在性能要求高的應(yīng)用中我們建議密切關(guān)注這些數(shù)據(jù)。
asMap視圖
asMap視圖提供了緩存的ConcurrentMap形式诸尽,但asMap視圖與緩存的交互需要注意:
- cache.asMap()包含當(dāng)前所有加載到緩存的項(xiàng)原杂。因此相應(yīng)地,cache.asMap().keySet()包含當(dāng)前所有已加載鍵;
- asMap().get(key)實(shí)質(zhì)上等同于cache.getIfPresent(key)弦讽,而且不會(huì)引起緩存項(xiàng)的加載污尉。這和Map的語(yǔ)義約定一致。
- 所有讀寫(xiě)操作都會(huì)重置相關(guān)緩存項(xiàng)的訪問(wèn)時(shí)間往产,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法某宪,也不包括在Cache.asMap()的集合視圖上的操作仿村。比如,遍歷Cache.asMap().entrySet()不會(huì)重置緩存項(xiàng)的讀取時(shí)間兴喂。
中斷
緩存加載方法(如Cache.get)不會(huì)拋出InterruptedException蔼囊。我們也可以讓這些方法支持InterruptedException,但這種支持注定是不完備的衣迷,并且會(huì)增加所有使用者的成本畏鼓,而只有少數(shù)使用者實(shí)際獲益。詳情請(qǐng)繼續(xù)閱讀壶谒。
Cache.get請(qǐng)求到未緩存的值時(shí)會(huì)遇到兩種情況:當(dāng)前線程加載值云矫;或等待另一個(gè)正在加載值的線程。這兩種情況下的中斷是不一樣的汗菜。等待另一個(gè)正在加載值的線程屬于較簡(jiǎn)單的情況:使用可中斷的等待就實(shí)現(xiàn)了中斷支持让禀;但當(dāng)前線程加載值的情況就比較復(fù)雜了:因?yàn)榧虞d值的CacheLoader是由用戶提供的,如果它是可中斷的陨界,那我們也可以實(shí)現(xiàn)支持中斷巡揍,否則我們也無(wú)能為力。
如果用戶提供的CacheLoader是可中斷的菌瘪,為什么不讓Cache.get也支持中斷腮敌?從某種意義上說(shuō),其實(shí)是支持的:如果CacheLoader拋出InterruptedException,Cache.get將立刻返回(就和其他異常情況一樣)糜工;此外斗这,在加載緩存值的線程中,Cache.get捕捉到InterruptedException后將恢復(fù)中斷啤斗,而其他線程中InterruptedException則被包裝成了ExecutionException表箭。
原則上钮莲,我們可以拆除包裝免钻,把ExecutionException變?yōu)镮nterruptedException,但這會(huì)讓所有的LoadingCache使用者都要處理中斷異常,即使他們提供的CacheLoader不是可中斷的渤刃。如果你考慮到所有非加載線程的等待仍可以被中斷诫舅,這種做法也許是值得的刊懈。但許多緩存只在單線程中使用,它們的用戶仍然必須捕捉不可能拋出的InterruptedException異常。即使是那些跨線程共享緩存的用戶瓢娜,也只是有時(shí)候能中斷他們的get調(diào)用挂洛,取決于那個(gè)線程先發(fā)出請(qǐng)求。
對(duì)于這個(gè)決定眠砾,我們的指導(dǎo)原則是讓緩存始終表現(xiàn)得好像是在當(dāng)前線程加載值虏劲。這個(gè)原則讓使用緩存或每次都計(jì)算值可以簡(jiǎn)單地相互切換。如果老代碼(加載值的代碼)是不可中斷的褒颈,那么新代碼(使用緩存加載值的代碼)多半也應(yīng)該是不可中斷的柒巫。
如上所述,Guava Cache在某種意義上支持中斷谷丸。另一個(gè)意義上說(shuō)堡掏,Guava Cache不支持中斷,這使得LoadingCache成了一個(gè)有漏洞的抽象:當(dāng)加載過(guò)程被中斷了刨疼,就當(dāng)作其他異常一樣處理泉唁,這在大多數(shù)情況下是可以的;但如果多個(gè)線程在等待加載同一個(gè)緩存項(xiàng)揩慕,即使加載線程被中斷了亭畜,它也不應(yīng)該讓其他線程都失敗(捕獲到包裝在ExecutionException里的InterruptedException)漩绵,正確的行為是讓剩余的某個(gè)線程重試加載贱案。為此,我們記錄了一個(gè)bug止吐。然而,與其冒著風(fēng)險(xiǎn)修復(fù)這個(gè)bug侨糟,我們可能會(huì)花更多的精力去實(shí)現(xiàn)另一個(gè)建議AsyncLoadingCache碍扔,這個(gè)實(shí)現(xiàn)會(huì)返回一個(gè)有正確中斷行為的Future對(duì)象。