91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何正確使用Caffeine Cache

發布時間:2021-10-12 13:38:47 來源:億速云 閱讀:205 作者:iii 欄目:編程語言

這篇文章主要介紹“如何正確使用Caffeine Cache”,在日常操作中,相信很多人在如何正確使用Caffeine Cache問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何正確使用Caffeine Cache”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

一、關于 Caffeine Cache

在推薦服務中,雖然允許少量請求因計算超時等原因返回默認列表。但從運營指標來說,越高的“完算率”意味著越完整的算法效果呈現,也意味著越高的商業收益。(完算率類比視頻的完播率,成功完成整個推薦線上流程計算的請求次數/總請求次數)

為了能夠盡可能快地完成計算,多級緩存方案已經成為推薦線上服務的標配。其中本地緩存顯得尤為重要,而 Caffeine Cache 就是近幾年脫穎而出的高性能本地緩存庫。Caffeine Cache 已經在 Spring Boot 2.0 中取代了 Google Guava 成為默認緩存框架,足見其成熟和可靠。

關于 Caffeine 的介紹文章有很多,不再累述,可閱讀文末的參考資料了解 Caffeine 的簡述、性能基準測試結果、基本 API 用法和 Window-TinyLFU 緩存算法原理等。雖然接觸 Caffeine 的時間不長,但其簡潔的 API 和如絲般順滑的異步加載能力簡直不要太好用。而本菜鳥在使用的過程中也踩了一些坑,使用不當甚至緩存也能卡得和磁盤 IO 一樣慢。

經過一番學習嘗試,總算了解到 Caffeine Cache 如絲般順滑的奧秘,總結下來分享一下。

二、Caffeine Cache 配置套路

使用 Caffeine Cache,除了 Spring 中常見的 @EnableCache、@Cacheable 等注解外,直接使用 Caffeine.newBuilder().build() 方法創建 LoadingCache 也是推薦服務常用的方式。

我們先來看看 Caffeine#builder 都有哪些配置套路:

如何正確使用Caffeine Cache

2.1 追問三連

2.1.1 ObjectPool

當然可以,光腳的不怕穿鞋的,上線后別走……

2.1.2 expireAfterWrite、expireAfterAccess 都配置?

雖然 expireAfterWrite 和 expireAfterAccess 同時配置不報錯,但 access 包含了 write,所以選一個就好了親。

2.1.3 reference-based 驅逐有啥特點?

只要配置上都會使用 == 來比較對象相等,而不是 equals;還有一個非常重要的配置,也是決定緩存如絲般順滑的秘訣:刷新策略 refreshAfterWrite。該配置使得 Caffeine 可以在數據加載后超過給定時間時刷新數據。下文詳解。

機智如我在 Builder 上也能踩坑

和 lombok 的 builder 不同,Caffeine#builder 的策略調用兩次將會導致運行時異常!這是因為 Caffeine 構建時每個策略都保存了已設置的標記位,所以重復設置并不是覆蓋而是直接拋異常:

public Caffeine<K, V> maximumWeight(@NonNegative long maximumWeight) {
  requireState(this.maximumWeight == UNSET_INT,
      "maximum weight was already set to %s", this.maximumWeight);
  requireState(this.maximumSize == UNSET_INT,
      "maximum size was already set to %s", this.maximumSize);
  this.maximumWeight = maximumWeight;
  requireArgument(maximumWeight >= 0, "maximum weight must not be negative");
  return this;
}

比如上述代碼,maximumWeight() 調用兩次的話就會拋出異常并提示 maximum weight was already set to xxx。

三、Caffeine Cache 精華

3.1 get 方法都做了什么?

首先在實現類 LocalLoadingCache<K, V> 中可以看到;

default @Nullable V get(K key) {
    return cache().computeIfAbsent(key, mappingFunction());
}

但突然發現這個 get 方法沒有實現類!Why?我們跟蹤 cache() 方法就可以發現端倪:

public BoundedLocalCache<K, V> cache() {
    return cache;
}
public UnboundedLocalCache<K, V> cache() {
    return cache;
}

根據調用 Caffeine.newBuilder().build() 的過程,決定了具體生成的是 BoundedLocalCache 還是 UnboundedLocalCache;

判定 BoundedLocalCache 的條件如下:

public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
    @NonNull CacheLoader<? super K1, V1> loader) {
  requireWeightWithWeigher();
 
  @SuppressWarnings("unchecked")
  Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
  return isBounded() || refreshes()
      ? new BoundedLocalCache.BoundedLocalLoadingCache<>(self, loader)
      : new UnboundedLocalCache.UnboundedLocalLoadingCache<>(self, loader);
}

