<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Guava Cache 使用小結

          共 10428字,需瀏覽 21分鐘

           ·

          2022-02-26 02:41

          閑聊

          話說原創(chuàng)文章已經斷更 2 個月了,倒也不是因為忙,主要還是懶。但是也感覺可以拿出來跟大家分享的技術點越來越少了,一方面主要是最近在從事一些“內部項目”的研發(fā),縱使我很想分享,也沒法搬到公眾號 & 博客上來;一方面是一些我并不是很擅長的技術點,在我還是新手時,我敢于去寫,而有了一定工作年限之后,反而有些包袱了,我的讀者會不會介意呢?思來想去,我回憶起了寫作的初心,不就是為了記錄自己的學習過程嗎?于是乎,我還是按照我之前的文風記錄下了此文,以避免成為一名斷更的博主。

          以下是正文。

          前言

          “緩存”一直是我們程序員聊的最多的那一類技術點,諸如 Redis、Encache、Guava Cache,你至少會聽說過一個。需要承認的是,無論是面試八股文的風氣,還是實際使用的頻繁度,Redis 分布式緩存的確是當下最為流行的緩存技術,但同時,從我個人的項目經驗來看,本地緩存也是非常常用的一個技術點。

          分析 Redis 緩存的文章很多,例如 Redis 雪崩、Redis 過期機制等等,諸如此類的公眾號標題不鮮出現(xiàn)在我朋友圈的 timeline 中,但是分析本地緩存的文章在我的映像中很少。

          在最近的項目中,有一位新人同事使用了 Guava Cache 來對一個 RPC 接口的響應進行緩存,我在 review 其代碼時恰好發(fā)現(xiàn)了一個不太合理的寫法,遂有此文。

          本文將會介紹 Guava Cache 的一些常用操作:基礎 API 使用,過期策略,刷新策略。并且按照我的寫作習慣,會附帶上實際開發(fā)中的一些總結。需要事先說明的是,我沒有閱讀過 Guava Cache 的源碼,對其的介紹僅僅是一些使用經驗或者最佳實踐,不會有過多深入的解析。

          先簡單介紹一下 Guava Cache,它是 Google 封裝的基礎工具包 guava 中的一個內存緩存模塊,主要提供了以下能力:

          • 封裝了緩存與數(shù)據源交互的流程,使得開發(fā)更關注于業(yè)務操作
          • 提供線程安全的存取操作(可以類比 ConcurrentHashMap)
          • 提供常用的緩存過期策略,緩存刷新策略
          • 提供緩存命中率的監(jiān)控

          基礎使用

          使用一個示例介紹 Guava Cache 的基礎使用方法 -- 緩存大小寫轉換的返回值。

          private?String?fetchValueFromServer(String?key)?{
          ????return?key.toUpperCase();
          }

          @Test
          public?void?whenCacheMiss_thenFetchValueFromServer()?throws?ExecutionException?{
          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().build(new?CacheLoader()?{
          ????????????@Override
          ????????????public?String?load(String?key)?{
          ????????????????return?fetchValueFromServer(key);
          ????????????}
          ????????});

          ????assertEquals(0,?cache.size());
          ????assertEquals("HELLO",?cache.getUnchecked("hello"));
          ????assertEquals("HELLO",?cache.get("hello"));
          ????assertEquals(1,?cache.size());
          }

          使用 Guava Cache 的好處已經躍然于紙上了,它解耦了緩存存取與業(yè)務操作。CacheLoader?的?load?方法可以理解為從數(shù)據源加載原始數(shù)據的入口,當調用 LoadingCache 的?getUnchecked?或者?get方法時,Guava Cache 行為如下:

          • 緩存未命中時,同步調用 load 接口,加載進緩存,返回緩存值
          • 緩存命中,直接返回緩存值
          • 多線程緩存未命中時,A 線程 load 時,會阻塞 B 線程的請求,直到緩存加載完畢

          注意到,Guava 提供了兩個?getUnchecked?或者?get?加載方法,沒有太大的區(qū)別,無論使用哪一個,都需要注意,數(shù)據源無論是 RPC 接口的返回值還是數(shù)據庫,都要考慮訪問超時或者失敗的情況,做好異常處理。

          預加載緩存

          預加載緩存的常見使用場景:

          • 老生常談的秒殺場景,事先緩存預熱,將熱點商品加入緩存;
          • 系統(tǒng)重啟過后,事先加載好緩存,避免真實請求擊穿緩存

          Guava Cache 提供了?put?和?putAll?方法

          @Test
          public?void?whenPreloadCache_thenPut()?{
          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().build(new?CacheLoader()?{
          ????????????@Override
          ????????????public?String?load(String?key)?{
          ????????????????return?fetchValueFromServer(key);
          ????????????}
          ????????});

          ????String?key?=?"kirito";
          ????cache.put(key,fetchValueFromServer(key));

          ????assertEquals(1,?cache.size());
          }

          操作和 HashMap 一模一樣。

          這里有一個誤區(qū),而那位新人同事恰好踩到了,也是我寫這篇文章的初衷,請務必僅在預加載緩存這個場景使用 put,其他任何場景都應該使用 load 去觸發(fā)加載緩存。看下面這個反面示例

          //?注意這是一個反面示例
          @Test
          public?void?wrong_usage_whenCacheMiss_thenPut()?throws?ExecutionException?{
          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().build(new?CacheLoader()?{
          ????????????@Override
          ????????????public?String?load(String?key)?{
          ????????????????return?"";
          ????????????}
          ????????});

          ????String?key?=?"kirito";
          ????String?cacheValue?=?cache.get(key);
          ????if?("".equals(cacheValue))?{
          ????????cacheValue?=?fetchValueFromServer(key);
          ????????cache.put(key,?cacheValue);
          ????}
          ????cache.put(key,?cacheValue);

          ????assertEquals(1,?cache.size());
          }

          這樣的寫法,在 load 方法中設置了一個空值,后續(xù)通過手動 put + get 的方式使用緩存,這種習慣更像是在操作一個 HashMap,但并不推薦在 Cache 中使用。在前面介紹過 get 配合 load 是由 Guava Cache 去保障了線程安全,保障多個線程訪問緩存時,第一個請求加載緩存的同時,阻塞后續(xù)請求,這樣的 HashMap 用法既不優(yōu)雅,在極端情況下還會引發(fā)緩存擊穿、線程安全等問題。

          請務必僅僅將 put 方法用作預加載緩存場景。

          緩存過期

          前面的介紹使用起來依舊沒有脫離 ConcurrentHashMap 的范疇,Cache 與其的第一個區(qū)別在“緩存過期”這個場景可以被體現(xiàn)出來。本節(jié)介紹 Guava 一些常見的緩存過期行為及策略。

          緩存固定數(shù)量的值

          @Test
          public?void?whenReachMaxSize_thenEviction()?throws?ExecutionException?{
          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().maximumSize(3).build(new?CacheLoader()?{
          ????????????@Override
          ????????????public?String?load(String?key)?{
          ????????????????return?fetchValueFromServer(key);
          ????????????}
          ????????});

          ????cache.get("one");
          ????cache.get("two");
          ????cache.get("three");
          ????cache.get("four");
          ????assertEquals(3,?cache.size());
          ????assertNull(cache.getIfPresent("one"));
          ????assertEquals("FOUR",?cache.getIfPresent("four"));
          }

          使用?ConcurrentHashMap?做緩存的一個最大的問題,便是我們沒有簡易有效的手段阻止其無限增長,而 Guava Cache 可以通過初始化 LoadingCache 的過程,配置?maximumSize?,以確保緩存內容不導致你的系統(tǒng)出現(xiàn) OOM。

          值得注意的是,我這里的測試用例使用的是除了?get?、getUnchecked?外的第三種獲取緩存的方式,如字面意思描述的那樣,getIfPresent?在緩存不存在時,并不會觸發(fā)?load?方法加載數(shù)據源。

          LRU 過期策略

          依舊沿用上述的示例,我們在設置容量為 3 時,僅獲悉 LoadingCache 可以存儲 3 個值,卻并未得知第 4 個值存入后,哪一個舊值需要淘汰,為新值騰出空位。實際上,Guava Cache 默認采取了 LRU 緩存淘汰策略。Least Recently Used 即最近最少使用,這個算法你可能沒有實現(xiàn)過,但一定會聽說過,在 Guava Cache 中 Used 的語義代表任意一次訪問,例如 put、get。繼續(xù)看下面的示例。

          @Test
          public?void?whenReachMaxSize_thenEviction()?throws?ExecutionException?{
          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().maximumSize(3).build(new?CacheLoader()?{
          ????????????@Override
          ????????????public?String?load(String?key)?{
          ????????????????return?fetchValueFromServer(key);
          ????????????}
          ????????});

          ????cache.get("one");
          ????cache.get("two");
          ????cache.get("three");
          ????//?access?one
          ????cache.get("one");
          ????cache.get("four");
          ????assertEquals(3,?cache.size());
          ????assertNull(cache.getIfPresent("two"));
          ????assertEquals("ONE",?cache.getIfPresent("one"));
          }

          注意此示例與上一節(jié)示例的區(qū)別:第四次 get 訪問 one 后,two 變成了最久未被使用的值,當?shù)谒膫€值 four 存入后,淘汰的對象變成了 two,而不再是 one 了。

          緩存固定時間

          為緩存設置過期時間,也是區(qū)分 HashMap 和 Cache 的一個重要特性。Guava Cache 提供了expireAfterAccess、?expireAfterWrite?的方案,為 LoadingCache 中的緩存值設置過期時間。

          @Test
          public?void?whenEntryIdle_thenEviction()
          ????throws?InterruptedException,?ExecutionException?
          {

          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().expireAfterAccess(1,?TimeUnit.SECONDS).build(new?CacheLoader()?{
          ????????????@Override
          ????????????public?String?load(String?key)?{
          ????????????????return?fetchValueFromServer(key);
          ????????????}
          ????????});

          ????cache.get("kirito");
          ????assertEquals(1,?cache.size());

          ????cache.get("kirito");
          ????Thread.sleep(2000);

          ????assertNull(cache.getIfPresent("kirito"));
          }

          緩存失效

          @Test
          public?void?whenInvalidate_thenGetNull()?throws?ExecutionException?{
          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder()
          ????????????.build(new?CacheLoader()?{
          ????????????????@Override
          ????????????????public?String?load(String?key)?{
          ????????????????????return?fetchValueFromServer(key);
          ????????????????}
          ????????????});

          ????String?name?=?cache.get("kirito");
          ????assertEquals("KIRITO",?name);

          ????cache.invalidate("kirito");
          ????assertNull(cache.getIfPresent("kirito"));
          }

          使用?void invalidate(Object key)?移除單個緩存,使用?void invalidateAll()?移除所有緩存。

          緩存刷新

          緩存刷新的常用于使用數(shù)據源的新值覆蓋緩存舊值,Guava Cache 提供了兩類刷新機制:手動刷新和定時刷新。

          手動刷新

          cache.refresh("kirito");

          refresh 方法將會觸發(fā) load 邏輯,嘗試從數(shù)據源加載緩存。

          需要注意點的是,refresh 方法并不會阻塞 get 方法,所以在 refresh 期間,舊的緩存值依舊會被訪問到,直到 load 完畢,看下面的示例。

          @Test
          public?void?whenCacheRefresh_thenLoad()
          ????throws?InterruptedException,?ExecutionException?
          {

          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().expireAfterWrite(1,?TimeUnit.SECONDS).build(new?CacheLoader()?{
          ????????????@Override
          ????????????public?String?load(String?key)?throws?InterruptedException?{
          ????????????????Thread.sleep(2000);
          ????????????????return?key?+?ThreadLocalRandom.current().nextInt(100);
          ????????????}
          ????????});

          ????String?oldValue?=?cache.get("kirito");

          ????new?Thread(()?->?{
          ????????cache.refresh("kirito");
          ????}).start();

          ????//?make?sure?another?refresh?thread?is?scheduling
          ????Thread.sleep(500);

          ????String?val1?=?cache.get("kirito");

          ????assertEquals(oldValue,?val1);

          ????//?make?sure?refresh?cache?
          ????Thread.sleep(2000);

          ????String?val2?=?cache.get("kirito");
          ????assertNotEquals(oldValue,?val2);

          }

          其實任何情況下,緩存值都有可能和數(shù)據源出現(xiàn)不一致,業(yè)務層面需要做好訪問到舊值的容錯邏輯。

          自動刷新

          @Test
          public?void?whenTTL_thenRefresh()?throws?ExecutionException,?InterruptedException?{
          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().refreshAfterWrite(1,?TimeUnit.SECONDS).build(new?CacheLoader()?{
          ????????????@Override
          ????????????public?String?load(String?key)?{
          ????????????????return?key?+?ThreadLocalRandom.current().nextInt(100);
          ????????????}
          ????????});

          ????String?first?=?cache.get("kirito");
          ????Thread.sleep(1000);
          ????String?second?=?cache.get("kirito");

          ????assertNotEquals(first,?second);
          }

          和上節(jié)的 refresh 機制一樣,refreshAfterWrite?同樣不會阻塞 get 線程,依舊有訪問舊值的可能性。

          緩存命中統(tǒng)計

          Guava Cache 默認情況不會對命中情況進行統(tǒng)計,需要在構建 CacheBuilder 時顯式配置?recordStats

          @Test
          public?void?whenRecordStats_thenPrint()?throws?ExecutionException?{
          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().maximumSize(100).recordStats().build(new?CacheLoader()?{
          ????????????@Override
          ????????????public?String?load(String?key)?{
          ????????????????return?fetchValueFromServer(key);
          ????????????}
          ????????});

          ????cache.get("one");
          ????cache.get("two");
          ????cache.get("three");
          ????cache.get("four");

          ????cache.get("one");
          ????cache.get("four");

          ????CacheStats?stats?=?cache.stats();
          ????System.out.println(stats);
          }
          ---
          CacheStats{hitCount=2,?missCount=4,?loadSuccessCount=4,?loadExceptionCount=0,?totalLoadTime=1184001,?evictionCount=0}

          緩存移除的通知機制

          在一些業(yè)務場景中,我們希望對緩存失效進行一些監(jiān)測,或者是針對失效的緩存做一些回調處理,就可以使用?RemovalNotification?機制。

          @Test
          public?void?whenRemoval_thenNotify()?throws?ExecutionException?{
          ????LoadingCache?cache?=
          ????????CacheBuilder.newBuilder().maximumSize(3)
          ????????????.removalListener(
          ????????????????cacheItem?->?System.out.println(cacheItem?+?"?is?removed,?cause?by?"?+?cacheItem.getCause()))
          ????????????.build(new?CacheLoader()?{
          ????????????????@Override
          ????????????????public?String?load(String?key)?{
          ????????????????????return?fetchValueFromServer(key);
          ????????????????}
          ????????????});

          ????cache.get("one");
          ????cache.get("two");
          ????cache.get("three");
          ????cache.get("four");
          }
          ---
          one=ONE?is?removed,?cause?by?SIZE

          removalListener?可以給 LoadingCache 增加一個回調處理器,RemovalNotification?實例包含了緩存的鍵值對以及移除原因。

          Weak Keys & Soft Values

          Java 基礎中的弱引用和軟引用的概念相信大家都學習過,這里先給大家復習一下

          • 軟引用:如果一個對象只具有軟引用,則內存空間充足時,垃圾回收器不會回收它;如果內存空間不足,就會回收這些對象。只要垃圾回收器沒有回收它,該對象就可以被程序使用
          • 弱引用:只具有弱引用的對象擁有更短暫生命周期。在垃圾回收器線程掃描它所管轄的內存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。

          在 Guava Cache 中,CacheBuilder 提供了 weakKeys、weakValues、softValues 三種方法,將緩存的鍵值對與 JVM 垃圾回收機制產生關聯(lián)。

          該操作可能有它適用的場景,例如最大限度的使用 JVM 內存做緩存,但依賴 GC 清理,性能可想而知會比較低。總之我是不會依賴 JVM 的機制來清理緩存的,所以這個特性我不敢使用,線上還是穩(wěn)定性第一。

          如果需要設置清理策略,可以參考緩存過期小結中的介紹固定數(shù)量和固定時間兩個方案,結合使用確保使用緩存獲得高性能的同時,不把內存打掛。

          總結

          本文介紹了 Guava Cache 一些常用的 API 、用法示例,以及需要警惕的一些使用誤區(qū)。

          在選擇使用 Guava 時,我一般會結合實際使用場景,做出以下的考慮:

          1. 為什么不用 Redis?

            如果本地緩存能夠解決,我不希望額外引入一個中間件。

          2. 如果保證緩存和數(shù)據源數(shù)據的一致性?

            一種情況,我會在數(shù)據要求敏感度不高的場景使用緩存,所以短暫的不一致可以忍受;另外一些情況,我會在設置定期刷新緩存以及手動刷新緩存的機制。舉個例子,頁面上有一個顯示應用 developer 列表的功能,而本地僅存儲了應用名,developer 列表是通過一個 RPC 接口查詢獲取的,而由于對方的限制,該接口 qps 承受能力非常低,便可以考慮緩存 developer 列表,并配置 maximumSize 以及 expireAfterAccess。如果有用戶在 developer 數(shù)據源中新增了數(shù)據,導致了數(shù)據不一致,頁面也可以設置一個同步按鈕,讓用戶去主動 refresh;或者,如果判斷當前用戶不在 developer 列表,也可以程序 refresh 一次。總之非常靈活,使用 Guava Cache 的 API 可以滿足大多數(shù)業(yè)務場景的緩存需求。

          3. 為什么是 Guava Cache,它的性能怎么樣?

            我現(xiàn)在主要是出于穩(wěn)定性考慮,項目一直在使用 Guava Cache。據說有比 Guava Cache 快的本地緩存,但那點性能我的系統(tǒng)不是特別關心。

          - END -

          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  A V视频在线观看 | 中文字幕无码人妻在线二区 | 欧美性爱午夜视频 | 精品国产乱码久久久 | 色撸撸在线视频 |