Guava Cache 總結


想對Guava cache部分進行總結,但思索之后,文檔才是最全面、詳細的。所以,決定對guava文檔進行翻譯。

英文地址如下:https://github.com/google/guava/wiki/CachesExplained

花費了一些時間進行翻譯,翻譯的水平有待提高,有些地方翻譯的不准確,因為有些沒有實際用到,所以無法給出清晰的解釋。

如果對您有幫助,莫感欣慰!!!

 

一 概要

Guava cache是google開發的,目前被常用在單機上,如果是分布式,它就無能為力了。廢話不多說,下面開始進入正文。

 

二內存解釋

Example

 1 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
 2        .maximumSize(1000)
 3        .expireAfterWrite(10, TimeUnit.MINUTES)
 4        .removalListener(MY_LISTENER)
 5        .build(
 6            new CacheLoader<Key, Graph>() {
 7              public Graph load(Key key) throws AnyException {
 8                return createExpensiveGraph(key);
 9              }
10            });

應用性

緩存在許多的地方非常的有用。比如:當一個計算或是查詢一個值花費很大代價時,或者,你需要多次用到一個值時,你應該考慮使用緩存。
Cache 跟ConcurrentMap 很像,但不一樣。最大的功能上的區別,ConcurrentMap允許所有的元素直到被手動移除為止,一直存在。另一方面,Cache為了限制內存的占用,通常會自動地移除值。某些時候,LoadingCache 即使不驅除元素,但由於他自動導入緩存的特點,它也是十分有用的。

一般情況下,當滿足以下場景時:
・希望花費一下內存來提高速度
・有些keys會被多次查詢
・你的cache保存的東西不會超過你機器的內存量
此時,你應該選擇Guava cache

獲得一個Cache 用上面的code例子就可以了,但是自定義一個cache 會更有趣。

渲染

問自己關於你的內存的第一個問題應該是:有什么默認的方法來導入或是計算key的值嗎。如果是這樣的話,你應使用CacheLoader 。如果不是的話,或者說,你需要覆蓋掉默認的方法,但你仍想保留“存在就直接獲取,不存在就去計算”這種機制時,你應該往get方法調用中傳一個callable 。使用Cache.put 可以直接插入元素,但是從所有數據緩存一致性方面來說,使用自動的緩存導入方法更加簡單。

使用CacheLoader

一個LoadingCache 就是關聯了一個CacheLoader 的緩存。創建一個CacheLoader 就跟實現方法V load(K key) throws Exception 一樣簡單。你可以用如下的例子來創建LoadingCache :

 1 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
 2        .maximumSize(1000)
 3        .build(
 4            new CacheLoader<Key, Graph>() {
 5              public Graph load(Key key) throws AnyException {
 6                return createExpensiveGraph(key);
 7              }
 8            });
 9 
10 ...
11 try {
12   return graphs.get(key);
13 } catch (ExecutionException e) {
14   throw new OtherException(e.getCause());
15 }

查詢LoadingCache 的權威方法是用get(K) 。如果已經換存了值,就會直接返回;如果沒有,就會使用CacheLoader 來往緩存中自動導入一個新值。因為CacheLoader 會拋出Exception ,LoadingCache.get(K)可能會拋出ExecutionException 。你也可以用getUnchecked(K) ,它在UncheckedExecutionException 中包裝了所有的UncheckedExecutionException ,但是,如果CacheLoader 拋出了 checked exceptions的話,會導致奇怪的行為發生。

 1 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
 2        .expireAfterAccess(10, TimeUnit.MINUTES)
 3        .build(
 4            new CacheLoader<Key, Graph>() {
 5              public Graph load(Key key) { // no checked exception
 6                return createExpensiveGraph(key);
 7              }
 8            });
 9 
10 ...
11 return graphs.getUnchecked(key);

體積查詢可以用方法getAll(Iterable<? extends K>) 。默認情況下,getAll 會對CacheLoader.load 產生一個單獨的調用,對cache中每個不存在緩存值的key ,進行取值。當體積的查詢已經比單個查詢效率更高時,你可以通過覆蓋CacheLoader.loadAll 方法,來開發它。

注意:你可以寫一個CacheLoader.loadAll 的實現為那些沒有特殊指定的key來導入值。比如:如果計算某些group中的任意key的值,會給你group內所有key的值,loadAll 也許會同時導入group內其他key的值。

