集中式內存緩存 Guava Cache

背景

緩存的主要做用是暫時在內存中保存業務系統的數據處理結果,而且等待下次訪問使用。在日長開發有不少場合,有一些數據量不是很大,不會常常改動,而且訪問很是頻繁。可是因爲受限於硬盤IO的性能或者遠程網絡等緣由獲取可能很是的費時。會致使咱們的程序很是緩慢,這在某些業務上是不能忍的!而緩存正是解決這類問題的神器!java

固然也並非說你用了緩存你的系統就必定會變快,建議在用以前看一下使用緩存的9大誤區(上) 使用緩存的9大誤區(下)git

緩存在不少系統和架構中都用普遍的應用,例如:github

  • CPU緩存數據庫

  • 操做系統緩存後端

  • HTTP緩存緩存

  • 數據庫緩存服務器

  • 靜態文件緩存網絡

  • 本地緩存架構

  • 分佈式緩存框架

能夠說在計算機和網絡領域,緩存是無處不在的。能夠這麼說,只要有硬件性能不對等,涉及到網絡傳輸的地方都會有緩存的身影。

緩存整體可分爲兩種 集中式緩存 和 分佈式緩存

「集中式緩存"與"分佈式緩存"的區別其實就在於「集中」與"非集中"的概念,其對象多是服務器、內存條、硬盤等。好比:

1. 服務器版本:
  • 緩存集中在一臺服務器上,爲集中式緩存。

  • 緩存分散在不一樣的服務器上,爲分佈式緩存。

2. 內存條版本:
  • 緩存集中在一臺服務器的一條內存條上,爲集中式緩存。

  • 緩存分散在一臺服務器的不一樣內存條上,爲分佈式緩存。

3. 硬盤版本:
  • 緩存集中在一臺服務器的一個硬盤上,爲集中式緩存。

  • 緩存分散在一臺服務器的不一樣硬盤上,爲分佈式緩存。

想了解分佈式緩存能夠看一下淺談分佈式緩存那些事兒

這是幾個當前比較流行的java 分佈式緩存框架5個強大的Java分佈式緩存框架推薦

而咱們今天要講的是集中式內存緩存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):緩存項在給定時間內沒有被寫訪問(建立或覆蓋),則回 收。若是認爲緩存數據老是在固定時候後變得陳舊不可用,這種回收方式是可取的。

    1. 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這塊還有不少其它很是有用的工具。

    本文參考:https://github.com/google/guava/wiki/CachesExplained


    做者信息
    本文系力譜宿雲LeapCloud旗下MaxLeap團隊_Service&Infra成員:賈威威 【原創】
    賈威威,從過後端開發已有多年,目前主要負責MaxWon服務端部分功能的開發與設計。
    力譜宿雲LeapCloud 首發:https://blog.maxleap.cn/archi...

    相關文章
    相關標籤/搜索