Google Guava之CacheBuilder


在什么場景下需要使用緩存呢?

緩存在很多場景下都是需要使用的。比如在需要一個值的過程和代價特別高的情況下,而且對這個值的需要不止一次的情況下,我們可能就需要考慮使用緩存了。

在什么場景下需要使用本地緩存呢?

一般來說要使用本地緩存,首先,是緩存中的數據總量不會超過內存的容量。並且你願意消耗一些內存來提升速度。

那怎么實現本地緩存呢?

一般來說我們可以直接使用jdk里提供的數據結構來作為緩存,但這樣有個問題就是緩存的一些機制,比如緩存過期的淘汰策略,緩存的初始化,緩存最大容量的設置,緩存的共享等等一些列的問題需要自己去考慮和實現。

第二種方法就是我們可以使用一些業界開源的,成熟的一些第三方的工具來幫助我們實現緩存。這其中有:EHCache,cahce4j等等好多框架和工具。但從我使用的來看我認為google里guava包內的緩存工具是我使用過的最方便,簡單的緩存框架。

下面就來介紹這個Guava包內的CacheBuilder。

加載(初始化)

使用Cacheloder自動加載

LoadingCache是附帶CacheLoader構建而成的緩存實現。創建自己的CacheLoader通常只需要簡單地實現V load(K key) throws Exception方法。(當然你也可以重新實現Cacheloder里的其他方法,來擴展你緩存的功能,比如loadAll,reload等。)

簡單的一個例子:

LoadingCache<Key, String> graphs = CacheBuilder.newBuilder().maximumSize(2000).build( new CacheLoader<Key, String>() { public Graph load(Key key) throws AnyException { return createExpensiveGraph(key); } }); ... ... try { return graphs.get(key); } catch (ExecutionException e) { throw new OtherException(e.getCause()); } 

由於CacheLoader可能拋出異常,LoadingCache.get(K)也聲明為拋出ExecutionException異常。如果你定義的CacheLoader沒有聲明任何檢查型異常,則可以通過getUnchecked(K)查找緩存;但必須注意,一旦CacheLoader聲明了檢查型異常,就不可以調用getUnchecked(K)。

ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException;這個方法用來執行批量查詢。默認情況下,對每個不在緩存中的鍵,getAll方法會單獨調用CacheLoader.load來加載緩存項。如果批量的加載比多個單獨加載更高效,你可以重載CacheLoader.loadAll來利用這一點。getAll(Iterable)的性能也會相應提升。

獲取緩存-如果沒有-則計算(get-if-absent-compute)

get(K, Callable<V>)這個方法不論有沒有實現自動加載都可以使用。代碼用例如下:

Cache<Key, Graph> cache = CacheBuilder.newBuilder()
        .maximumSize(1000) .build(); // look Ma, no CacheLoader ... try { // If the key wasn't in the "easy to compute" group, we need to // do things the hard way. cache.get(key, () -> doThingsTheHardWay(key)); } catch (ExecutionException e) { throw new OtherException(e.getCause()); } 

tip:在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式"如果有緩存則返回;否則運算、緩存、然后返回"。

顯示插入

使用cache.put(key, value)方法可以直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請注意,asMap視圖的任何方法都不能保證緩存項被原子地加載到緩存中。進一步說,asMap視圖的原子運算在Guava Cache的原子加載范疇之外,所以相比於Cache.asMap().putIfAbsent(K,
V),Cache.get(K, Callable<V>) 應該總是優先使用。

緩存的回收

Guava Cache提供了三種基本的緩存回收方式:基於容量回收、定時回收和基於引用回收。

基於容量的回收(size-based eviction)

如果要規定緩存項的數目不超過固定值,只需使用CacheBuilder.maximumSize(long)
緩存將嘗試回收最近沒有使用或總體上很少使用的緩存項。——警告:在緩存項的數目達到限定值之前,緩存就可能進行回收操作——通常來說,這種情況發生在緩存項的數目逼近限定值時。

另外,不同的緩存項有不同的“權重”(weights)——例如,如果你的緩存值,占據完全不同的內存空間,你可以使用CacheBuilder.weigher(Weigher)指定一個權重函數,並且用CacheBuilder.maximumWeight(long)指定最大總重。在權重限定場景中,除了要注意回收也是在重量逼近限定值時就進行了,還要知道重量是在緩存創建時計算的,因此要考慮重量計算的復雜度。。

當cache中所有的“weight”總和達到maxKeyWeight時,將會觸發“剔除策略”。

定時回收(Timed Eviction)