其中的 isBounded()、refreshes() 方法分別如下:

boolean isBounded() {
  return (maximumSize != UNSET_INT)
      || (maximumWeight != UNSET_INT)
      || (expireAfterAccessNanos != UNSET_INT)
      || (expireAfterWriteNanos != UNSET_INT)
      || (expiry != null)
      || (keyStrength != null)
      || (valueStrength != null);
}
boolean refreshes() {
  // 調用了 refreshAfter 就會返回 false
  return refreshNanos != UNSET_INT;
}

可以看到一般情況下常規的配置都是 BoundedLocalCache。所以我們以它為例繼續看 BoundedLocalCache#computeIfAbsent 方法吧:

  public @Nullable V computeIfAbsent(K key,
    Function<? super K, ? extends V> mappingFunction,
    boolean recordStats, boolean recordLoad) {
  // 常用的 LoadingCache#get 方法 recordStats、recordLoad 都為 true
  // mappingFunction 即 builder 中傳入的 CacheLoader 實例包裝
 
  requireNonNull(key);
  requireNonNull(mappingFunction);
  // 默認的 ticker read 返回的是 System.nanoTime();
  // 關于其他的 ticker 見文末參考文獻,可以讓使用者自定義超時的計時方式
  long now = expirationTicker().read();
 
  // data 是 ConcurrentHashMap<Object, Node<K, V>>
  // key 根據代碼目前都是 LookupKeyReference 對象
  // 可以發現 LookupKeyReference 保存的是 System.identityHashCode(key) 結果
  // 關于 identityHashCode 和 hashCode 的區別可閱讀文末參考資料
  Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
  if (node != null) {
    V value = node.getValue();
    if ((value != null) && !hasExpired(node, now)) {
      // isComputingAsync 中將會判斷當前是否為異步類的緩存實例
      // 是的話再判斷 node.getValue 是否完成。BoundedLocaCache 總是返回 false
      if (!isComputingAsync(node)) {
        // 此處在 BoundedLocaCache 中也是直接 return 不會執行
        tryExpireAfterRead(node, key, value, expiry(), now);
        setAccessTime(node, now);
      }
 
      // 異步驅逐任務提交、異步刷新操作
      // CacheLoader#asyncReload 就在其中的 refreshIfNeeded 方法被調用
      afterRead(node, now, recordStats);
      return value;
    }
  }
  if (recordStats) {
    // 記錄緩存的加載成功、失敗等統計信息
    mappingFunction = statsAware(mappingFunction, recordLoad);
  }
 
  // 這里2.8.0版本不同實現類生成的都是 WeakKeyReference
  Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
 
  // 本地緩存沒有,使用加載函數讀取到緩存
  return doComputeIfAbsent(key, keyRef, mappingFunction,
    new long[] { now }, recordStats);
}

上文中 hasExpired 判斷數據是否過期,看代碼就很明白了:是通過 builder 的配置 + 時間計算來判斷的。

boolean hasExpired(Node<K, V> node, long now) {
  return
    (expiresAfterAccess() &&
      (now - node.getAccessTime() >= expiresAfterAccessNanos()))
  | (expiresAfterWrite() &&
      (now - node.getWriteTime() >= expiresAfterWriteNanos()))
  | (expiresVariable() &&
      (now - node.getVariableTime() >= 0));
}

