分散式系統緩存系列之guava cache

来源:https://www.cnblogs.com/linlinismine/archive/2018/07/22/9349343.html
-Advertisement-
Play Games

guava是google的一個開源java框架,其github地址是 https://github.com/google/guava。guava工程包含了若幹被Google的 Java項目廣泛依賴的核心庫,例如:集合 [collections] 、緩存 [caching] 、原生類型支持 [prim ...


    guava是google的一個開源java框架,其github地址是 https://github.com/google/guava。guava工程包含了若幹被Google的 Java項目廣泛依賴的核心庫,例如:集合 [collections] 、緩存 [caching] 、原生類型支持 [primitives support] 、併發庫 [concurrency libraries] 、通用註解 [common annotations] 、字元串處理 [string processing] 、I/O 等等。 所有這些工具每天都在被Google的工程師應用在產品服務中。 其中caching這一塊是我常用的模塊的之一,今天就來分享一下我對guava cache的一些見解。

 

   guava cache使用簡介

     guava cache 是利用CacheBuilder類用builder模式構造出兩種不同的cache載入方式CacheLoader,Callable,共同邏輯都是根據key是載入value。不同的地方在於CacheLoader的定義比較寬泛,是針對整個cache定義的,可以認為是統一的根據key值load value的方法,而Callable的方式較為靈活,允許你在get的時候指定load方法。看以下代碼

Cache<String,Object> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(10, TimeUnit.SECONDS).maximumSize(500).build();

         cache.get("key", new Callable<Object>() { //Callable 載入
            @Override
            public Object call() throws Exception {
                return "value";
            }
        });

        LoadingCache<String, Object> loadingCache = CacheBuilder.newBuilder()
                .expireAfterAccess(30, TimeUnit.SECONDS).maximumSize(5)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return "value";
                    }
                });

 

    這裡面有幾個參數expireAfterWrite、expireAfterAccess、maximumSize其實這幾個定義的都是過期策略。expireAfterWrite適用於一段時間cache可能會發先變化場景。expireAfterAccess是包括expireAfterWrite在內的,因為read和write操作都被定義的access操作。另外expireAfterAccess,expireAfterAccess都是受到maximumSize的限制。當緩存的數量超過了maximumSize時,guava cache會要據LRU演算法淘汰掉最近沒有寫入或訪問的數據。這裡的maximumSize指的是緩存的個數並不是緩存占據記憶體的大小。 如果想限制緩存占據記憶體的大小可以配置maximumWeight參數。

      看代碼:

  CacheBuilder.newBuilder().weigher(new Weigher<String, Object>() {

              @Override
              public int weigh(String key, Object value) {
                  return 0;  //the value.size()
              }
          }).expireAfterWrite(10, TimeUnit.SECONDS).maximumWeight(500).build();

   weigher返回每個cache value占據記憶體的大小,這個大小是由使用者自身定義的,並且put進記憶體時就已經確定後面就再不會發生變動。maximumWeight定義了所有cache value加起的weigher的總和不能超過的上限。

    註意一點就是maximumWeight與maximumSize兩者只能生效一個是不能同時使用的!

 

   guava cache的設計

    guava cache作為一個被廣泛使用的緩存組件,設計上它有哪些過人之處?

    先看下cache的類實現定義

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {....} 

    我們看到了ConcurrentMap,所以我們知道了一點guava cache基於ConcurrentHashMap的基礎上設計。所以ConcurrentHashMap的優點它也具備。既然實現了      ConcurrentMap那再看下guava cache中的Segment的實現是怎樣?

 

   我們看到guava cache 中的Segment本質是一個ReentrantLock。內部定義了table,wirteQueue,accessQueue定義屬性。其中table是一個ReferenceEntry原子類數組,裡面就存放了cache的內容。wirteQueue存放的是對table的寫記錄,accessQueue是訪問記錄。guava cache的expireAfterWrite,expireAfterAccess就是藉助這個兩個queue來實現的。

  瞭解了guava cache的大概存儲結構,下麵看通過對cache的操作來進行更深入的瞭解。

   put(key,val)操作。

  public V put(K key, V value) {
    checkNotNull(key);
    checkNotNull(value);
    int hash = hash(key);
    return segmentFor(hash).put(key, hash, value, false);
  }

  設置緩存大概的過程:根據key 哈希到對應的segment,然後對segment加鎖lock(),然後獲取segment.table對應的結點

