Guava 是 Google 提供的一套 Java 工具包,而 Guava Cache 是該工具包中提供的一套完善的 JVM 級(jí)別高并發(fā)緩存框架;本文主要介紹它的相關(guān)功能及基本使用,文中所使用到的軟件版本:Java 1.8.0_341、Guava 32.1.3-jre。
1、簡(jiǎn)介
緩存在很多情況下非常有用。例如,當(dāng)某個(gè)值的計(jì)算或檢索代價(jià)很高,并且你需要在特定輸入下多次使用該值時(shí),就應(yīng)該考慮使用緩存。
Guava Cache 與 ConcurrentMap 類似,但并不完全相同。最基本的區(qū)別在于,ConcurrentMap 會(huì)一直保存所有添加到其中的元素,直到顯式地將它們刪除。而 Guava Cache 通常會(huì)配置自動(dòng)刪除條目,以限制其內(nèi)存占用。在某些情況下,即使不刪除條目,LoadingCache 也很有用,因?yàn)樗哂凶詣?dòng)加載條目的功能。
通常情況下,Guava Cache 適用于以下情況:
- 你愿意花費(fèi)一些內(nèi)存來提高速度。
- 你預(yù)期某些鍵有時(shí)會(huì)被多次查詢。
- 你的緩存不需要存儲(chǔ)超過內(nèi)存容量的數(shù)據(jù)。(Guava Cache 是局限于應(yīng)用程序運(yùn)行期間的本地緩存。它們不會(huì)將數(shù)據(jù)存儲(chǔ)在文件或外部服務(wù)器上。如果這不符合你的需求,可以考慮使用像Memcached 這樣的工具。)
如果你的情況符合上述每一點(diǎn),那么 Guava Cache 可能適合你。
注意:如果你不需要緩存的特性,ConcurrentHashMap 在內(nèi)存效率方面更高——但是使用任何 ConcurrentMap 幾乎不可能復(fù)制大多數(shù) Guava Cache 的特性。
2、數(shù)據(jù)加載
使用 Guava Cache 時(shí),首先要問自己一個(gè)問題:是否有合理的默認(rèn)函數(shù)來加載或計(jì)算需緩存的數(shù)據(jù)?如果是這樣,應(yīng)該使用 CacheLoader。如果沒有,或者需要覆蓋默認(rèn)函數(shù),但仍然希望具有原子的“如果不存在則計(jì)算并獲取”語義,你應(yīng)該將一個(gè) Callable 對(duì)象傳遞給 get 方法??梢灾苯邮褂?Cache.put 方法插入元素,但更推薦自動(dòng)加載數(shù)據(jù),因?yàn)檫@樣可以更容易地推斷所有緩存內(nèi)容的一致性。
2.1、CacheLoader
LoadingCache 是一個(gè)帶有 CacheLoader 的緩存。創(chuàng)建 CacheLoader 很容易,只需要實(shí)現(xiàn)方法 V load(K key) throws Exception 即可。
LoadingCache<Long, String> loadingCache = CacheBuilder.newBuilder() .maximumSize(1000) .build(new CacheLoader<Long, String>() { @Override public String load(Long key) throws Exception { //TODO: 根據(jù)業(yè)務(wù)加載數(shù)據(jù) return RandomStringUtils.randomAlphanumeric(10); } }); try { log.info(loadingCache.get(1L)); } catch (ExecutionException e) { e.printStackTrace(); }
LoadingCache 使用 get(K) 方法來獲取數(shù)據(jù)。該方法要么返回已緩存的值,要么使用 CacheLoader 來原子地加載一個(gè)新值到緩存中。由于 CacheLoader 可能會(huì)拋出異常,LoadingCache.get(K) 方法會(huì)拋出 ExecutionException 異常。(如果 CacheLoader?拋出未經(jīng)檢查異常,get(K) 方法將拋出包裝異常 UncheckedExecutionException)。也可以選擇使用 getUnchecked(K) 方法,它將所有異常都包裝在UncheckedExecutionException 中,但如果底層的 CacheLoader 拋出已檢查異常,這可能導(dǎo)致意外行為。
LoadingCache<Long, String> loadingCache = CacheBuilder.newBuilder() .maximumSize(1000) .build(new CacheLoader<Long, String>() { @Override public String load(Long key) {//拋出未檢查異常 //TODO: 根據(jù)業(yè)務(wù)加載數(shù)據(jù) return RandomStringUtils.randomAlphanumeric(10); } }); log.info(loadingCache.getUnchecked(1L));
可以使用 getAll(Iterable<? extends K>)方法執(zhí)行批量查詢。默認(rèn)情況下,getAll 會(huì)為緩存中不存在的每個(gè)鍵單獨(dú)調(diào)用 CacheLoader.load 方法。當(dāng)批量檢索比多個(gè)單獨(dú)查找更高效時(shí),可以重寫CacheLoader.loadAll 以利用此功能。getAll(Iterable)的性能將相應(yīng)提高。
2.2、Callable
所有 Guava Cache,無論是 LoadingCache 還是非 LoadingCache,都支持 get(K, Callable<V>) 方法。該方法返回與緩存中鍵相關(guān)聯(lián)的值,或者從指定的 Callable 計(jì)算它并將其添加到緩存中。在加載完成之前,與此緩存關(guān)聯(lián)的任何可觀察狀態(tài)都不會(huì)被修改。該方法為傳統(tǒng)的“如果有緩存,則返回;否則創(chuàng)建、緩存并返回”模式提供了一個(gè)簡(jiǎn)單的替代方案。
Cache<Long, String> cache = CacheBuilder.newBuilder() .maximumSize(1000) .build(); try { String s = cache.get(1L, new Callable<String>() { @Override public String call() throws Exception { //TODO: 根據(jù)業(yè)務(wù)加載數(shù)據(jù) return RandomStringUtils.randomAlphanumeric(10); } }); log.info(s); } catch (ExecutionException e) { e.printStackTrace(); }
2.3、直接插入
可以使用 cache.put(key, value) 方法直接將值插入到緩存中。這會(huì)覆蓋緩存中指定鍵的的任何先前條目。還可以使用 Cache.asMap() 視圖公開的任何 ConcurrentMap 方法更改緩存。請(qǐng)注意,asMap 視圖上的任何方法都不會(huì)自動(dòng)將條目加載到緩存中。此外,視圖上的原子操作在緩存自動(dòng)加載的范圍之外運(yùn)行,因此在使用 CacheLoader 或 Callable 加載值的緩存中,始終應(yīng)優(yōu)先選擇 Cache.get(K, Callable<V>) 而不是 Cache.asMap().putIfAbsent()。
3、數(shù)據(jù)淘汰
現(xiàn)實(shí)情況是,我們幾乎肯定沒有足夠的內(nèi)存來緩存所有可能的內(nèi)容。你必須決定:什么時(shí)候不值得保留緩存條目?Guava提供了三種數(shù)據(jù)淘汰方式:基于大小的淘汰、基于時(shí)間的淘汰和基于引用的淘汰。
3.1、基于容量的淘汰
?如果你的緩存不應(yīng)該超過一定大小,只需使用 CacheBuilder.maximumSize(long) 。緩存將嘗試淘汰最近未被使用或使用頻率很低的條目。警告:在達(dá)到限制之前,緩存可能會(huì)淘汰條目,通常是在緩存大小接近限制時(shí)。
或者,如果不同的緩存條目具有不同的“權(quán)重”——例如,如果你的緩存值具有截然不同的內(nèi)存占用,你可以使用 CacheBuilder.weigher(Weigher) 來指定一個(gè)權(quán)重函數(shù),并使用CacheBuilder.maximumWeight(long) 來設(shè)置最大的緩存權(quán)重。除了與 maximumSize 相同的注意事項(xiàng)外,請(qǐng)注意權(quán)重是在條目創(chuàng)建時(shí)計(jì)算的,并且在此后是靜態(tài)的。
Cache<Long, String> cache = CacheBuilder.newBuilder() .maximumWeight(100000) .weigher(new Weigher<Long, String>() { @Override public int weigh(Long key, String value) { return value.getBytes().length; } }).build();
3.2、基于時(shí)間的淘汰
CacheBuilder 提供了兩種基于時(shí)間淘汰數(shù)據(jù)的方法:
expireAfterAccess(long, TimeUnit):在最后一次讀取或?qū)懭霔l目后,僅在指定的持續(xù)時(shí)間過去后才淘汰條目。需要注意的是,條目的淘汰順序類似于基于大小的淘汰策略。
expireAfterWrite(long, TimeUnit):在條目創(chuàng)建或最近一次替換值之后,僅在指定的持續(xù)時(shí)間過去后才淘汰條目。如果緩存數(shù)據(jù)在一段時(shí)間后變得過時(shí),這種方式可能是可取的。
3.3、基于引用的淘汰
Guava 允許通過對(duì)鍵或值使用弱引用和對(duì)值使用軟引用來設(shè)置緩存,從而利用垃圾回收來淘汰數(shù)據(jù)。
- CacheBuilder.weakKeys() 使用弱引用來存儲(chǔ)鍵。這意味著當(dāng)鍵沒有其他(強(qiáng)或軟)引用時(shí),條目可以被垃圾回收。由于垃圾回收僅依賴于內(nèi)存地址相等性,這導(dǎo)致整個(gè)緩存使用(==)來比較鍵,而不是equals()方法。
- CacheBuilder.weakValues() 使用弱引用來存儲(chǔ)值。這意味著當(dāng)值沒有其他(強(qiáng)或軟)引用時(shí),條目可以被垃圾回收。由于垃圾回收僅依賴于內(nèi)存地址相等性,這導(dǎo)致整個(gè)緩存使用(==)來比較值,而不是equals()方法。
- CacheBuilder.softValues() 使用軟引用來存儲(chǔ)值。以軟引用方式引用的對(duì)象會(huì)根據(jù)內(nèi)存需求以全局最近最少使用的方式進(jìn)行垃圾回收。由于使用軟引用可能會(huì)影響性能,我們通常建議使用更可預(yù)測(cè)的 maximum cache size 替代。使用 softValues() 將導(dǎo)致值使用(==)來比較,而不是 equals() 方法。
3.4、顯式刪除
在任何時(shí)候,你可以顯式地使緩存條目失效,而不是等待條目被淘汰??梢酝ㄟ^以下方式實(shí)現(xiàn):
單個(gè)失效:使用 Cache.invalidate(key)
批量失效:使用 Cache.invalidateAll(keys)
全部失效:使用 Cache.invalidateAll()
3.5、刪除監(jiān)聽器
你可以為緩存指定一個(gè)刪除監(jiān)聽器(RemovalListener),以在條目被移除時(shí)執(zhí)行某些操作,通過 CacheBuilder.removalListener(RemovalListener)?方法指定刪除監(jiān)聽器。RemovalListener 會(huì)接收到一個(gè)RemovalNotification 對(duì)象,其中包含了 RemovalCause、鍵和值的信息。
需要注意的是,任何由 RemovalListener 拋出的異常都會(huì)被記錄(使用 Logger 時(shí))并被忽略。
Cache<Long, String> cache = CacheBuilder.newBuilder() .maximumSize(1000) .removalListener(new RemovalListener<Long, String>() { @Override public void onRemoval(RemovalNotification<Long, String> notification) { log.info(notification.toString()); } }) .build();
警告:默認(rèn)情況下,移除監(jiān)聽器操作是同步執(zhí)行的。由于緩存維護(hù)通常在正常緩存操作期間執(zhí)行,因此移除監(jiān)聽器可能會(huì)降低緩存的速度!如果需要移除監(jiān)聽器,請(qǐng)使用RemovalListeners.asynchronous(RemovalListener, Executor) 方法來裝飾 RemovalListener,這樣可以以異步的方式運(yùn)行。
3.6、數(shù)據(jù)清理時(shí)機(jī)
使用 CacheBuilder 構(gòu)建的緩存不會(huì)“自動(dòng)”執(zhí)行清理和逐出值,也不會(huì)在值過期后立即執(zhí)行清理和逐出值,也不會(huì)執(zhí)行任何類似操作。相反,它會(huì)在寫入操作期間執(zhí)行少量維護(hù),或者在偶爾的讀取操作期間(如果寫入很少)執(zhí)行少量維護(hù)。
原因是:如果我們想要連續(xù)執(zhí)行緩存維護(hù),我們需要?jiǎng)?chuàng)建一個(gè)線程,它的操作將與用戶操作競(jìng)爭(zhēng)共享鎖。此外,某些環(huán)境限制了線程的創(chuàng)建,這將使 CacheBuilder 在該環(huán)境中無法使用。
相反,我們將選擇權(quán)交到你手中。如果你的緩存是高吞吐量的,那么你無需擔(dān)心執(zhí)行緩存維護(hù)來清除過期條目等問題。如果你的緩存只偶爾進(jìn)行寫操作,并且不想讓清理阻塞緩存讀取,你可以創(chuàng)建自己的維護(hù)線程,定期調(diào)用 Cache.cleanUp() 方法。
如果要為很少進(jìn)行寫入操作的緩存安排定期緩存維護(hù),請(qǐng)使用 ScheduledExecutorService。
3.7、刷新
刷新(Refreshing)與淘汰(Eviction)并不完全相同。根據(jù) LoadingCache.refresh(K) 的定義,刷新一個(gè)鍵會(huì)加載該鍵的新值,這可能是異步的。在鍵正在刷新的過程中,舊值(如果存在)仍然會(huì)被返回,這與淘汰操作不同,淘汰操作會(huì)導(dǎo)致獲取操作等待直到新值加載完成。
如果在刷新過程中發(fā)生異常,舊值將被保留,異常將被記錄并忽略。
可以根據(jù)業(yè)務(wù)需要,重寫 CacheLoader 的?CacheLoader.reload(K, V) 方法來重新定義刷新操作;這允許你在計(jì)算新值時(shí)使用舊值。
LoadingCache<Integer, String> loadingCache = CacheBuilder.newBuilder() .maximumSize(1000) .refreshAfterWrite(1, TimeUnit.MINUTES) .build(new CacheLoader<Integer, String>() { @Override public String load(Integer key) throws Exception { //TODO: 根據(jù)業(yè)務(wù)加載數(shù)據(jù) return RandomStringUtils.randomAlphanumeric(10); } @Override public ListenableFuture<String> reload(Integer key, String oldValue) throws Exception { if (neverNeedsRefresh(key)) {//不需要刷新 return Futures.immediateFuture(oldValue); } else { //異步刷新 ListenableFutureTask<String> task = ListenableFutureTask.create(new Callable<String>() { public String call() { return RandomStringUtils.randomAlphanumeric(10); } }); executorService.execute(task); return task; } } });
可以使用 CacheBuilder.refreshAfterWrite(long, TimeUnit) 為緩存添加自動(dòng)定時(shí)刷新。與 expireAfterWrite 不同,refreshAfterWrite 會(huì)使一個(gè)鍵在指定的時(shí)間后變?yōu)榭伤⑿聽顟B(tài),但只有在查詢?cè)摋l目時(shí)才會(huì)實(shí)際啟動(dòng)刷新(如果 CacheLoader.reload 被實(shí)現(xiàn)為異步,則查詢不會(huì)因刷新而變慢)。因此,可以在同一個(gè)緩存上同時(shí)指定 refreshAfterWrite 和 expireAfterWrite,以便在條目變?yōu)榭伤⑿聽顟B(tài)時(shí)不會(huì)盲目地重置過期計(jì)時(shí)器,如果一個(gè)條目在變?yōu)榭伤⑿聽顟B(tài)后沒有被查詢,它就允許過期。
4、特點(diǎn)
4.1、統(tǒng)計(jì)信息
使用 CacheBuilder.recordStats() 可以為 Guava Cache 打開統(tǒng)計(jì)信息收集功能。Cache.stats() 方法返回一個(gè) CacheStats 對(duì)象,該對(duì)象提供了諸如以下統(tǒng)計(jì)信息:
- hitRate():返回命中次數(shù)與請(qǐng)求次數(shù)之比。
- averageLoadPenalty():平均加載新值所花費(fèi)的時(shí)間(以納秒為單位)。
- evictionCount():緩存淘汰的數(shù)量。
還有許多其他的統(tǒng)計(jì)信息。這些統(tǒng)計(jì)信息在緩存調(diào)優(yōu)中非常重要,我們建議在性能關(guān)鍵的應(yīng)用程序中密切關(guān)注這些統(tǒng)計(jì)信息。
4.2、asMap
可以使用 Cache 的 asMap 視圖將任何緩存視為 ConcurrentMap,但是 asMap 視圖與緩存的交互需要一些說明。
- cache.asMap() 包含當(dāng)前加載在緩存中的所有條目。例如,cache.asMap().keySet() 包含當(dāng)前加載的所有鍵。
- asMap().get(key) 基本等同于 cache.getIfPresent(key),并且不會(huì)導(dǎo)致值被加載。這與 Map 的約定一致。
- 訪問時(shí)間會(huì)被讀取和寫入操作重置(包括 Cache.asMap().get(Object) 和 Cache.asMap().put(K, V)),但不會(huì)被 containsKey(Object) 或其他操作所重置。因此,遍歷 cache.asMap().entrySet() 不會(huì)重置條目的訪問時(shí)間。
5、簡(jiǎn)單使用
5.1、引入依賴
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> </dependency>
5.2、簡(jiǎn)單使用
public static void main(String[] args) { LoadingCache<Long, String> loadingCache = CacheBuilder.newBuilder() .initialCapacity(1000) .maximumSize(10000) .expireAfterWrite(10, TimeUnit.MINUTES) .refreshAfterWrite(3, TimeUnit.MINUTES) .recordStats() .build(new CacheLoader<Long, String>() { @Override public String load(Long key) throws Exception {//拋出已檢查異常 //TODO: 根據(jù)業(yè)務(wù)加載數(shù)據(jù) return RandomStringUtils.randomAlphanumeric(10); } }); try { log.info(loadingCache.get(1L)); } catch (ExecutionException e) { e.printStackTrace(); } }
?文章來源:http://www.zghlxwxcb.cn/news/detail-746082.html
?
參考:https://github.com/google/guava/wiki/CachesExplained。文章來源地址http://www.zghlxwxcb.cn/news/detail-746082.html
到了這里,關(guān)于Guava Cache 介紹的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!