繼續看代碼,doComputeIfAbsent 方法主要內容如下:

  @Nullable V doComputeIfAbsent(K key, Object keyRef,
    Function<? super K, ? extends V> mappingFunction,
    long[] now, boolean recordStats) {
  @SuppressWarnings("unchecked")
  V[] oldValue = (V[]) new Object[1];
  @SuppressWarnings("unchecked")
  V[] newValue = (V[]) new Object[1];
  @SuppressWarnings("unchecked")
  K[] nodeKey = (K[]) new Object[1];
  @SuppressWarnings({"unchecked", "rawtypes"})
  Node<K, V>[] removed = new Node[1];
 
  int[] weight = new int[2]; // old, new
  RemovalCause[] cause = new RemovalCause[1];
 
  // 對 data 這個 ConcurrentHashMap 調用 compute 方法,計算 key 對應的值
  // compute 方法的執行是原子的,并且會對 key 加鎖
  // JDK 注釋說明 compute 應該短而快并且不要在其中更新其他的 key-value
  Node<K, V> node = data.compute(keyRef, (k, n) -> {
    if (n == null) {
      // 沒有值的時候調用 builder 傳入的 CacheLoader#load 方法
      // mappingFunction 是在 LocalLoadingCache#newMappingFunction 中創建的
      newValue[0] = mappingFunction.apply(key);
      if (newValue[0] == null) {
        return null;
      }
      now[0] = expirationTicker().read();
 
      // builder 沒有指定 weigher 時,這里默認為 SingletonWeigher,總是返回 1
      weight[1] = weigher.weigh(key, newValue[0]);
      n = nodeFactory.newNode(key, keyReferenceQueue(),
          newValue[0], valueReferenceQueue(), weight[1], now[0]);
      setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));
      return n;
    }
 
    // 有值的時候對 node 實例加同步塊
    synchronized (n) {
      nodeKey[0] = n.getKey();
      weight[0] = n.getWeight();
      oldValue[0] = n.getValue();
 
      // 設置驅逐原因,如果數據有效直接返回
      if ((nodeKey[0] == null) || (oldValue[0] == null)) {
        cause[0] = RemovalCause.COLLECTED;
      } else if (hasExpired(n, now[0])) {
        cause[0] = RemovalCause.EXPIRED;
      } else {
        return n;
      }
 
      // 默認的配置 writer 是 CacheWriter.disabledWriter(),無操作;
      // 自己定義的 CacheWriter 一般用于驅逐數據時得到回調進行外部數據源操作
      // 詳情可以參考文末的資料
      writer.delete(nodeKey[0], oldValue[0], cause[0]);
      newValue[0] = mappingFunction.apply(key);
      if (newValue[0] == null) {
        removed[0] = n;
        n.retire();
        return null;
      }
      weight[1] = weigher.weigh(key, newValue[0]);
      n.setValue(newValue[0], valueReferenceQueue());
      n.setWeight(weight[1]);
 
      now[0] = expirationTicker().read();
      setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));
      setAccessTime(n, now[0]);
      setWriteTime(n, now[0]);
      return n;
    }
  });
 
  // 剩下的代碼主要是調用 afterWrite、notifyRemoval 等方法
  // 進行后置操作,后置操作中將會再次嘗試緩存驅逐
  // ...
  return newValue[0];
}

看完上面的代碼,遇到這些問題也就心里有數了。

3.2 緩存的數據什么時候淘汰?

顯式調用 invalid 方法時;弱引用、軟引用可回收時;get 方法老值存在且已完成異步加載后調用 afterRead。

get 方法老值不存在,調用 doComputeIfAbsent 加載完數據后調用 afterWrite。

3.3 CacheLoader#load和 CacheLoader#asyncReload 有什么區別?

首先 CacheLoader#load 方法是必須提供的,緩存調用時將是同步操作(回顧上文 data.compute 方法),會阻塞當前線程。

而 CacheLoader#asyncReload 需要配合builder#refreshAfterWrite 使用這樣將在computeIfAbsent->afterRead->refreshIfNeeded 中調用,并異步更新到 data 對象上;并且,load 方法沒有傳入oldValue,而 asyncReload 方法提供了oldValue,這意味著如果觸發 load 操作時,緩存是不能保證 oldValue 是否存在的(可能是首次,也可能是已失效)。

3.4 加載數據耗時較長,對性能的影響是什么?

CacheLoader#load 耗時長,將會導致緩存運行過程中查詢數據時阻塞等待加載,當多個線程同時查詢同一個 key 時,業務請求可能阻塞,甚至超時失敗;

CacheLoader#asyncReload 耗時長,在時間周期滿足的情況下,即使耗時長,對業務的影響也較小

3.5 說好的如絲般順滑呢?

首要前提是外部數據查詢能保證單次查詢的性能(一次查詢天長地久那加本地緩存也于事無補);然后,我們在構建 LoadingCache 時,配置 refreshAfterWrite 并在 CacheLoader 實例上定義 asyncReload 方法;

靈魂追問:只有以上兩步就夠了嗎?

機智的我突然覺得事情并不簡單。還有一個時間設置的問題,我們來看看:

如果 expireAfterWrite 周期 < refreshAfterWrite 周期會如何?此時查詢失效數據時總是會調用 load 方法,refreshAfterWrite 根本沒用!

如果 CacheLoader#asyncReload 有額外操作,導致它自身實際執行查詢耗時超過 expireAfterWrite 又會如何?還是 CacheLoader#load 生效,refreshAfterWrite 還是沒用!

所以絲滑的正確打開方式,是 refreshAfterWrite 周期明顯小于 expireAfterWrite 周期,并且 CacheLoader#asyncReload 本身也有較好的性能,才能如絲般順滑地加載數據。此時就會發現業務不斷進行 get 操作,根本感知不到數據加載時的卡頓!

3.6 用本地緩存會不會出現緩存穿透?怎么防止?