int index = hash & (table.length() - 1);
ReferenceEntry<K, V> first = table.get(index);

  之後入隊的過程和hashMap的入隊過程類似。入隊完之後還會進行相關操作比如更新accessQueue和wiriteQueue,累加totalWeight

 void recordWrite(ReferenceEntry<K, V> entry, int weight, long now) {
      // we are already under lock, so drain the recency queue immediately
      drainRecencyQueue();
      totalWeight += weight;

      if (map.recordsAccess()) {
        entry.setAccessTime(now);
      }
      if (map.recordsWrite()) {
        entry.setWriteTime(now);
      }
      accessQueue.add(entry);
      writeQueue.add(entry);
    }

  get(key)操作 。

     第一步也是先定位到所在segment

V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
    int hash = hash(checkNotNull(key));
    return segmentFor(hash).get(key, hash, loader);
  }

   判斷key對應的ReferenceEntry存在

  ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {
            long now = map.ticker.read();
            V value = getLiveValue(e, now);
            if (value != null) {
              recordRead(e, now);
              statsCounter.recordHits(1);
              return scheduleRefresh(e, key, hash, value, now, loader);
            }
            ValueReference<K, V> valueReference = e.getValueReference();
            if (valueReference.isLoading()) {
              return waitForLoadingValue(e, key, valueReference);
            }
          
getLiveValue(e, now)如果返回了null就表示當前cache已經過期了,不為null時recordRead(e, now)記錄最新訪問時間為now,然後統計命中率。scheduleRefresh(e, key, hash, value, now, loader)相當於一個雙重檢查,再次檢查cache需不需要刷新,如果需要刷新看不看不能馬上拿到新值。
如果可以返回新值,否直接拿原值返回。
這時註意valueReference.isLoading()為true的時候就表示有其它線程正在更新該cache,其它所有線程都要wait到這個線程loading完
才能返回。

key對應的ReferenceEntry不存在:緩存沒有載入進來或者已經被remove掉。
      return lockedGetOrLoad(key, hash, loader);

  lockedGetOrLoad執行邏輯是先加鎖lock(),判斷當前是否有其它線程在loading該cache,如果有等待其載入完畢然後返回。否自己執行loader把值設進cache中然後返回。   

try {
          // Synchronizes on the entry to allow failing fast when a recursive load is
          // detected. This may be circumvented when an entry is copied, but will fail fast most
          // of the time.
          synchronized (e) {
            return loadSync(key, hash, loadingValueReference, loader);
          }
        } finally {
          statsCounter.recordMisses(1);
        }

  

    guava cache的淘汰策略

     guava cache總體來說有四種淘汰策略。

     1、size-based 基本於使用量。

      當緩存個數超過CacheBuilder.maximumSize(long)設置的值時,優先淘汰最近沒有使用或者不常用的元素。同理CacheBuilder.maximumWeight(long)也是一樣邏輯。

     2、timed eviction 基於時間驅逐。

       expireAfterAccess(long, TimeUnit)僅在指定上一次讀/更新操作過了指定持續時間之後才考慮淘汰,淘汰邏輯與size-based是類似的。優先淘汰最近沒有使用或者不常用的元素

     expireAfterWrite(long, TimeUnit) 僅在指定上一次寫/更新操作過了指定持續時間之後才考慮淘汰,淘汰邏輯與size-based是類似的。優先淘汰最近沒有使用或者不常用的元素

    3、Reference-based Eviction 基本於引用驅逐

        在JDK1.2之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Refernce)、虛引用(Phantom Reference)。四種引用強度依次減弱。這四種引用除了強引用(Strong Reference)之外,其它的引用所對應的對象來JVM進行GC時都是可以確保被回收的。所以通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置為允許垃圾回收:

  • CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恆等式(==),使用弱引用鍵的緩存用==而不是equals比較鍵。
  • CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恆等式(==),使用弱引用值的緩存用==而不是equals比較值。
  • CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應記憶體需要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,我們通常建議使用更有性能預測性的緩存大小限定(使用軟引用值的緩存同樣用==而不是equals比較值)

        這樣的好處就是當記憶體資源緊張時可以釋放掉到緩存的記憶體。註意!CacheBuilder如果沒有指明預設是強引用的,GC時如果沒有元素到達指定的過期時間,記憶體是不能被回收的。

   4、顯示刪除

   任何時候,你都可以顯式地清除緩存項,而不是等到它被回收:

       提一下guava cache 是怎麼觸發元素回收的。guava的元素回收與其它的一些框架不一樣比如redis,redis是起額外的線程去回收元素。而guava是進行get,put操作的時候順便把元素回收的。這樣比一般的緩存另起線程監控清理相比,可以減少開銷,但如果長時間沒有調用方法的話,會導致不能及時的清理釋放記憶體空間的問題。回收時主要處理四個Queue:1. keyReferenceQueue;2. valueReferenceQueue;3. writeQueue;4. accessQueue。前兩個queue是因為WeakReference、SoftReference被垃圾回收時加入的,清理時只需要遍歷整個queue,將對應的項從LocalCache中移除即可,這裡keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference。而對後面兩個Queue,只需要檢查是否配置了相應的expire時間,然後從頭開始查找已經expire的Entry,將它們移除即可。

     總的來說,guava cache基於ConcurrentHashMap的優秀設計借鑒,在高併發場景支持線程安全,使用Reference引用命令,保證了GC的可回收到相應的數據,有效節省空間;同時write鏈和access鏈的設計,能更靈活、高效的實現多種類型的緩存清理策略,包括基於容量的清理、基於時間的清理、基於引用的清理等;

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • <!DOCTYPE html> 1、定義: DOCTYPE標簽是一種標準通用標記語言的文檔類型聲明,它的目的是要告訴標準通用標記語言解析器,它應該使用什麼樣的文檔類型定義(DTD)來解析文檔。<!DOCTYPE> 聲明必須是 HTML 文檔的第一行,位於 <html> 標簽之前。 2、作用: 聲明文 ...
  • functionName(parameter1, parameter2, parameter3) { // 要執行的代碼…… } 參數規則 JavaScript 函數定義時形參沒有指定數據類型。 JavaScript 函數對實參的類型不會進行檢測。 JavaScript 函數對實參的個數不會進行檢測 ...
  • 一、引入 相信很多人都遇到過敏感信息需要做部分隱藏功能,大多數都是用特殊符號去替換。 正好今天我又遇到這樣的前端顯示的需求,正好把相關JS記錄下來,方便下次再用。 二、JS部分 三、應用實例 1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta ...
  • 前言 上一篇文章以v-on指令綁定click事件為例介紹了v-on指令的使用方法,本文介紹一下v-on綁定事件的一些屬性的使用方法。 v-on綁定指令屬性 .stop屬性 阻止單擊事件繼續向上傳播(簡單點說就是不讓父節點及父節點以上的節點事件觸發),本示例如果沒有stop屬性,父節點和爺爺節點事件將 ...
  • // 6.2.4 組合使用構造函數和原型模式————創建自定義對象的方法: function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "C ...
  • <!-- 解決圖片旋轉 --> <script src="/libs/jquery/jquery.min.js"></script> <script src="/libs/exif/exif.js"></script> // html <input type="file" accept="image ...
  • 緩存這個東西相信大家工作中都接觸得比較多,相應的在不同場景下也會遇到各種各樣的問題。下麵我列舉幾種可能會遇到的問題並提供一些解決建議。 1、如何把海量數據存放在緩存中並提供快速查詢 現實中我們的緩存通常都是以string,map,array,list,set,tree等具體的類型或者集合存放記憶體中, ...
  • 在簡單工廠模式中產品的創建統一在工廠類的靜態工廠方法中創建,體現了面形對象的封裝性,客戶程式不需要知道產品產生的細節,也體現了面向對象的單一職責原則(SRP),這樣在產品很少的情況下使用起來還是很方便, 但是如果產品很多,並且不斷的有新產品加入,那麼就會導致靜態工廠方法變得極不穩定,每次加入一個新產 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...