From a Callable

所有Guava緩存,無論是否是導入,都支持get(K, Callable<V>) 方法。這個方法返回內存中這個key關聯的值,或是用Callable 接口計算得到的值並將它加入內存中。知道load() 使用,對內存的修改才有了一個可觀察的狀態。這個方法為“如果緩存了,返回緩存之;沒有緩存則創建,緩存並放回”這個模式提供了一個簡單的替代品。

 1 Cache<Key, Value> cache = CacheBuilder.newBuilder()
 2     .maximumSize(1000)
 3     .build(); // look Ma, no CacheLoader
 4 ...
 5 try {
 6   // If the key wasn't in the "easy to compute" group, we need to
 7   // do things the hard way.
 8   cache.get(key, new Callable<Value>() {
 9     @Override
10     public Value call() throws AnyException {
11       return doThingsTheHardWay(key);
12     }
13   });
14 } catch (ExecutionException e) {
15   throw new OtherException(e.getCause());
16 }

直接插入
值必須用cache.put(key, value) 方法來插入到緩存中。這個覆寫了內存中制定key的元素。值的變化也可以使用被Cache.asMap() 暴露出來的、ConcurrentMap 的任意的一個方法。注意的是,asMap 視圖中沒有任何方法會讓鍵值對自動導入到內存中,所以使用Cache.get(K, Callable<V>) 與使用CacheLoader 或是 Callable 來導入內存的Cache.asMap().putIfAbsent相比,前者更好。

 

驅逐
殘酷的事實是我們沒有足夠的內存緩存所有東西。你必須決定:何時內存值不值得保存了。Guava 提供三種驅逐方式:基於大小,基於時間,基於引用。

容量驅逐
如果你緩存的值的數量不應該超過一定的數量,那么就用CacheBuilder.maximumSize(long) 方法。緩存會驅逐最近沒被使用的,或是不常用的。警告:內存可能會在數量超過前,將鍵值對驅逐,基本上是當數量達到限定值。


如果內存的鍵值對有不通的權重時,它們會交替執行,比如:如果你的內存值有完全不同的內存覆蓋范圍,你可以制定一個權重的函數CacheBuilder.weigher(Weigher) 和一個最大緩存權重的函數CacheBuilder.maximumWeight(long) 。此外,正如maximumSize 所要求的,要意識到權重時每回創建時計算的,並且,那之后,是靜態的。

 1 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
 2        .maximumWeight(100000)
 3        .weigher(new Weigher<Key, Graph>() {
 4           public int weigh(Key k, Graph g) {
 5             return g.vertices().size();
 6           }
 7         })
 8        .build(
 9            new CacheLoader<Key, Graph>() {
10              public Graph load(Key key) { // no checked exception
11                return createExpensiveGraph(key);
12              }
13            });

 

超時驅逐
CacheBuilder 提供兩種超時驅逐:
expireAfterAccess(long, TimeUnit) 只用最后被讀過或是寫過的內存,經歷過存活時間之后,才會死亡。注意鍵值對被驅逐的時間容量驅逐很相似。
expireAfterWrite(long, TimeUnit) 當被創建的鍵值對或是最近被替換過的,經過一段存活期間后,會走向死亡。這個可用於經歷過一段期間后,緩存的數據變得過期數據,這樣場景下使用。

Testing Timed Eviction

測試超時驅逐不是很難,也不必花上2秒鍾去測試一個2秒超時。使用Ticker 接口和 CacheBuilder.ticker(Ticker) 方法在你的cache 中指定時間,而不是去等待系統時鍾的2秒。

基於引用的驅逐
Guava 允許你建立基於垃圾回收的緩存,可以使用弱引用和軟引用。
:Java中的引用分為四種:強、軟、弱、虛
強引用:Java之中普遍存在,如Object object = new Object() 只要引用存在,垃圾回收器永遠不會回收掉被引用的對象
軟引用:描述一些有用,但非必須的對象。在系統將要發生內存溢出時,會將這些對象放進回收范圍之內,進行回收
弱引用:描述非必需的對象,強度比軟引用弱,無論當前內存是否充足,垃圾回收時都會對其進行回收
虛醫用:最弱的一種引用關系。設置虛引用,唯一的目的就是,在這個對象唄收集器回收時收到一個系統通知
引自《深入理解Java虛擬機-周志華 )


