1. 程式人生 > >集中式記憶體快取Guava Cache學習

集中式記憶體快取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 載入快取主要有兩種方式:

  1. cacheLoader
  2. 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實現為非同步,那麼檢索不會被重新整理拖慢)。因此,如果你在快取上同時宣告expireAfterWriterefreshAfterWrite,快取並不會因為重新整理盲目地定時重置,如果快取項沒有被檢索,那重新整理就不會真的發生,快取項在過期時間後也變得可以回收。

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這塊還有很多其它非常有用的工具。