Guava Cache:核心引數深度剖析和原始碼分析

夜雨落花發表於2020-10-04

目錄

1、核心引數概覽

2、核心引數分功能分析

2.1 初始容量initialCapacity

2.2 最大容量maximumSize

2.3 加權器weigher和最大加權值maximumWeight

2.4 寫後超時expireAfterWrite和讀後超時expireAfterRead

2.5 寫後重新整理refreshAfterWrite

2.6 併發級別concurrencyLevel

2.7 時間源ticker

2.8 快取移除監聽器removalListener

2.9 快取載入器CacheLoader


1、核心引數概覽

2、核心引數分功能分析

2.1 初始容量initialCapacity

1、主要作用:

    initialCapacity表示初始容量,在快取建立的過程中進行設定。注意事項:

  • 不可重複設定:初始容量只能設定一次有效值,否則會丟擲異常;
  • 最小值限制:初始容量應該大於等於0,否則會丟擲異常;
  • 最大值限制:initialCapacity會從2的30次方、maximumWeight、CacheBuilder的傳入值中取最小值;
  • 併發性相關:initialCapacity最好設定成段數segmentCount(segmentCount的計算參加:? )的整數倍,因為initialCapacity用來設定段的初始容量segmentCapacity,segmentCapacity取值即initialCapacity除以segmentCount並向上取整。

2、核心程式碼:

    1)CacheBuilder中設定初始容量的程式碼:

  public CacheBuilder<K, V> initialCapacity(int initialCapacity) {
    checkState(
        this.initialCapacity == UNSET_INT,
        "initial capacity was already set to %s",
        this.initialCapacity);
    checkArgument(initialCapacity >= 0);
    this.initialCapacity = initialCapacity;
    return this;
  }

    2)LocalCache構造器中initialCapacity取值:

    int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
    if (evictsBySize() && !customWeigher()) {
      initialCapacity = (int) Math.min(initialCapacity, maxWeight);
    }

    3)LocalCache構造器中initialCapacity用來計算段的初始容量:

    int segmentCapacity = initialCapacity / segmentCount;
    if (segmentCapacity * segmentCount < initialCapacity) {
      ++segmentCapacity;
    }

2.2 最大容量maximumSize

1、主要作用:

    用來形容快取的最大容量,以避免快取過多導致記憶體洩露。注意事項:

  • 實質:maximumSize主要是用來設定maxWeight,在快取內部是通過maxWeight來限制快取容量的;
  • 不可重複設定:最大容量只能設定一次有效值,否則會丟擲異常;最大容量和最大權重maxWeight只允許設定一個,否則會丟擲異常;

    和最大權重對比:

  • 相同性:在快取沒有特殊加權計算的情況下,兩者等價,只需設定任意即可;
  • 差異性1:有特殊加權值計算(指定weigher)的場景,必須也只能使用最大權重maximumWeight;
  • 差異性2:在沒有特殊加權值計算(指定weigher)的場景,必須也只能使用最大大小maximumSize;
  • 差異性3:在快取例項內部,成員變數使用的是最大權重maximumWeight,maximumSize會通過轉化成maxWeight來生效。

2、核心程式碼:

    1)CacheBuilder中設定最大容量的程式碼:

  public CacheBuilder<K, V> maximumSize(long maximumSize) {
    checkState(
        this.maximumSize == UNSET_INT, "maximum size was already set to %s", this.maximumSize);
    checkState(
        this.maximumWeight == UNSET_INT,
        "maximum weight was already set to %s",
        this.maximumWeight);
    checkState(this.weigher == null, "maximum size can not be combined with weigher");
    checkArgument(maximumSize >= 0, "maximum size must not be negative");
    this.maximumSize = maximumSize;
    return this;
  }

    2)獲取最大加權值:maximumSize和maximumWeight是否生效取決於是否指定了weigher

  long getMaximumWeight() {
    if (expireAfterWriteNanos == 0 || expireAfterAccessNanos == 0) {
      return 0;
    }
    return (weigher == null) ? maximumSize : maximumWeight;
  }