CacheBuilder提供兩種定時回收的方法:

  • expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收一樣。

  • expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認為緩存數據總是在固定時候后變得陳舊不可用,這種回收方式是可取的。

基於引用的回收(Reference-based Eviction)

通過弱引用的鍵或者弱引用的值,或者軟引用的值,guava Cache可以把緩存設置為允許垃圾回收

  • CacheBuilder.weakKeys():使用過弱引用存儲鍵值。當被垃圾回收的時候,當前鍵值沒有其他引用的時候緩存項可以被垃圾回收。
  • CacheBuilder.weakValues():使用弱引用存儲值。
  • CacheBuilder.softValues():使用軟引用存儲值。軟引用就是在內存不夠是才會按照順序回收。

顯示清除

任何時候,你都可以顯式地清除緩存項,而不是等到它被回收:

  • 個別清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有緩存項:Cache.invalidateAll()

移除監聽器

通過CacheBuilder.removalListener(RemovalListener),你可以聲明一個監聽器,以便緩存項被移除時做一些額外操作。緩存項被移除時,RemovalListener會獲取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、鍵和值。

請注意,RemovalListener拋出的任何異常都會在記錄到日志后被丟棄

警告:默認情況下,監聽器方法是在移除緩存時同步調用的。因為緩存的維護和請求響應通常是同時進行的,代價高昂的監聽器方法在同步模式下會拖慢正常的緩存請求。在這種情況下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把監聽器裝飾為異步操作。

清理什么時候發生?

使用CacheBuilder構建的緩存不會"自動"執行清理和回收工作,也不會在某個緩存項過期后馬上清理,也沒有諸如此類的清理機制。相反,它會在寫操作時順帶做少量的維護工作,或者偶爾在讀操作時做——如果寫操作實在太少的話。

這樣做的原因在於:如果要自動地持續清理緩存,就必須有一個線程,這個線程會和用戶操作競爭共享鎖。此外,某些環境下線程創建可能受限制,這樣CacheBuilder就不可用了。

相反,我們把選擇權交到你手里。如果你的緩存是高吞吐的,那就無需擔心緩存的維護和清理等工作。如果你的 緩存只會偶爾有寫操作,而你又不想清理工作阻礙了讀操作,那么可以創建自己的維護線程,以固定的時間間隔調用Cache.cleanUp()。ScheduledExecutorService可以幫助你很好地實現這樣的定時調度。

刷新

刷新和回收不太一樣。正如LoadingCache.refresh(K)所聲明,刷新表示為鍵加載新值,這個過程可以是異步的。在刷新操作進行時,緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。

如果刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日志后被丟棄[swallowed]。

重載CacheLoader.reload(K, V)可以擴展刷新時的行為,這個方法允許開發者在計算新值時使用舊的值。

//有些鍵不需要刷新,並且我們希望刷新是異步完成的 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() .maximumSize(1000) .refreshAfterWrite(1, TimeUnit.MINUTES) .build( new CacheLoader<Key, Graph>() { public Graph load(Key key) { // no checked exception return getGraphFromDatabase(key); } public ListenableFuture<Key, Graph> reload(final Key key, Graph prevGraph) { if (neverNeedsRefresh(key)) { return Futures.immediateFuture(prevGraph); }else{ // asynchronous! ListenableFutureTask<Key, Graph> task=ListenableFutureTask.create(new Callable<Key, Graph>() { public Graph call() { return getGraphFromDatabase(key); } }); executor.execute(task); return task; } } }); 

其他特性

統計

CacheBuilder.recordStats()用來開啟Guava Cache的統計功能。統計打開后,Cache.stats()方法會返回CacheStats對象以提供如下統計信息:

  • hitRate():緩存命中率;
  • averageLoadPenalty():加載新值的平均時間,單位為納秒;
  • evictionCount():緩存項被回收的總數,不包括顯式清除。

此外,還有其他很多統計信息。這些統計信息對於調整緩存設置是至關重要的,在性能要求高的應用中我們建議密切關注這些數據。

asMap視圖

asMap視圖提供了緩存的ConcurrentMap形式,但asMap視圖與緩存的交互需要注意:

  • cache.asMap()包含當前所有加載到緩存的項。因此相應地,cache.asMap().keySet()包含當前所有已加載鍵;
  • asMap().get(key)實質上等同於cache.getIfPresent(key),而且不會引起緩存項的加載。這和Map的語義約定一致。
  • 所有讀寫操作都會重置相關緩存項的訪問時間,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合視圖上的操作。比如,遍歷Cache.asMap().entrySet()不會重置緩存項的讀取時間。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM