GUAVA--緩存(緩存回收)

一、緩存回收

一個比較現實的問題,咱們幾乎必定沒有足夠的內存緩存全部數據。你你必須決定:何時某個緩存項就不值得 保留了?Guava Cache 提供了三種基本的緩存回收方式:基於容量回收、定時回收和基於引用回收。java

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

若是要規定緩存項的數目不超過固定值,只需使用 CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒有使用或整體上不多使用的緩存項。——警告:在緩存項的數目達到限定值以前,緩存就可能進行回收操做——一般來講,這種狀況發生在緩存項的數目逼近限定值時。算法

另外,不一樣的緩存項有不一樣的「權重」(weights)——例如,若是你的緩存值,佔據徹底不一樣的內存空間,你可使用 CacheBuilder.weigher(Weigher)指定一個權重函數,而且用 CacheBuilder.maximumWeight(long)指定最大總重。在權重限定場景中,除了要注意回收也是在重量逼近限定值時就進行了,還要知道重量是在緩存建立時計算的,所以要考慮重量計算的複雜度。緩存

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder().maximumWeight(100000)
		.weigher(new Weigher<Key, Graph>() {
			public int weigh(Key k, Graph g) {
				return g.vertices().size();
			}
		}).build(new CacheLoader<Key, Graph>() {
			public Graph load(Key key) { // no checked exception
				return createExpensiveGraph(key);
			}
		});

2.一、定時回收(Timed Eviction)

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

  • expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收同樣。
  • expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(建立或覆蓋),則回收。若是認爲緩存數據老是在固定時候後變得陳舊不可用,這種回收方式是可取的。

2.二、測試定時回收

對定時回收進行測試時,不必定非得花費兩秒鐘去測試兩秒的過時。你可使用 Ticker 接口和 CacheBuilder.ticker(Ticker)方法在緩存中自定義一個時間源,而不是非得用系統時鐘。async

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

經過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache 能夠把緩存設置爲容許垃圾回收:函數

  • CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。由於垃圾回收僅依賴恆等式(==),使用弱引用鍵的緩存用==而不是 equals 比較鍵。
  • CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。由於垃圾回收僅依賴恆等式(==),使用弱引用值的緩存用==而不是 equals 比較值。
  • CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存須要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,咱們一般建議使用更有性能預測性的緩存大小限定(見上文,基於容量回收)。使用軟引用值的緩存一樣用==而不是 equals 比較值。

四、顯式清除

任什麼時候候,你均可以顯式地清除緩存項,而不是等到它被回收:性能

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

五、移除監聽器

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

請注意,RemovalListener 拋出的任何異常都會在記錄到日誌後被丟棄[swallowed]。ui

CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection>() {
	public DatabaseConnection load(Key key) throws Exception {
		return openConnection(key);
	}
};
RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
	public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
		DatabaseConnection conn = removal.getValue();
		conn.close(); // tear down properly
	}
};
return CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.MINUTES).removalListener(removalListener)
		.build(loader);

警告:默認狀況下,監聽器方法是在移除緩存時同步調用的。由於緩存的維護和請求響應一般是同時進行的,代價高昂的監聽器方法在同步模式下會拖慢正常的緩存請求。在這種狀況下,你可使用 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) { 
				// 沒有檢查異常
				return getGraphFromDatabase(key);
			}

			public ListenableFuture<Key, Graph> reload(final Key key, Graph prevGraph) {
				if (neverNeedsRefresh(key)) {
					return Futures.immediateFuture(prevGraph);
				} else {
					// 異步的
					ListenableFutureTask<Key, Graph> task = ListenableFutureTask
							.create(new Callable<Key, Graph>() {
								public Graph call() {
									return getGraphFromDatabase(key);
								}
							});
					executor.execute(task);
					return task;
				}
			}
		});

CacheBuilder.refreshAfterWrite(long, TimeUnit)能夠爲緩存增長自動定時刷新功能。和 expireAfterWrite相反,refreshAfterWrite 經過定時刷新可讓緩存項保持可用,但請注意:緩存項只有在被檢索時纔會真正刷新(若是 CacheLoader.refresh 實現爲異步,那麼檢索不會被刷新拖慢)。所以,若是你在緩存上同時聲明 expireAfterWrite 和 refreshAfterWrite,緩存並不會由於刷新盲目地定時重置,若是緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過時時間後也變得能夠回收。

八、LRU算法

LRU(Least recently used,最近最少使用)是核心思想是基於「若是數據最近被訪問過,那未來被訪問的概率也更高」。

  • 新數據插入到鏈表的頭部。
  • 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部。
  • 當鏈表滿的時候,將鏈表尾部的數據丟棄。

LRU緩存機制:https://www.jianshu.com/p/8bf1c8f0eea4

九、Guava 緩存底層實現

https://www.bbsmax.com/A/pRdBBGo9dn/

相關文章
相關標籤/搜索