2.3 加權器weigher和最大加權值maximumWeight

1、加權的主要作用:

    加權器weigher主要用來根據快取的key-value來設定不同的加權值。加權值表示佔據的記憶體的基本單後設資料的多少。注意事項:

  • 預設值:CacheBuilder中可以不設定weigher,此時weigher為null;構造快取LocalCache例項時,如果weigher為null則使用OneWeigher.INSTANCE;
  • 和最大容量引數的關係:指定weigher時,最大容量只能使用maximumWeight;不指定weigher時,最大容量只能使用maximumSize;
  • 自定義方式:實現weigher介面

2、最大加權值的作用:

    最大加權值,在CacheBuilder中使用欄位名maximumWeight,在LocalCache中使用欄位名maxWeight,用來描述快取的最大容量。注意事項:

  • 使用場景:只有在設定了weigher的時候,才會使用maximumWeight;否則使用maximumSize;
  • 內部機制:在快取LocalCache的內部,使用的是maxWeight;如果沒有指定weighter,會將CacheBuilder中的maximumSize讀取為maxWeight;
  • 生效方式:對每個分段計算一個最大分段加權值;
  • 預設值:-1,此時不會限制快取容量;-1無法顯示設定,不設定即為-1.

3、核心程式碼:

    1)實現weigher的例子:

    private static class MyWeigher implements Weigher<String, String> {
        @Override
        public int weigh(String key, String value) {
            return value.length() / 1000;
        }
    }

    2)LocalCache中呼叫Builder的方法獲取最大加權值:

  // LocalCache的構造器中獲取maxWeight
  maxWeight = builder.getMaximumWeight();

  // CacheBuilder的getMaximumWeight方法
  long getMaximumWeight() {
    if (expireAfterWriteNanos == 0 || expireAfterAccessNanos == 0) {
      return 0;
    }
    return (weigher == null) ? maximumSize : maximumWeight;
  }

    3)對段生效的方式:

    // 構造器中maxWeight對段生效的程式碼
    // segmentCapacity = initialCapacity 除以 segmentCount 向上取整
    int segmentCapacity = initialCapacity / segmentCount;
    if (segmentCapacity * segmentCount < initialCapacity) {
      ++segmentCapacity;
    }

    // segmentSize = 不小於segmentCapacity的 最小的 2的整數冪
    // segmentSize用作段的初始容量
    int segmentSize = 1;
    while (segmentSize < segmentCapacity) {
      segmentSize <<= 1;
    }

    // 是否限制容量
    if (evictsBySize()) {
      // Ensure sum of segment max weights = overall max weights
      // 段容量基礎值 = 總容量 除以 段數 向上取整
      long maxSegmentWeight = maxWeight / segmentCount + 1;
      long remainder = maxWeight % segmentCount;
      for (int i = 0; i < this.segments.length; ++i) {
        // 從第餘數段開始,段容量減1,以保證各段容量之和等於總容量
        if (i == remainder) {
          maxSegmentWeight--;
        }
        this.segments[i] =
            createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
      }
    } else {
      // 如果未設定總的最大容量,則每個分段都不設定最大容量
      for (int i = 0; i < this.segments.length; ++i) {
        this.segments[i] =
            createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
      }
    }


  // 是否會根據容量進行淘汰
  boolean evictsBySize() {
    return maxWeight >= 0;
  }

2.4 寫後超時expireAfterWrite和讀後超時expireAfterRead

1、寫後超時的作用:

    寫後超時expireAfterWrite描述的是,快取寫入多長時間後會失效。快取失效後,如果再次方法,如果設定了載入器CacheLoader,會重新載入;如果未設定CacheLoader,或者CacheLoader未獲取到相應快取,則會返回異常,提示獲取不到相應的快取。