computeIfAbsent 和 doComputeIfAbsent 方法可以看出如果加載結果是 null,那么每次從緩存查詢,都會觸發 mappingFunction.apply,進一步調用 CacheLoader#load。從而流量會直接打到后端數據庫,造成緩存穿透。

防止的方法也比較簡單,在業務可接受的情況下,如果未能查詢到結果,則返回一個非 null 的“假對象”到本地緩存中。

靈魂追問:如果查不到,new 一個對象返回行不行?

key 范圍不大時可以,builder 設置了 size-based 驅逐策略時可以,但都存在消耗較多內存的風險,可以定義一個默認的 PLACE_HOLDER 靜態對象作為引用。

靈魂追問:都用同一個假對象引用真的大丈夫(沒問題)?

這么大的坑本菜鳥怎么能錯過!緩存中存的是對象引用,如果業務 get 后修改了對象的內容,那么其他線程再次獲取到這個對象時,將會得到修改后的值!鬼知道那個深夜定位出這個問題的我有多興奮(蒼蠅搓手)。

當時緩存中保存的是 List<Item>,而不同線程中對這些 item 的 score 進行了不同的 set 操作,導致同一個 item 排序后的分數和順序變幻莫測。本菜鳥一度以為是推薦之神降臨,冥冥中加持 CTR 所以把 score 變來變去。

靈魂追問:那怎么解決緩存被意外修改的問題呢?怎么 copy 一個對象呢?

So easy,就在 get 的時候 copy 一下對象就好了。

靈魂追問4:怎么 copy 一個對象?……停!咱們以后有機會再來說說這個淺拷貝和深拷貝,以及常見的拷貝工具吧,聚焦聚焦……

3.7 某次加載數據失敗怎么辦,還能用之前的緩存值嗎?

根據 CacheLoader#load和 CacheLoader#asyncReload 的參數區別,我們可以發現:

應該在 asyncReload 中來處理,如果查詢數據庫異常,則可以返回 oldValue 來繼續使用之前的緩存;否則只能通過 load 方法中返回預留空對象來解決。使用哪一種方法需要根據具體的業務場景來決定。

【踩坑】返回 null 將導致 Caffeine 認為該值不需要緩存,下次查詢還會繼續調用 load 方法,緩存并沒生效。

3.8 多個線程同時 get 一個本地緩存不存在的值,會如何?

根據代碼可以知道,已經進入 doComputeIfAbsent 的線程將阻塞在 data.compute 方法上;

比如短時間內有 N 個線程同時 get 相同的 key 并且 key 不存在,則這 N 個線程最終都會反復執行 compute 方法。但只要 data 中該 key 的值更新成功,其他進入 computeIfAbsent 的線程都可直接獲得結果返回,不會出現阻塞等待加載;

所以,如果一開始就有大量請求進入 doComputeIfAbsent 阻塞等待數據,就會造成短時間請求掛起、超時的問題。由此在大流量場景下升級服務時,需要考慮在接入流量前對緩存進行預熱(我查我自己,嗯),防止瞬時請求太多導致大量請求掛起或超時。

靈魂追問:如果一次 load 耗時 100ms,一開始有 10 個線程冷啟動,最終等待時間會是 1s 左右嗎?

其實……要看情況,回顧一下 data.compute 里面的代碼:

if (n == null) {
    // 這部分代碼其他后續線程進入后已經有值,不再執行
}
synchronized (n) {
  // ...
 
  if ((nodeKey[0] == null) || (oldValue[0] == null)) {
    cause[0] = RemovalCause.COLLECTED;
  } else if (hasExpired(n, now[0])) {
    cause[0] = RemovalCause.EXPIRED;
  } else {
    // 未失效時在這里返回,不會觸發 load 函數
    return n;
  }
 
  // ...
}

所以,如果 load 結果不是 null,那么只第一個線程花了 100ms,后續線程會盡快返回,最終時長應該只比 100ms 多一點。但如果 load 結果返回 null(緩存穿透),相當于沒有查到數據,于是后續線程還會再次執行 load,最終時間就是 1s 左右。

到此,關于“如何正確使用Caffeine Cache”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

宣武区| 万全县| 阿勒泰市| 全州县| 绥江县| 墨玉县| 榆社县| 酉阳| 山阳县| 岗巴县| 新蔡县| 东台市| 二连浩特市| 扎鲁特旗| 合水县| 伊春市| 鸡泽县| 婺源县| 大邑县| 广西| 海盐县| 资中县| 清水县| 台东县| 绥中县| 丰城市| 增城市| 孝昌县| 察雅县| 盐山县| 武山县| 道真| 兴义市| 江油市| 山东省| 综艺| 敦煌市| 虞城县| 勃利县| 皋兰县| 庆元县|