・CacheBuilder.weakKeys() 使用弱引用來保存key值。如果沒有其他引用指向這個key,那么它將允許被垃圾收集器回收掉。既然垃圾回收僅依賴於恆等式的一致,這就導致整個緩存用 == 來比較key,而不是equals()。
・CacheBuilder.weakValues() 使用弱引用來保存value值。如果沒有其他引用指向這個value,那么它將允許被垃圾收集器回收掉。既然垃圾回收僅依賴於恆等式的一致,這就導致整個緩存用 == 來比較value,而不是equals()。
・CacheBuilder.softValues() 用軟引用包裝值。應對內存的需求,軟引對象使用最近最少使用條例,來進行垃圾回收。因為使用軟引用的性能上的關系,我們通常建議使用最大緩存數量。softValues() 的使用會導致使用整個緩存用 == 比較value,而不是equals()。

監視移除
你會制定一個監視器,可以通過CacheBuilder.removalListener(RemovalListener) ,來監視鍵值對在緩存中被移除。RemovalListener 獲得了一個RemovalNotification, 它指定了RemovalCause ,鍵和值。
注意,任何被RemovalListener 拋出的異常都會被打進log里。

 1 CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
 2   public DatabaseConnection load(Key key) throws Exception {
 3     return openConnection(key);
 4   }
 5 };
 6 RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
 7   public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
 8     DatabaseConnection conn = removal.getValue();
 9     conn.close(); // tear down properly
10   }
11 };
12 
13 return CacheBuilder.newBuilder()
14   .expireAfterWrite(2, TimeUnit.MINUTES)
15   .removalListener(removalListener)
16   .build(loader);


警告:監視器的操作默認是同步的,因此,內存的保持一般來說都是正常操作。花費(時間)較大的監視器會拖慢緩存的功能。如果,你有一個花費(時間)較大的監視器,異步地使用RemovalListeners.asynchronous(RemovalListener, Executor) 來裝飾RemovalListener 。


什么時候發生清空操作?
用CacheBuilder 建立的緩存不會發生清空,不會自動驅逐值,不會當值過期后立即清除,不會清除任何排序的東西。相反,在讀寫操作發生后,它會有短暫的保留。


原因如下:如果要緩存一直可用,那么我們需要創建一個線程,它的操作需要user的操作來完成。此外,一些環境限制我們創建線程,這樣,會導致CacheBuilder 不可用。
相反呢,我們讓您來決定。如果緩存有比較高的吞吐量,那么你不必擔心緩存一直可用會清理掉過期的鍵值對。如果你的緩存,僅僅的寫操作,你不想讓清空來鎖住緩存的讀取,你會希望創建你自己的保持線程,以常規的間隔來調用Cache.cleanUp() 。
如果你想為幾乎只有寫操作的緩存來定制常規的內存保持,那么就用ScheduledExecutorService 。


刷新
刷新和驅逐不太一樣。正如LoadingCache.refresh(K) 中指定的,刷新key導入一個新值,可能是異步地操作。和驅逐做對比,當刷新時,強制查詢直到獲取新值時,返回的仍是舊值。
如果刷新時有異常發生,異常會被記錄在log中。
CacheLoader 會以通過覆蓋CacheLoader.reload(K, V) 這個方法來使用刷新。這個方法允許你在計算新值時使用舊值。

 1 // Some keys don't need refreshing, and we want refreshes to be done asynchronously.
 2 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
 3        .maximumSize(1000)
 4        .refreshAfterWrite(1, TimeUnit.MINUTES)
 5        .build(
 6            new CacheLoader<Key, Graph>() {
 7              public Graph load(Key key) { // no checked exception
 8                return getGraphFromDatabase(key);
 9              }
10 
11              public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
12                if (neverNeedsRefresh(key)) {
13                  return Futures.immediateFuture(prevGraph);
14                } else {
15                  // asynchronous!
16                  ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
17                    public Graph call() {
18                      return getGraphFromDatabase(key);
19                    }
20                  });
21                  executor.execute(task);
22                  return task;
23                }
24              }
25            });