2、訪問後超時的作用:

    訪問後超時expireAfterAccess描述的是,快取讀取多長時間後會失效。每次訪問(包括讀和寫)都會重置時間。快取失效後,如果再次方法,如果設定了載入器CacheLoader,會重新載入;如果未設定CacheLoader,或者CacheLoader未獲取到相應快取,則會返回異常,提示獲取不到相應的快取。

3、注意事項:

  • 每次寫入的時候,也會重置訪問的時間

4、核心程式碼:

    1)記錄寫操作:

    @GuardedBy("this")
    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);
    }

    2)記錄讀操作:

    void recordRead(ReferenceEntry<K, V> entry, long now) {
      if (map.recordsAccess()) {
        entry.setAccessTime(now);
      }
      recencyQueue.add(entry);
    }

    3)快取超時淘汰:

    @GuardedBy("this")
    void expireEntries(long now) {
      drainRecencyQueue();

      ReferenceEntry<K, V> e;
      while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {
        if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
          throw new AssertionError();
        }
      }
      while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
        if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
          throw new AssertionError();
        }
      }
    }

    4)超限導致的淘汰:

    @GuardedBy("this")
    void evictEntries(ReferenceEntry<K, V> newest) {
      if (!map.evictsBySize()) {
        return;
      }

      drainRecencyQueue();

      // If the newest entry by itself is too heavy for the segment, don't bother evicting
      // anything else, just that
      if (newest.getValueReference().getWeight() > maxSegmentWeight) {
        if (!removeEntry(newest, newest.getHash(), RemovalCause.SIZE)) {
          throw new AssertionError();
        }
      }

      while (totalWeight > maxSegmentWeight) {
        ReferenceEntry<K, V> e = getNextEvictable();
        if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
          throw new AssertionError();
        }
      }
    }

2.5 寫後重新整理refreshAfterWrite

1、主要作用:

    寫後重新整理refreshAfterWrite主要作用是,在快取寫入一定時間後,再次訪問會先使用CacheLoader去重新整理快取;如果重新整理失敗,或者有其他任務正在重新整理快取,則會返回現有的快取值。注意事項:

  • 非同步和同步:重新整理是非同步進行的,但是第一次請求重新整理的服務執行緒,會阻塞等待非同步重新整理完成;
  • 預設值:如果重新整理快取失敗,則會返回現有的舊值;
  • 併發性:對於同一個key,如果已經正在重新整理了,則後續的所有任務不會阻塞,而是直接返回現在的舊值。

2、refreshAfterWrite和expireAfterWrite對比:

  • 在指定CacheLoader的情況下,refreshAfterWrite在CacheLoader載入不到時,會返回現有的舊值;expireAfterWrite在CacheLoader載入不到結果時,會丟擲異常提示獲取不到快取;
  • 在未指定CacheLoader的情況下,不允許設定refreshAfterWrite,否則會報錯;但是可以設定expireAfterWrite,但是此時在get方法中必須指定CacheLoader;
  • 併發訪問超時的情況下,refreshAfterWrite的超時機制,會使第一次訪問的執行緒阻塞等待重新整理執行緒的結果,但是重新整理過程中如果有其他執行緒訪問,會直接返回現在的舊值;而expireAfterWrite的超時機制,會清除讓現有的值,並讓全部訪問這個key的快取的執行緒,都阻塞等待非同步重新整理的結果。

