集中式記憶體快取Guava Cache學習
本文摘轉自:https://www.jianshu.com/p/64b0df87e51b,還有這篇文章寫的也不錯:http://www.cnblogs.com/peida/p/guava.html,是一個Guava學習系列,通過這幾篇文章的講解,對Guava應該有了一個清晰的認識,不過,實戰中,還需多查文件,多看官方示例demo。PS:有時比較擔心轉載的文章圖片連結過段時間會失效。部落格後臺編輯模組,csdn可以學習下微信公眾號嗎?他們的後臺編輯功能很好用的,比如:格式刷功能 / 清除格式功能等,這些很常見的功能,csdn都沒有,別告訴我你們做不出來呀,作為一家技術部落格服務公司,那就尷尬了。
背景
快取的主要作用是暫時在記憶體中儲存業務系統的資料處理結果,並且等待下次訪問使用。在日長開發有很多場合,有一些資料量不是很大,不會經常改動,並且訪問非常頻繁。但是由於受限於硬碟IO的效能或者遠端網路等原因獲取可能非常的費時。會導致我們的程式非常緩慢,這在某些業務上是不能忍的!而快取正是解決這類問題的神器!
快取在很多系統和架構中都用廣泛的應用,例如:
- CPU快取
- 作業系統快取
- HTTP快取
- 資料庫快取
- 靜態檔案快取
- 本地快取
- 分散式快取
可以說在計算機和網路領域,快取是無處不在的。可以這麼說,只要有硬體效能不對等,涉及到網路傳輸的地方都會有快取的身影。
快取總體可分為兩種 集中式快取 和 分散式快取
“集中式快取"與"分散式快取"的區別其實就在於“集中”與"非集中"的概念,其物件可能是伺服器、記憶體條、硬碟等。比如:
1.伺服器版本:
- 快取集中在一臺伺服器上,為集中式快取。
- 快取分散在不同的伺服器上,為分散式快取。
2.記憶體條版本:
- 快取集中在一臺伺服器的一條記憶體條上,為集中式快取。
- 快取分散在一臺伺服器的不同記憶體條上,為分散式快取。
3.硬碟版本:
- 快取集中在一臺伺服器的一個硬碟上,為集中式快取。
- 快取分散在一臺伺服器的不同硬碟上,為分散式快取。
而我們今天要講的是集中式記憶體快取guava cache,這是當前我們專案正在用的快取工具,研究一下感覺還蠻好用的。當然也有很多其他工具,還是看個人喜歡。oschina上面也有很多類似開源的java快取框架
正文
Guava Cache與ConcurrentMap很相似,但也不完全一樣。最基本的區別是ConcurrentMap會一直儲存所有新增的元素,直到顯式地移除。相對地,Guava Cache為了限制記憶體佔用,通常都設定為自動回收元素。在某些場景下,儘管LoadingCache 不回收元素,它也是很有用的,因為它會自動載入快取。
guava cache 載入快取主要有兩種方式:
- cacheLoader
- callable callback
cacheLoader
建立自己的CacheLoader通常只需要簡單地實現V load(K key) throws Exception
方法.
cacheLoader方式實現例項:
LoadingCache<Key, Value> cache = CacheBuilder.newBuilder()
.build(
new CacheLoader<Key, Value>() {
public Value load(Key key) throws AnyException {
return createValue(key);
}
});
...
try {
return cache.get(key);
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
從LoadingCache查詢的正規方式是使用get(K)
方法。這個方法要麼返回已經快取的值,要麼使用CacheLoader向快取原子地載入新值(通過load(String key)
方法載入)。由於CacheLoader可能丟擲異常,LoadingCache.get(K)
也宣告丟擲ExecutionException異常。如果你定義的CacheLoader沒有宣告任何檢查型異常,則可以通過getUnchecked(K)
查詢快取;但必須注意,一旦CacheLoader聲明瞭檢查型異常,就不可以呼叫getUnchecked(K)
。
Callable
這種方式不需要在建立的時候指定load方法,但是需要在get的時候實現一個Callable匿名內部類。
Callable方式實現例項:
Cache<Key, Value> cache = CacheBuilder.newBuilder()
.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, new Callable<Value>() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
而如果加上現在java8裡面的Lambda表示式會看起來舒服很多
try {
cache.get(key,()->{
return null;
});
} catch (ExecutionException e) {
e.printStackTrace();
}
所有型別的Guava Cache,不管有沒有自動載入功能,都支援get(K, Callable<V>)
方法。這個方法返回快取中相應的值,或者用給定的Callable運算並把結果加入到快取中。在整個載入方法完成前,快取項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式"如果有快取則返回;否則運算、快取、然後返回"。
當然除了上面那種被動的載入,它還提供了主動載入的方法cache.put(key, value)
,這會直接覆蓋掉給定鍵之前對映的值。使用Cache.asMap()檢視提供的任何方法也能修改快取。但請注意,asMap檢視的任何方法都不能保證快取項被原子地載入到快取中。進一步說,asMap檢視的原子運算在Guava Cache的原子載入範疇之外,所以相比於Cache.asMap().putIfAbsent(K,V)
,Cache.get(K, Callable<V>)
應該總是優先使用。
快取回收
上面有提到 Guava Cache與ConcurrentMap 不一樣的地方在於 guava cache可以自動回收元素,這在某種情況下可以更好優化資源被浪費的情況。
基於容量的回收
當快取設定CacheBuilder.maximumSize(size)
。這個size是指具體快取專案的數量而不是記憶體的大小。而且並不是說數量大於size才會回收,而是接近size就回收。
定時回收
expireAfterAccess(long, TimeUnit)
:快取項在給定時間內沒有被讀/寫訪問,則回收。請注意這種快取的回收順序和基於大小回收一樣。expireAfterWrite(long, TimeUnit)
:快取項在給定時間內沒有被寫訪問(建立或覆蓋),則回 收。如果認為快取資料總是在固定時候後變得陳舊不可用,這種回收方式是可取的。
guava cache 還提供一個Ticker方法來設定快取失效的具體時間精度為納秒級。
基於引用的回收
通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把快取設定為允許垃圾回收:
CacheBuilder.weakKeys()
:使用弱引用儲存鍵。當鍵沒有其它(強或軟)引用時,快取項可以被垃圾回收。因為垃圾回收僅依賴恆等式(==),使用弱引用鍵的快取用==而不是equals比較鍵。CacheBuilder.weakValues()
:使用弱引用儲存值。當值沒有其它(強或軟)引用時,快取項可以被垃圾回收。因為垃圾回收僅依賴恆等式(==),使用弱引用值的快取用==而不是equals比較值。CacheBuilder.softValues()
:使用軟引用儲存值。軟引用只有在響應記憶體需要時,才按照全域性最近最少使用的順序回收。考慮到使用軟引用的效能影響,我們通常建議使用更有效能預測性的快取大小限定(見上文,基於容量回收)。使用軟引用值的快取同樣用==而不是equals比較值。
顯式清除
任何時候,你都可以顯式地清除快取項,而不是等到它被回收:
- 個別清除:
Cache.invalidate(key)
- 批量清除:
Cache.invalidateAll(keys)
- 清除所有快取項:
Cache.invalidateAll()
這裡說一個小技巧,由於guava cache是存在就取不存在就載入的機制,我們可以對快取資料有修改的地方顯示的把它清除掉,然後再有任務去取的時候就會去資料來源重新載入,這樣就可以最大程度上保證獲取快取的資料跟資料來源是一致的。
移除監聽器
不要被名字所迷惑,這裡指的是移除快取的時候所觸發的監聽器。
請注意,RemovalListener丟擲的任何異常都會在記錄到日誌後被丟棄[swallowed]。
LoadingCache<K , V> cache = CacheBuilder
.newBuilder()
.removalListener(new RemovalListener<K, V>(){
@Override
public void onRemoval(RemovalNotification<K, V> notification) {
System.out.println(notification.getKey()+"被移除");
}
})
Lambda的寫法:
LoadingCache<K , V> cache = CacheBuilder
.newBuilder()
.removalListener((notification)->{
System.out.println(notification.getKey()+"已移除");
})
警告:預設情況下,監聽器方法是在移除快取時同步呼叫的。因為快取的維護和請求響應通常是同時進行的,代價高昂的監聽器方法在同步模式下會拖慢正常的快取請求。在這種情況下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)
把監聽器裝飾為非同步操作。
這裡提一下guava cache的自動回收,並不是快取項過期起馬上清理掉,而是在讀或寫的時候做少量的維護工作,這樣做的原因在於:如果要自動地持續清理快取,就必須有一個執行緒,這個執行緒會和使用者操作競爭共享鎖。此外,某些環境下執行緒建立可能受限制,這樣CacheBuilder就不可用了。
相反,我們把選擇權交到你手裡。如果你的快取是高吞吐的,那就無需擔心快取的維護和清理等工作。如果你的快取只會偶爾有寫操作,而你又不想清理工作阻礙了讀操作,那麼可以建立自己的維護執行緒,以固定的時間間隔呼叫Cache.cleanUp()
。ScheduledExecutorService
可以幫助你很好地實現這樣的定時排程。
重新整理
guava cache 除了回收還提供一種重新整理機制LoadingCache.refresh(K)
,他們的的區別在於,guava cache 在重新整理時,其他執行緒可以繼續獲取它的舊值。這在某些情況是非常友好的。而回收的話就必須等新值載入完成以後才能繼續讀取。而且重新整理是可以非同步進行的。
如果重新整理過程丟擲異常,快取將保留舊值,而異常會在記錄到日誌後被丟棄[swallowed]。
過載CacheLoader.reload(K, V)
可以擴充套件重新整理時的行為,這個方法允許開發者在計算新值時使用舊的值。
//有些鍵不需要重新整理,並且我們希望重新整理是非同步完成的
LoadingCache<Key, Value> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(
new CacheLoader<Key, Value>() {
public Graph load(Key key) { // no checked exception
return getValue(key);
}
public ListenableFuture<Value> reload(final Key key, Value value) {
if (neverNeedsRefresh(key)) {
return Futures.immediateFuture(value);
} else {
// asynchronous!
ListenableFutureTask<Value> task = ListenableFutureTask.create(new Callable<Value>() {
public Graph call() {
return getValue(key);
}
});
executor.execute(task);
return task;
}
}
});
CacheBuilder.refreshAfterWrite(long, TimeUnit)
可以為快取增加自動定時重新整理功能。和expireAfterWrite
相反,refreshAfterWrite
通過定時重新整理可以讓快取項保持可用,但請注意:快取項只有在被檢索時才會真正重新整理(如果CacheLoader.refresh
實現為非同步,那麼檢索不會被重新整理拖慢)。因此,如果你在快取上同時宣告expireAfterWrite
和refreshAfterWrite
,快取並不會因為重新整理盲目地定時重置,如果快取項沒有被檢索,那重新整理就不會真的發生,快取項在過期時間後也變得可以回收。
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()
不會重置快取項的讀取時間。
統計
guava cache為我們實現統計功能,這在其它快取工具裡面還是很少有的。
CacheBuilder.recordStats()
用來開啟Guava Cache的統計功能。統計開啟後,Cache.stats()
方法會返回CacheStats物件以提供如下統計資訊:hitRate()
:快取命中率;averageLoadPenalty()
:載入新值的平均時間,單位為納秒;evictionCount()
:快取項被回收的總數,不包括顯式清除。
此外,還有其他很多統計資訊。這些統計資訊對於調整快取設定是至關重要的,在效能要求高的應用中我們建議密切關注這些資料, 這裡我們就不一一介紹了。
最後
快取雖然是個好東西,但是一定不能濫用,一定要根據自己系統的需求來妥善抉擇。
當然 guava 除了cache這塊還有很多其它非常有用的工具。