使用CacheBuilder.refreshAfterWrite(long, TimeUnit) 方法可以將時間刷新加入到緩存中。和expireAfterWrite 相比較,refreshAfterWrite 會讓一個值在指定的時間段之后進行刷新,但是刷新也只有當鍵值對被查詢時才會開始。所以,舉例子來說,你可以同時指定refreshAfterWrite 和 expireAfterWrite ,所以當鍵值對可以被刷新時,驅逐計時器不會盲目地被重置,所以,當一個鍵值對可以被刷新時,但是此時沒有被查詢,那么,它將會被驅逐。

特性

統計數據
通過CacheBuilder.recordStats() 你可以為Guava 緩存打開數據收集。Cache.stats() 方法返回一個Cache.stats() 對象,這個對象提供了統計數據,如:
・hitRate() 返回采樣數的比率
・averageLoadPenalty() 導入新值平均花費時長 單位:納秒
・evictionCount() 緩存驅逐的個數
此外還有許多其他的統計數據。這些統計數據在緩存優化方面啟動決定性作用,我們建議在性能很重要的應用中,留心這些統計數據。

asMap
你可以將緩存看做是一個使用asMap 視圖的ConcurrentMap 。但是,asMap 視圖和緩存如何交互需要下面的一些解釋。
・cache.asMap() 包含了所有現在導入緩存中的鍵值對。所以,比如,cache.asMap().keySet() 包含了所有導入的key
・asMap().get(key) 本質上與cache.getIfPresent(key) 相等,從不會引起值的導入。這個和Map相比,是一致的。
・讀寫操作會導致access time被重置。但containsKey(Object) 和Cache.asMap() 操作不會導致重置發生。舉例子來說,用cache.asMap().entrySet() 來迭代不會導致access time被重置。

中斷

像get() 這樣的導入方法永遠不會拋出InterruptedException。不過,我們可以設計這些方法來支持InterruptedException 。但是,我們的支持並不是完整的,強制地在所用用戶上產生花銷只會收益很少。具體來說,比如讀取。

get 把那些請求的、未緩存的值大體分為兩類:那些導入的的值和那些等待另一個線程導入的值。這兩者以不同方式支持中斷。簡單的方法是等待另一個正在執行的線程完事后,再進行導入。這里呢,我們就會進入可中斷的等待。比較難的方法是我們自己導入值。我們用用戶定義的CacheLoader 。如果它支持中斷,那么我們可以支持中斷;如果不行,那么我們也不能支持中斷。

那么為什么當提供的CacheLoader 支持中斷,而自定義的不支持呢?某種意義上來說,我們支持中斷。如果CacheLoader 拋出中斷異常,所有關於key 的調用會立即返回。此外,get 會在導入線程中存儲中斷標記位。驚奇的是,InterruptedException 被包裝在ExecutionException 中。

原則上講,我們可以不為你包裝這個異常。然而,這將導致強迫所有LoadingCache 用戶去處理InterruptedException ,即使是那些從未拋出中斷異常的、CacheLoader 的實現。也許你考慮那些非導入線程的登台可以誒中斷是值得的,但是需要緩存只是單一線程。他們額用戶必須仍要catch不可能的InterruptedException 。

在這部分我們的原則是讓緩存在所有調用的線程中導入值。這個原則讓每個調用中再計算值變得簡單。如果舊代碼不可被中斷,那么,或許對於新代碼來說也是不可被中斷。

我說過我們在某種意義上支持中斷。在另一層(讓LoadingCache 作為有漏洞的抽象)來說,我們不支持中斷。如果導入線程被中斷了,我們很可能將這個異常看做其他異常。這個,在很多地方來說,沒有大礙。但是當多次調用get 等待返回值時,就會出錯。雖然,剛巧要計算值得操作被中斷了,其他的需要這個值的一些操作不會被執行。然而,這些調用者收到InterruptedException (包裝在ExecutionException中), 即使導入沒有將失敗作為終止。正確的行為將是遺留下來的一個線程再次進行嘗試。關於我們有個一個bug列表(https://github.com/google/guava/issues/1122)。然而,修正的話也有一定風險。並非是修正問題,我們會投入額外的精力到被推薦的AsyncLoadingCache 中,它面對中斷會做出正確的行為,同時返回Future 對象。

 


免責聲明!

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



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