3、核心程式碼:

    1)第一條執行緒發現超過refreshNanos,執行重新整理;後面的執行緒訪問到正在重新整理的快取時,直接返回舊值:

    V scheduleRefresh(
        ReferenceEntry<K, V> entry,
        K key,
        int hash,
        V oldValue,
        long now,
        CacheLoader<? super K, V> loader) {
      // 判斷: 是否需要重新整理 && 並不是正在重新整理
      if (map.refreshes()
          && (now - entry.getWriteTime() > map.refreshNanos)
          && !entry.getValueReference().isLoading()) {
        V newValue = refresh(key, hash, loader, true);
        if (newValue != null) {
          return newValue;
        }
      }
      // 不需要重新整理,或者有其他執行緒這個字重新整理,就返回現在的舊值
      return oldValue;
    }

    2)第一條觸發refresh邏輯的執行緒,阻塞等待非同步執行結果:

    @Nullable
    V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
      // 插入一個LoadingValueReference,這樣後面的執行緒訪問到的時候,可以知道這個快取正在被重新整理
      final LoadingValueReference<K, V> loadingValueReference =
          insertLoadingValueReference(key, hash, checkTime);
      if (loadingValueReference == null) {
        return null;
      }

      // 非同步執行,生成future佔位符
      ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
      // 服務執行緒阻塞等待非同步任務執行完成
      if (result.isDone()) {
        try {
          return Uninterruptibles.getUninterruptibly(result);
        } catch (Throwable t) {
          // don't let refresh exceptions propagate; error was already logged
        }
      }
      return null;
    }

    3)非同步重新整理快取的具體邏輯:

    ListenableFuture<V> loadAsync(
        final K key,
        final int hash,
        final LoadingValueReference<K, V> loadingValueReference,
        CacheLoader<? super K, V> loader) {
      final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);
      loadingFuture.addListener(
          new Runnable() {
            @Override
            public void run() {
              try {
                // 非同步任務執行完成時,回撥getAndRecordStats方法去設定快取的值,並記錄統計結果
                getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
              } catch (Throwable t) {
                logger.log(Level.WARNING, "Exception thrown during refresh", t);
                loadingValueReference.setException(t);
              }
            }
          },
          directExecutor());
      return loadingFuture;
    }

2.6 併發級別concurrencyLevel

1、主要作用:

    併發級別concurrencyLevel用來控制快取的併發處理能力。注意事項:

  • concurrencyLevel最大值是2的16次冪(65535),最小值是1,預設值是4;
  • concurrencyLevel是通過設定分段數量segmentCount,來生效的;
  • concurrencyLevel並不一定完全等同於segmentCount,segmentCount的取值還跟最大加權值maxWeight有關。

2、分段數量segmentCount的取值規則:

  • segmentCount是2的整數倍;
  • segmentCount在允許的取值範圍內取最大值;
  • concurrencyLevel的約束:1/2 * segmentCount滿足:小於concurrencyLevel ;
  • maxWeight的約束:如果maxWeight < 0(不限制快取最大容量),則對segmentCount無影響;如果設定了有效的maxWeight,則 1/2 * segmentCount 小於等於1/20 * maxWeight

3、concurrencyLevel取值的特點:

  • 在maxWeight影響不明顯的情況下(不設定maxWeight,或者maxWeight遠大於concurrencyLevel的情況),concurrencyLevel實際生效的值,是不小於它的最小的2的整數倍。例如concurrencyLevel = 7或者concurrencyLevel = 8時,如果可以忽略maxWeight的影響,segmentCount計算得到的數值為8;
  • 設定了maxWeight的情況,concurrencyLevel應該明顯小於最大加權值maxWeight:即concurrencyLevel設定成任意大於等於(1/20 * maxWeight + 1)的數值,和設定成(1/20 * maxWeight + 1)的效果是一樣的。

4、核心程式碼

    1)segmentCount的計算邏輯:

    int segmentShift = 0;
    int segmentCount = 1;
    while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
      // 這時的segmentShift還是表示segmentCount是2的多少次冪
      ++segmentShift;
      // segmentCount是滿足while條件的最大值的2倍
      segmentCount <<= 1;
    }
    // 最終的segmentShift用於取hash的高位的相應位數,用來計算尋找一個元素在哪個segment中
    this.segmentShift = 32 - segmentShift;
    // 掩碼,取hash的低位的相應位數的值,即為在segment中的角標
    segmentMask = segmentCount - 1;


  // 是否限制容量
  boolean evictsBySize() {
    return maxWeight >= 0;
  }

    2)concurrencyLevel取值限制:

  // 構造器中獲取concurrencyLevel.最大值是2的16次冪
  concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);


  // CacheBuilder的方法,用來提供設定的concurrencyLevel,或者預設的concurrencyLevel。預設為4
  int getConcurrencyLevel() {
    return (concurrencyLevel == UNSET_INT) ? DEFAULT_CONCURRENCY_LEVEL : concurrencyLevel;
  }

2.7 時間源ticker

1、主要作用:

    時間源工具Ticker是快取內部獲取時間的工具,納秒級精度。Ticker用來在快取內部測量流逝的時間,從而計算快取是否超時、是否需要重新整理等。注意事項:

  • 使用者可以實現Ticker介面,以定製自己需要的Ticker;
  • 一般在有測試需求的時候才需要定製Ticker,比如快取超時時間的1小時,可以通過讓Ticker的read方法返回的1小時後的納秒時間戳,來觀察1小時候的快取變化;
  • 預設使用Ticker.SYSTEM_TICKER,呼叫System.nanoTime()獲取時間戳。

2、重要程式碼:

    1)定製Ticker:

    private static class MyTicker extends Ticker {

        private long start = Ticker.systemTicker().read();
        private long elapsedTime = 0L;

        @Override
        public long read() {
            return start + elapsedTime;
        }

        // 開發給使用者,用來加速時間流逝
        public void setElapsedTime(long nanos) {
            this.elapsedTime = nanos;
        }
    }

2.8 快取移除監聽器removalListener

1、主要作用:

    快取移除監聽器removalListener,在快取被移除的時候會收到一條通知,通知攜帶被移除快取的key、value、移除原因。

2、移除原因RemovalCause:

  • EXPLICIT:使用者顯式移除了快取;非被驅逐,即wasEvicted方法返回false;
  • REPLACED:使用者顯式替換了快取;非被驅逐,即wasEvicted方法返回false;
  • COLLECTED:被垃圾回收器回收了key或者value。被驅逐,即wasEvicted方法返回true;
  • EXPIRED:快取超時被移除。被驅逐,即wasEvicted方法返回true;
  • SIZE:容量超限被移除。被驅逐,即wasEvicted方法返回true。

3、重要程式碼:

    1)定製RemovalListener:

    private static class MyRemoveListener implements RemovalListener<String, String> {
        @Override
        public void onRemoval(RemovalNotification<String, String> notification) {
            System.out.println(String.format("remove, key: %s, value: %s, cause: %s, wasEvicted: %s", notification.getKey(),
                    notification.getValue(), notification.getCause(), notification.wasEvicted()));
        }
    }

2.9 快取載入器CacheLoader

1、主要作用:

    CacheLoader用於載入快取。

    CacheBuilder提供了build()方法的兩種過載,提供了兩種設定CacheLoader的方式:

  • 在構造快取時設定CacheLoader:使用build(CacheLoader loader)方法構造快取,此時會構造LoadingCache的例項,傳入的loader會被設定成預設的loader;
  • 在獲取快取時指定CacheLoader 1:使用build()方法構造快取,會構造LocalCache.LocalMaualCache的例項。由於沒有指定預設的loader,需要在get方法中傳入loader,用於獲取不到快取、快取超時、快取需要重新整理時載入快取;
  • 在獲取快取時指定CacheLoader 2:如果使用build(CacheLoader loader)方法構造快取構造LoadingCache的例項,依然可以在get方法中傳入CacheLoader,此時如果存在獲取不到快取、快取超時、快取需要重新整理等情況,會優先使用get方法中傳入的CacheLoader載入快取。

2、重要程式碼:

    1)定製CacheLoader:

    private static class Loader extends CacheLoader<String, String> {

        @Override
        public String load(String key) throws Exception {
            return "value";
        }
    }

 

相關文章