天天看點

Caffeine 緩存

簡介

在本文中,我們來看看 ​​Caffeine​​ — 一個高性能的 Java 緩存庫。

緩存和 Map 之間的一個根本差別在于緩存可以回收存儲的 item。

回收政策為在指定時間删除哪些對象。此政策直接影響緩存的命中率 — 緩存庫的一個重要特征。

Caffeine 因使用 Window TinyLfu 回收政策,提供了一個近乎最佳的命中率。

填充政策(Population)

Caffeine 為我們提供了三種填充政策:手動、同步和異步

手動加載(Manual)

Cache<String, Object> manualCache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(10_000)
        .build();

String key = "name1";
// 根據key查詢一個緩存,如果沒有傳回NULL
graph = manualCache.getIfPresent(key);
// 根據Key查詢一個緩存,如果沒有調用createExpensiveGraph方法,并将傳回值儲存到緩存。
// 如果該方法傳回Null則manualCache.get傳回null,如果該方法抛出異常則manualCache.get抛出異常
graph = manualCache.get(key, k -> createExpensiveGraph(k));
// 将一個值放入緩存,如果以前有值就覆寫以前的值
manualCache.put(key, graph);
// 删除一個緩存
manualCache.invalidate(key);

ConcurrentMap<String, Object> map = manualCache.asMap();
cache.invalidate(key);

Cache<String, Object> manualCache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(10_000)
        .build();

String key = "name1";
// 根據key查詢一個緩存,如果沒有傳回NULL
graph = manualCache.getIfPresent(key);
// 根據Key查詢一個緩存,如果沒有調用createExpensiveGraph方法,并将傳回值儲存到緩存。
// 如果該方法傳回Null則manualCache.get傳回null,如果該方法抛出異常則manualCache.get抛出異常
graph = manualCache.get(key, k -> createExpensiveGraph(k));
// 将一個值放入緩存,如果以前有值就覆寫以前的值
manualCache.put(key, graph);
// 删除一個緩存
manualCache.invalidate(key);

ConcurrentMap<String, Object> map = manualCache.asMap();
cache.invalidate(key);      

Cache接口允許顯式的去控制緩存的檢索,更新和删除。

我們可以通過cache.getIfPresent(key) 方法來擷取一個key的值,通過cache.put(key, value)方法顯示的将數控放入緩存,但是這樣子會覆寫緩原來key的資料。更加建議使用cache.get(key,k - > value) 的方式,get 方法将一個參數為 key 的 Function (createExpensiveGraph) 作為參數傳入。如果緩存中不存在該鍵,則調用這個 Function 函數,并将傳回值作為該緩存的值插入緩存中。get 方法是以阻塞方式執行調用,即使多個線程同時請求該值也隻會調用一次Function方法。這樣可以避免與其他線程的寫入競争,這也是為什麼使用 get 優于 getIfPresent 的原因。

注意:如果調用該方法傳回NULL(如上面的 createExpensiveGraph 方法),則cache.get傳回null,如果調用該方法抛出異常,則get方法也會抛出異常。

可以使用Cache.asMap() 方法擷取ConcurrentMap進而對緩存進行一些更改。

同步加載(Loading)

LoadingCache<String, Object> loadingCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    
String key = "name1";
// 采用同步方式去擷取一個緩存和上面的手動方式是一個原理。在build Cache的時候會提供一個createExpensiveGraph函數。
// 查詢并在缺失的情況下使用同步的方式來建構一個緩存
Object graph = loadingCache.get(key);

// 擷取組key的值傳回一個Map
List<String> keys = new ArrayList<>();
keys.add(key);
Map<String, Object> graphs = loadingCache.getAll(keys);

LoadingCache<String, Object> loadingCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    
String key = "name1";
// 采用同步方式去擷取一個緩存和上面的手動方式是一個原理。在build Cache的時候會提供一個createExpensiveGraph函數。
// 查詢并在缺失的情況下使用同步的方式來建構一個緩存
Object graph = loadingCache.get(key);

// 擷取組key的值傳回一個Map
List<String> keys = new ArrayList<>();
keys.add(key);
Map<String, Object> graphs = loadingCache.getAll(keys);      

LoadingCache是使用CacheLoader來建構的緩存的值。

批量查找可以使用getAll方法。預設情況下,getAll将會對緩存中沒有值的key分别調用CacheLoader.load方法來建構緩存的值。我們可以重寫CacheLoader.loadAll方法來提高getAll的效率。

注意:您可以編寫一個CacheLoader.loadAll來實作為特别請求的key加載值。例如,如果計算某個組中的任何鍵的值将為該組中的所有鍵提供值,則loadAll可能會同時加載該組的其餘部分。

異步加載(Asynchronously Loading)

AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            // Either: Build with a synchronous computation that is wrapped as asynchronous
            .buildAsync(key -> createExpensiveGraph(key));
            // Or: Build with a asynchronous computation that returns a future
            // .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

 String key = "name1";

// 查詢并在缺失的情況下使用異步的方式來建構緩存
CompletableFuture<Object> graph = asyncLoadingCache.get(key);
// 查詢一組緩存并在缺失的情況下使用異步的方式來建構緩存
List<String> keys = new ArrayList<>();
keys.add(key);
CompletableFuture<Map<String, Object>> graphs = asyncLoadingCache.getAll(keys);
// 異步轉同步
loadingCache = asyncLoadingCache.synchronous();

AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            // Either: Build with a synchronous computation that is wrapped as asynchronous
            .buildAsync(key -> createExpensiveGraph(key));
            // Or: Build with a asynchronous computation that returns a future
            // .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

 String key = "name1";

// 查詢并在缺失的情況下使用異步的方式來建構緩存
CompletableFuture<Object> graph = asyncLoadingCache.get(key);
// 查詢一組緩存并在缺失的情況下使用異步的方式來建構緩存
List<String> keys = new ArrayList<>();
keys.add(key);
CompletableFuture<Map<String, Object>> graphs = asyncLoadingCache.getAll(keys);
// 異步轉同步
loadingCache = asyncLoadingCache.synchronous();      

AsyncLoadingCache是繼承自LoadingCache類的,異步加載使用Executor去調用方法并傳回一個CompletableFuture。異步加載緩存使用了響應式程式設計模型。

如果要以同步方式調用時,應提供CacheLoader。要以異步表示時,應該提供一個AsyncCacheLoader,并傳回一個CompletableFuture。

synchronous()這個方法傳回了一個LoadingCacheView視圖,LoadingCacheView也繼承自LoadingCache。調用該方法後就相當于你将一個異步加載的緩存AsyncLoadingCache轉換成了一個同步加載的緩存LoadingCache。

預設使用ForkJoinPool.commonPool()來執行異步線程,但是我們可以通過Caffeine.executor(Executor) 方法來替換線程池。

驅逐政策(eviction)

Caffeine提供三類驅逐政策:基于大小(size-based),基于時間(time-based)和基于引用(reference-based)。

基于大小(size-based)

基于大小驅逐,有兩種方式:一種是基于緩存大小,一種是基于權重。

// Evict based on the number of entries in the cache
// 根據緩存的計數進行驅逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .build(key -> createExpensiveGraph(key));

// Evict based on the number of vertices in the cache
// 根據緩存的權重來進行驅逐(權重隻是用于确定緩存大小,不會用于決定該緩存是否被驅逐)
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumWeight(10_000)
    .weigher((Key key, Graph graph) -> graph.vertices().size())
    .build(key -> createExpensiveGraph(key));      

我們可以使用Caffeine.maximumSize(long)方法來指定緩存的最大容量。當緩存超出這個容量的時候,會使用​​Window TinyLfu政策​​來删除緩存。

我們也可以使用權重的政策來進行驅逐,可以使用Caffeine.weigher(Weigher) 函數來指定權重,使用Caffeine.maximumWeight(long) 函數來指定緩存最大權重值。

maximumWeight與maximumSize不可以同時使用。

基于時間(Time-based)

// Evict based on a fixed expiration policy
// 基于固定的到期政策進行退出
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfterAccess(5, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

// Evict based on a varying expiration policy
// 基于不同的到期政策進行退出
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfter(new Expiry<Key, Graph>() {
      @Override
      public long expireAfterCreate(Key key, Graph graph, long currentTime) {
        // Use wall clock time, rather than nanotime, if from an external resource
        long seconds = graph.creationDate().plusHours(5)
            .minus(System.currentTimeMillis(), MILLIS)
            .toEpochSecond();
        return TimeUnit.SECONDS.toNanos(seconds);
      }
      
      @Override
      public long expireAfterUpdate(Key key, Graph graph, 
          long currentTime, long currentDuration) {
        return currentDuration;
      }
      
      @Override
      public long expireAfterRead(Key key, Graph graph,
          long currentTime, long currentDuration) {
        return currentDuration;
      }
    })
    .build(key -> createExpensiveGraph(key));

// Evict based on a fixed expiration policy
// 基于固定的到期政策進行退出
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfterAccess(5, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

// Evict based on a varying expiration policy
// 基于不同的到期政策進行退出
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfter(new Expiry<Key, Graph>() {
      @Override
      public long expireAfterCreate(Key key, Graph graph, long currentTime) {
        // Use wall clock time, rather than nanotime, if from an external resource
        long seconds = graph.creationDate().plusHours(5)
            .minus(System.currentTimeMillis(), MILLIS)
            .toEpochSecond();
        return TimeUnit.SECONDS.toNanos(seconds);
      }
      
      @Override
      public long expireAfterUpdate(Key key, Graph graph, 
          long currentTime, long currentDuration) {
        return currentDuration;
      }
      
      @Override
      public long expireAfterRead(Key key, Graph graph,
          long currentTime, long currentDuration) {
        return currentDuration;
      }
    })
    .build(key -> createExpensiveGraph(key));      

Caffeine提供了三種定時驅逐政策:

  • expireAfterAccess(long, TimeUnit):在最後一次通路或者寫入後開始計時,在指定的時間後過期。假如一直有請求通路該key,那麼這個緩存将一直不會過期。
  • expireAfterWrite(long, TimeUnit): 在最後一次寫入緩存後開始計時,在指定的時間後過期。
  • expireAfter(Expiry): 自定義政策,過期時間由Expiry實作獨自計算。

緩存的删除政策使用的是惰性删除和定時删除。這兩個删除政策的時間複雜度都是O(1)。

測試定時驅逐不需要等到時間結束。我們可以使用Ticker接口和Caffeine.ticker(Ticker)方法在緩存生成器中指定時間源,而不必等待系統時鐘。如:

FakeTicker ticker = new FakeTicker(); // Guava's testlib
Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .executor(Runnable::run)
    .ticker(ticker::read)
    .maximumSize(10)
    .build();

cache.put(key, graph);
ticker.advance(30, TimeUnit.MINUTES)
assertThat(cache.getIfPresent(key), is(nullValue());

FakeTicker ticker = new FakeTicker(); // Guava's testlib
Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .executor(Runnable::run)
    .ticker(ticker::read)
    .maximumSize(10)
    .build();

cache.put(key, graph);
ticker.advance(30, TimeUnit.MINUTES)
assertThat(cache.getIfPresent(key), is(nullValue());      

基于引用(reference-based)

​​強引用,軟引用,弱引用概念說明​​請點選連接配接,這裡說一下各各引用的差別:

Java4種引用的級别由高到低依次為:強引用 > 軟引用 > 弱引用 > 虛引用

引用類型 被垃圾回收時間 用途 生存時間
強引用 從來不會 對象的一般狀态 JVM停止運作時終止
軟引用 在記憶體不足時 對象緩存 記憶體不足時終止
弱引用 在垃圾回收時 對象緩存 gc運作後終止
虛引用 Unknown Unknown Unknown
// Evict when neither the key nor value are strongly reachable
// 當key和value都沒有引用時驅逐緩存
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .weakKeys()
    .weakValues()
    .build(key -> createExpensiveGraph(key));

// Evict when the garbage collector needs to free memory
// 當垃圾收集器需要釋放記憶體時驅逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .softValues()
    .build(key -> createExpensiveGraph(key));

// Evict when neither the key nor value are strongly reachable
// 當key和value都沒有引用時驅逐緩存
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .weakKeys()
    .weakValues()
    .build(key -> createExpensiveGraph(key));

// Evict when the garbage collector needs to free memory
// 當垃圾收集器需要釋放記憶體時驅逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .softValues()
    .build(key -> createExpensiveGraph(key));      

我們可以将緩存的驅逐配置成基于垃圾回收器。為此,我們可以将key 和 value 配置為弱引用或隻将值配置成軟引用。

注意:AsyncLoadingCache不支援弱引用和軟引用。

Caffeine.weakKeys() 使用弱引用存儲key。如果沒有其他地方對該key有強引用,那麼該緩存就會被垃圾回收器回收。由于垃圾回收器隻依賴于身份(identity)相等,是以這會導緻整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。

Caffeine.weakValues() 使用弱引用存儲value。如果沒有其他地方對該value有強引用,那麼該緩存就會被垃圾回收器回收。由于垃圾回收器隻依賴于身份(identity)相等,是以這會導緻整個緩存使用身份 (==) 相等來比較 key,而不是使用 equals()。

Caffeine.softValues() 使用軟引用存儲value。當記憶體滿了過後,軟引用的對象以将使用最近最少使用(least-recently-used ) 的方式進行垃圾回收。由于使用軟引用是需要等到記憶體滿了才進行回收,是以我們通常建議給緩存配置一個使用記憶體的最大值。 softValues() 将使用身份相等(identity) (==) 而不是equals() 來比較值。

注意:Caffeine.weakValues()和Caffeine.softValues()不可以一起使用。

移除監聽器(Removal)

概念:

  • 驅逐(eviction):由于滿足了某種驅逐政策,背景自動進行的删除操作
  • 無效(invalidation):表示由調用方手動删除緩存
  • 移除(removal):監聽驅逐或無效操作的監聽器

手動删除緩存:

在任何時候,您都可能明确地使緩存無效,而不用等待緩存被驅逐。

// individual key
cache.invalidate(key)
// bulk keys
cache.invalidateAll(keys)
// all keys
cache.invalidateAll()

// individual key
cache.invalidate(key)
// bulk keys
cache.invalidateAll(keys)
// all keys
cache.invalidateAll()      

Removal 監聽器:

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .removalListener((Key key, Graph graph, RemovalCause cause) ->
        System.out.printf("Key %s was removed (%s)%n", key, cause))
    .build();

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .removalListener((Key key, Graph graph, RemovalCause cause) ->
        System.out.printf("Key %s was removed (%s)%n", key, cause))
    .build();      

您可以通過Caffeine.removalListener(RemovalListener) 為緩存指定一個删除偵聽器,以便在删除資料時執行某些操作。 RemovalListener可以擷取到key、value和RemovalCause(删除的原因)。

删除偵聽器的裡面的操作是使用Executor來異步執行的。預設執行程式是ForkJoinPool.commonPool(),可以通過Caffeine.executor(Executor)覆寫。當操作必須與删除同步執行時,請改為使用CacheWrite,CacheWrite将在下面說明。

注意:由RemovalListener抛出的任何異常都會被記錄(使用Logger)并不會抛出。

重新整理(Refresh)

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    // 指定在建立緩存或者最近一次更新緩存後經過固定的時間間隔,重新整理緩存
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    // 指定在建立緩存或者最近一次更新緩存後經過固定的時間間隔,重新整理緩存
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));      

重新整理和驅逐是不一樣的。重新整理的是通過LoadingCache.refresh(key)方法來指定,并通過調用CacheLoader.reload方法來執行,重新整理key會異步地為這個key加載新的value,并傳回舊的值(如果有的話)。驅逐會阻塞查詢操作直到驅逐作完成才會進行其他操作。

與expireAfterWrite不同的是,refreshAfterWrite将在查詢資料的時候判斷該資料是不是符合查詢條件,如果符合條件該緩存就會去執行重新整理操作。例如,您可以在同一個緩存中同時指定refreshAfterWrite和expireAfterWrite,隻有當資料具備重新整理條件的時候才會去重新整理資料,不會盲目去執行重新整理操作。如果資料在重新整理後就一直沒有被再次查詢,那麼該資料也會過期。

重新整理操作是使用Executor異步執行的。預設執行程式是ForkJoinPool.commonPool(),可以通過Caffeine.executor(Executor)覆寫。

如果重新整理時引發異常,則使用log記錄日志,并不會抛出。

Writer

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
  .writer(new CacheWriter<Key, Graph>() {
    @Override public void write(Key key, Graph graph) {
      // write to storage or secondary cache
    }
    @Override public void delete(Key key, Graph graph, RemovalCause cause) {
      // delete from storage or secondary cache
    }
  })
  .build(key -> createExpensiveGraph(key));

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
  .writer(new CacheWriter<Key, Graph>() {
    @Override public void write(Key key, Graph graph) {
      // write to storage or secondary cache
    }
    @Override public void delete(Key key, Graph graph, RemovalCause cause) {
      // delete from storage or secondary cache
    }
  })
  .build(key -> createExpensiveGraph(key));      

CacheWriter允許緩存充當一個底層資源的代理,當與CacheLoader結合使用時,所有對緩存的讀寫操作都可以通過Writer進行傳遞。Writer可以把操作緩存和操作外部資源擴充成一個同步的原子性操作。并且在緩存寫入完成之前,它将會阻塞後續的更新緩存操作,但是讀取(get)将直接傳回原有的值。如果寫入程式失敗,那麼原有的key和value的映射将保持不變,如果出現異常将直接抛給調用者。

CacheWriter可以同步的監聽到緩存的建立、變更和删除操作。加載(例如,LoadingCache.get)、重新加載(例如,LoadingCache.refresh)和計算(例如Map.computeIfPresent)的操作不被CacheWriter監聽到。

注意:CacheWriter不能與weakKeys或AsyncLoadingCache結合使用。

可能的用例(Possible Use-Cases)

CacheWriter是複雜工作流的擴充點,需要外部資源來觀察給定Key的變化順序。這些用法Caffeine是支援的,但不是本地内置。

寫模式(Write Modes)

CacheWriter可以用來實作一個直接寫(write-through )或回寫(write-back )緩存的操作。

write-through式緩存中,寫操作是一個同步的過程,隻有寫成功了才會去更新緩存。這避免了同時去更新資源和緩存的條件競争。

write-back式緩存中,對外部資源的操作是在緩存更新後異步執行的。這樣可以提高寫入的吞吐量,避免資料不一緻的風險,比如如果寫入失敗,則在緩存中保留無效的狀态。這種方法可能有助于延遲寫操作,直到指定的時間,限制寫速率或批寫操作。

通過對write-back進行擴充,我們可以實作以下特性:

  • 批處理和合并操作
  • 延遲操作并到一個特定的時間執行
  • 如果超過門檻值大小,則在定期重新整理之前執行批處理
  • 如果操作尚未重新整理,則從寫入後緩沖器(write-behind)加載
  • 根據外部資源的特點,處理重審,速率限制和并發

可以參考一個簡單的​​例子​​,使用RxJava實作。

分層(Layering)

CacheWriter可能用來內建多個緩存進而實作多級緩存。

多級緩存的加載和寫入可以使用系統外部高速緩存。這允許緩存使用一個小并且快速的緩存去調用一個大的并且速度相對慢一點的緩存。典型的off-heap、file-based和remote 緩存。

受害者緩存(Victim Cache)是一個多級緩存的變體,其中被删除的資料被寫入二級緩存。這個delete(K, V, RemovalCause) 方法允許檢查為什麼該資料被删除,并作出相應的操作。

同步監聽器(Synchronous Listeners)

同步監聽器會接收一個key在緩存中的進行了那些操作的通知。監聽器可以阻止緩存操作,也可以将事件排隊以異步的方式執行。這種類型的監聽器最常用于複制或建構分布式緩存。

統計(Statistics)

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats()
    .build();

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats()
    .build();      

使用Caffeine.recordStats(),您可以打開統計資訊收集。Cache.stats() 方法傳回提供統計資訊的CacheStats,如:

  • hitRate():傳回命中與請求的比率
  • hitCount(): 傳回命中緩存的總數
  • evictionCount():緩存逐出的數量
  • averageLoadPenalty():加載新值所花費的平均時間

Cleanup

緩存的删除政策使用的是惰性删除和定時删除,但是我也可以自己調用cache.cleanUp()方法手動觸發一次回收操作。cache.cleanUp()是一個同步方法。

政策(Policy)

在建立緩存的時候,緩存的政策就指定好了。但是我們可以在運作時可以獲得和修改該政策。這些政策可以通過一些選項來獲得,以此來确定緩存是否支援該功能。

Size-based

cache.policy().eviction().ifPresent(eviction -> {
  eviction.setMaximum(2 * eviction.getMaximum());
});

cache.policy().eviction().ifPresent(eviction -> {
  eviction.setMaximum(2 * eviction.getMaximum());
});      

如果緩存配置的時基于權重來驅逐,那麼我們可以使用weightedSize() 來擷取目前權重。這與擷取緩存中的記錄數的Cache.estimatedSize() 方法有所不同。

緩存的最大值(maximum)或最大權重(weight)可以通過getMaximum()方法來讀取,并使用setMaximum(long)進行調整。當緩存量達到新的閥值的時候緩存才會去驅逐緩存。

如果有需用我們可以通過hottest(int) 和 coldest(int)方法來擷取最有可能命中的資料和最有可能驅逐的資料快照。

Time-based

cache.policy().expireAfterAccess().ifPresent(expiration -> ...);
cache.policy().expireAfterWrite().ifPresent(expiration -> ...);
cache.policy().expireVariably().ifPresent(expiration -> ...);
cache.policy().refreshAfterWrite().ifPresent(expiration -> ...);

cache.policy().expireAfterAccess().ifPresent(expiration -> ...);
cache.policy().expireAfterWrite().ifPresent(expiration -> ...);
cache.policy().expireVariably().ifPresent(expiration -> ...);
cache.policy().refreshAfterWrite().ifPresent(expiration -> ...);      

ageOf(key,TimeUnit) 提供了從expireAfterAccess,expireAfterWrite或refreshAfterWrite政策的角度來看條目已經空閑的時間。最大持續時間可以從getExpiresAfter(TimeUnit)讀取,并使用setExpiresAfter(long,TimeUnit)進行調整。

如果有需用我們可以通過hottest(int) 和 coldest(int)方法來擷取最有可能命中的資料和最有可能驅逐的資料快照。

測試(Testing)

FakeTicker ticker = new FakeTicker(); // Guava's testlib
Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .executor(Runnable::run)
    .ticker(ticker::read)
    .maximumSize(10)
    .build();

cache.put(key, graph);
ticker.advance(30, TimeUnit.MINUTES)
assertThat(cache.getIfPresent(key), is(nullValue());

FakeTicker ticker = new FakeTicker(); // Guava's testlib
Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .executor(Runnable::run)
    .ticker(ticker::read)
    .maximumSize(10)
    .build();

cache.put(key, graph);
ticker.advance(30, TimeUnit.MINUTES)
assertThat(cache.getIfPresent(key), is(nullValue());      

測試的時候我們可以使用Caffeine..ticker(ticker)來指定一個時間源,并不需要等到key過期。

FakeTicker這個是guawa test包裡面的Ticker,主要用于測試。依賴:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava-testlib</artifactId>
    <version>23.5-jre</version>
</dependency>      

常見問題(Faq)

固定資料(Pinning Entries)

固定資料是不能通過驅逐政策去将資料删除的。當資料是一個有狀态的資源時(如鎖),那麼這條資料是非常有用的,你有在客端使用完這個條資料的時候才能删除該資料。在這種情況下如果驅逐政策将這個條資料删掉的話,将導緻資源洩露。

通過使用權重将該資料的權重設定成0,并且這個條資料不計入maximum size裡面。 當緩存達到maximum size 了以後,驅逐政策也會跳過該條資料,不會進行删除操作。我們還必須自定義一個标準來判斷這個資料是否屬于固定資料。

通過使用Long.MAX_VALUE(大約300年)的值作為key的有效時間,這樣可以将一條資料從過期中排除。自定義到期必須定義,這可以評估條目是否固定。

将資料寫入緩存時我們要指定該資料的權重和到期時間。這可以通過使用cache.asMap()擷取緩存清單後,再來實作引腳和解除綁定。

遞歸調用(Recursive Computations)

在原子操作内執行的加載,計算或回調可能不會寫入緩存。 ConcurrentHashMap不允許這些遞歸寫操作,因為這将有可能導緻活鎖(Java 8)或IllegalStateException(Java 9)。

解決方法是異步執行這些操作,例如使用AsyncLoadingCache。在異步這種情況下映射已經建立,value是一個CompletableFuture,并且這些操作是在緩存的原子範圍之外執行的。但是,如果發生無序的依賴鍊,這也有可能導緻死鎖。

示例代碼:

package com.xiaolyuh.controller;

import com.alibaba.fastjson.JSON;
import com.github.benmanes.caffeine.cache.*;
import com.google.common.testing.FakeTicker;
import com.xiaolyuh.entity.Person;
import com.xiaolyuh.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

@RestController
public class CaffeineCacheController {

    @Autowired
    PersonService personService;

    Cache<String, Object> manualCache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(10_000)
            .build();

    LoadingCache<String, Object> loadingCache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build(key -> createExpensiveGraph(key));

    AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            // Either: Build with a synchronous computation that is wrapped as asynchronous
            .buildAsync(key -> createExpensiveGraph(key));
    // Or: Build with a asynchronous computation that returns a future
    // .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

    private CompletableFuture<Object> createExpensiveGraphAsync(String key, Executor executor) {
        CompletableFuture<Object> objectCompletableFuture = new CompletableFuture<>();
        return objectCompletableFuture;
    }

    private Object createExpensiveGraph(String key) {
        System.out.println("緩存不存在或過期,調用了createExpensiveGraph方法擷取緩存key的值");
        if (key.equals("name")) {
            throw new RuntimeException("調用了該方法擷取緩存key的值的時候出現異常");
        }
        return personService.findOne1();
    }

    @RequestMapping("/testManual")
    public Object testManual(Person person) {
        String key = "name1";
        Object graph = null;

        // 根據key查詢一個緩存,如果沒有傳回NULL
        graph = manualCache.getIfPresent(key);
        // 根據Key查詢一個緩存,如果沒有調用createExpensiveGraph方法,并将傳回值儲存到緩存。
        // 如果該方法傳回Null則manualCache.get傳回null,如果該方法抛出異常則manualCache.get抛出異常
        graph = manualCache.get(key, k -> createExpensiveGraph(k));
        // 将一個值放入緩存,如果以前有值就覆寫以前的值
        manualCache.put(key, graph);
        // 删除一個緩存
        manualCache.invalidate(key);

        ConcurrentMap<String, Object> map = manualCache.asMap();
        System.out.println(map.toString());
        return graph;
    }

    @RequestMapping("/testLoading")
    public Object testLoading(Person person) {
        String key = "name1";

        // 采用同步方式去擷取一個緩存和上面的手動方式是一個原理。在build Cache的時候會提供一個createExpensiveGraph函數。
        // 查詢并在缺失的情況下使用同步的方式來建構一個緩存
        Object graph = loadingCache.get(key);

        // 擷取組key的值傳回一個Map
        List<String> keys = new ArrayList<>();
        keys.add(key);
        Map<String, Object> graphs = loadingCache.getAll(keys);
        return graph;
    }

    @RequestMapping("/testAsyncLoading")
    public Object testAsyncLoading(Person person) {
        String key = "name1";

        // 查詢并在缺失的情況下使用異步的方式來建構緩存
        CompletableFuture<Object> graph = asyncLoadingCache.get(key);
        // 查詢一組緩存并在缺失的情況下使用異步的方式來建構緩存
        List<String> keys = new ArrayList<>();
        keys.add(key);
        CompletableFuture<Map<String, Object>> graphs = asyncLoadingCache.getAll(keys);

        // 異步轉同步
        loadingCache = asyncLoadingCache.synchronous();
        return graph;
    }

    @RequestMapping("/testSizeBased")
    public Object testSizeBased(Person person) {
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .maximumSize(1)
                .build(k -> createExpensiveGraph(k));

        cache.get("A");
        System.out.println(cache.estimatedSize());
        cache.get("B");
        // 因為執行回收的方法是異步的,是以需要調用該方法,手動觸發一次回收操作。
        cache.cleanUp();
        System.out.println(cache.estimatedSize());

        return "";
    }

    @RequestMapping("/testTimeBased")
    public Object testTimeBased(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
                .ticker(ticker::read)
                .expireAfterAccess(5, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        System.out.println("expireAfterAccess:第一次擷取緩存");
        cache1.get(key);

        System.out.println("expireAfterAccess:等待4.9S後,第二次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(4900, TimeUnit.MILLISECONDS);
        cache1.get(key);

        System.out.println("expireAfterAccess:等待0.101S後,第三次次擷取緩存");
        ticker.advance(101, TimeUnit.MILLISECONDS);
        cache1.get(key);

        // expireAfterWrite
        LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
                .ticker(ticker::read)
                .expireAfterWrite(5, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        System.out.println("expireAfterWrite:第一次擷取緩存");
        cache2.get(key);

        System.out.println("expireAfterWrite:等待4.9S後,第二次次擷取緩存");
        ticker.advance(4900, TimeUnit.MILLISECONDS);
        cache2.get(key);

        System.out.println("expireAfterWrite:等待0.101S後,第三次次擷取緩存");
        ticker.advance(101, TimeUnit.MILLISECONDS);
        cache2.get(key);

        // Evict based on a varying expiration policy
        // 基于不同的到期政策進行退出
        LoadingCache<String, Object> cache3 = Caffeine.newBuilder()
                .ticker(ticker::read)
                .expireAfter(new Expiry<String, Object>() {

                    @Override
                    public long expireAfterCreate(String key, Object value, long currentTime) {
                        // Use wall clock time, rather than nanotime, if from an external resource
                        return TimeUnit.SECONDS.toNanos(5);
                    }

                    @Override
                    public long expireAfterUpdate(String key, Object graph,
                                                  long currentTime, long currentDuration) {

                        System.out.println("調用了 expireAfterUpdate:" + TimeUnit.NANOSECONDS.toMillis(currentDuration));
                        return currentDuration;
                    }

                    @Override
                    public long expireAfterRead(String key, Object graph,
                                                long currentTime, long currentDuration) {

                        System.out.println("調用了 expireAfterRead:" + TimeUnit.NANOSECONDS.toMillis(currentDuration));
                        return currentDuration;
                    }
                })
                .build(k -> createExpensiveGraph(k));

        System.out.println("expireAfter:第一次擷取緩存");
        cache3.get(key);

        System.out.println("expireAfter:等待4.9S後,第二次次擷取緩存");
        ticker.advance(4900, TimeUnit.MILLISECONDS);
        cache3.get(key);

        System.out.println("expireAfter:等待0.101S後,第三次次擷取緩存");
        ticker.advance(101, TimeUnit.MILLISECONDS);
        Object object = cache3.get(key);

        return object;
    }

    @RequestMapping("/testRemoval")
    public Object testRemoval(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .removalListener((String k, Object graph, RemovalCause cause) ->
                        System.out.printf("Key %s was removed (%s)%n", k, cause))
                .ticker(ticker::read)
                .expireAfterAccess(5, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        System.out.println("第一次擷取緩存");
        Object object = cache.get(key);

        System.out.println("等待6S後,第二次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(6000, TimeUnit.MILLISECONDS);
        cache.get(key);

        System.out.println("手動删除緩存");
        cache.invalidate(key);

        return object;
    }

    @RequestMapping("/testRefresh")
    public Object testRefresh(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .removalListener((String k, Object graph, RemovalCause cause) ->
                        System.out.printf("執行移除監聽器- Key %s was removed (%s)%n", k, cause))
                .ticker(ticker::read)
                .expireAfterWrite(5, TimeUnit.SECONDS)
                // 指定在建立緩存或者最近一次更新緩存後經過固定的時間間隔,重新整理緩存
                .refreshAfterWrite(4, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        System.out.println("第一次擷取緩存");
        Object object = cache.get(key);

        System.out.println("等待4.1S後,第二次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(4100, TimeUnit.MILLISECONDS);
        cache.get(key);

        System.out.println("等待5.1S後,第三次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(5100, TimeUnit.MILLISECONDS);
        cache.get(key);

        return object;
    }

    @RequestMapping("/testWriter")
    public Object testWriter(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .removalListener((String k, Object graph, RemovalCause cause) ->
                        System.out.printf("執行移除監聽器- Key %s was removed (%s)%n", k, cause))
                .ticker(ticker::read)
                .expireAfterWrite(5, TimeUnit.SECONDS)
                .writer(new CacheWriter<String, Object>() {
                    @Override
                    public void write(String key, Object graph) {
                        // write to storage or secondary cache
                        // 寫入存儲或者二級緩存
                        System.out.printf("testWriter:write - Key %s was write (%s)%n", key, graph);
                        createExpensiveGraph(key);
                    }

                    @Override
                    public void delete(String key, Object graph, RemovalCause cause) {
                        // delete from storage or secondary cache
                        // 删除存儲或者二級緩存
                        System.out.printf("testWriter:delete - Key %s was delete (%s)%n", key, graph);
                    }
                })
                // 指定在建立緩存或者最近一次更新緩存後經過固定的時間間隔,重新整理緩存
                .refreshAfterWrite(4, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        cache.put(key, personService.findOne1());
        cache.invalidate(key);

        System.out.println("第一次擷取緩存");
        Object object = cache.get(key);

        System.out.println("等待4.1S後,第二次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(4100, TimeUnit.MILLISECONDS);
        cache.get(key);

        System.out.println("等待5.1S後,第三次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(5100, TimeUnit.MILLISECONDS);
        cache.get(key);

        return object;
    }

    @RequestMapping("/testStatistics")
    public Object testStatistics(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .removalListener((String k, Object graph, RemovalCause cause) ->
                        System.out.printf("執行移除監聽器- Key %s was removed (%s)%n", k, cause))
                .ticker(ticker::read)
                .expireAfterWrite(5, TimeUnit.SECONDS)
                // 開啟統計
                .recordStats()
                // 指定在建立緩存或者最近一次更新緩存後經過固定的時間間隔,重新整理緩存
                .refreshAfterWrite(4, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        for (int i = 0; i < 10; i++) {
            cache.get(key);
            cache.get(key + i);
        }
        // 驅逐是異步操作,是以這裡要手動觸發一次回收操作
        ticker.advance(5100, TimeUnit.MILLISECONDS);
        // 手動觸發一次回收操作
        cache.cleanUp();

        System.out.println("緩存命數量:" + cache.stats().hitCount());
        System.out.println("緩存命中率:" + cache.stats().hitRate());
        System.out.println("緩存逐出的數量:" + cache.stats().evictionCount());
        System.out.println("加載新值所花費的平均時間:" + cache.stats().averageLoadPenalty());

        return cache.get(key);
    }

    @RequestMapping("/testPolicy")
    public Object testPolicy(Person person) {
        FakeTicker ticker = new FakeTicker();

        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .ticker(ticker::read)
                .expireAfterAccess(5, TimeUnit.SECONDS)
                .maximumSize(1)
                .build(k -> createExpensiveGraph(k));

        // 在代碼裡面動态的指定最大Size
        cache.policy().eviction().ifPresent(eviction -> {
            eviction.setMaximum(4 * eviction.getMaximum());
        });

        cache.get("E");
        cache.get("B");
        cache.get("C");
        cache.cleanUp();
        System.out.println(cache.estimatedSize() + ":" + JSON.toJSON(cache.asMap()).toString());

        cache.get("A");
        ticker.advance(100, TimeUnit.MILLISECONDS);
        cache.get("D");
        ticker.advance(100, TimeUnit.MILLISECONDS);
        cache.get("A");
        ticker.advance(100, TimeUnit.MILLISECONDS);
        cache.get("B");
        ticker.advance(100, TimeUnit.MILLISECONDS);
        cache.policy().eviction().ifPresent(eviction -> {
            // 擷取熱點資料Map
            Map<String, Object> hottestMap = eviction.hottest(10);
            // 擷取冷資料Map
            Map<String, Object> coldestMap = eviction.coldest(10);

            System.out.println("熱點資料:" + JSON.toJSON(hottestMap).toString());
            System.out.println("冷資料:" + JSON.toJSON(coldestMap).toString());
        });

        ticker.advance(3000, TimeUnit.MILLISECONDS);
        // ageOf通過這個方法來檢視key的空閑時間
        cache.policy().expireAfterAccess().ifPresent(expiration -> {

            System.out.println(JSON.toJSON(expiration.ageOf("A", TimeUnit.MILLISECONDS)));
        });
        return cache.get("name1");
    }
}

package com.xiaolyuh.controller;

import com.alibaba.fastjson.JSON;
import com.github.benmanes.caffeine.cache.*;
import com.google.common.testing.FakeTicker;
import com.xiaolyuh.entity.Person;
import com.xiaolyuh.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

@RestController
public class CaffeineCacheController {

    @Autowired
    PersonService personService;

    Cache<String, Object> manualCache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(10_000)
            .build();

    LoadingCache<String, Object> loadingCache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build(key -> createExpensiveGraph(key));

    AsyncLoadingCache<String, Object> asyncLoadingCache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            // Either: Build with a synchronous computation that is wrapped as asynchronous
            .buildAsync(key -> createExpensiveGraph(key));
    // Or: Build with a asynchronous computation that returns a future
    // .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

    private CompletableFuture<Object> createExpensiveGraphAsync(String key, Executor executor) {
        CompletableFuture<Object> objectCompletableFuture = new CompletableFuture<>();
        return objectCompletableFuture;
    }

    private Object createExpensiveGraph(String key) {
        System.out.println("緩存不存在或過期,調用了createExpensiveGraph方法擷取緩存key的值");
        if (key.equals("name")) {
            throw new RuntimeException("調用了該方法擷取緩存key的值的時候出現異常");
        }
        return personService.findOne1();
    }

    @RequestMapping("/testManual")
    public Object testManual(Person person) {
        String key = "name1";
        Object graph = null;

        // 根據key查詢一個緩存,如果沒有傳回NULL
        graph = manualCache.getIfPresent(key);
        // 根據Key查詢一個緩存,如果沒有調用createExpensiveGraph方法,并将傳回值儲存到緩存。
        // 如果該方法傳回Null則manualCache.get傳回null,如果該方法抛出異常則manualCache.get抛出異常
        graph = manualCache.get(key, k -> createExpensiveGraph(k));
        // 将一個值放入緩存,如果以前有值就覆寫以前的值
        manualCache.put(key, graph);
        // 删除一個緩存
        manualCache.invalidate(key);

        ConcurrentMap<String, Object> map = manualCache.asMap();
        System.out.println(map.toString());
        return graph;
    }

    @RequestMapping("/testLoading")
    public Object testLoading(Person person) {
        String key = "name1";

        // 采用同步方式去擷取一個緩存和上面的手動方式是一個原理。在build Cache的時候會提供一個createExpensiveGraph函數。
        // 查詢并在缺失的情況下使用同步的方式來建構一個緩存
        Object graph = loadingCache.get(key);

        // 擷取組key的值傳回一個Map
        List<String> keys = new ArrayList<>();
        keys.add(key);
        Map<String, Object> graphs = loadingCache.getAll(keys);
        return graph;
    }

    @RequestMapping("/testAsyncLoading")
    public Object testAsyncLoading(Person person) {
        String key = "name1";

        // 查詢并在缺失的情況下使用異步的方式來建構緩存
        CompletableFuture<Object> graph = asyncLoadingCache.get(key);
        // 查詢一組緩存并在缺失的情況下使用異步的方式來建構緩存
        List<String> keys = new ArrayList<>();
        keys.add(key);
        CompletableFuture<Map<String, Object>> graphs = asyncLoadingCache.getAll(keys);

        // 異步轉同步
        loadingCache = asyncLoadingCache.synchronous();
        return graph;
    }

    @RequestMapping("/testSizeBased")
    public Object testSizeBased(Person person) {
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .maximumSize(1)
                .build(k -> createExpensiveGraph(k));

        cache.get("A");
        System.out.println(cache.estimatedSize());
        cache.get("B");
        // 因為執行回收的方法是異步的,是以需要調用該方法,手動觸發一次回收操作。
        cache.cleanUp();
        System.out.println(cache.estimatedSize());

        return "";
    }

    @RequestMapping("/testTimeBased")
    public Object testTimeBased(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
                .ticker(ticker::read)
                .expireAfterAccess(5, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        System.out.println("expireAfterAccess:第一次擷取緩存");
        cache1.get(key);

        System.out.println("expireAfterAccess:等待4.9S後,第二次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(4900, TimeUnit.MILLISECONDS);
        cache1.get(key);

        System.out.println("expireAfterAccess:等待0.101S後,第三次次擷取緩存");
        ticker.advance(101, TimeUnit.MILLISECONDS);
        cache1.get(key);

        // expireAfterWrite
        LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
                .ticker(ticker::read)
                .expireAfterWrite(5, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        System.out.println("expireAfterWrite:第一次擷取緩存");
        cache2.get(key);

        System.out.println("expireAfterWrite:等待4.9S後,第二次次擷取緩存");
        ticker.advance(4900, TimeUnit.MILLISECONDS);
        cache2.get(key);

        System.out.println("expireAfterWrite:等待0.101S後,第三次次擷取緩存");
        ticker.advance(101, TimeUnit.MILLISECONDS);
        cache2.get(key);

        // Evict based on a varying expiration policy
        // 基于不同的到期政策進行退出
        LoadingCache<String, Object> cache3 = Caffeine.newBuilder()
                .ticker(ticker::read)
                .expireAfter(new Expiry<String, Object>() {

                    @Override
                    public long expireAfterCreate(String key, Object value, long currentTime) {
                        // Use wall clock time, rather than nanotime, if from an external resource
                        return TimeUnit.SECONDS.toNanos(5);
                    }

                    @Override
                    public long expireAfterUpdate(String key, Object graph,
                                                  long currentTime, long currentDuration) {

                        System.out.println("調用了 expireAfterUpdate:" + TimeUnit.NANOSECONDS.toMillis(currentDuration));
                        return currentDuration;
                    }

                    @Override
                    public long expireAfterRead(String key, Object graph,
                                                long currentTime, long currentDuration) {

                        System.out.println("調用了 expireAfterRead:" + TimeUnit.NANOSECONDS.toMillis(currentDuration));
                        return currentDuration;
                    }
                })
                .build(k -> createExpensiveGraph(k));

        System.out.println("expireAfter:第一次擷取緩存");
        cache3.get(key);

        System.out.println("expireAfter:等待4.9S後,第二次次擷取緩存");
        ticker.advance(4900, TimeUnit.MILLISECONDS);
        cache3.get(key);

        System.out.println("expireAfter:等待0.101S後,第三次次擷取緩存");
        ticker.advance(101, TimeUnit.MILLISECONDS);
        Object object = cache3.get(key);

        return object;
    }

    @RequestMapping("/testRemoval")
    public Object testRemoval(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .removalListener((String k, Object graph, RemovalCause cause) ->
                        System.out.printf("Key %s was removed (%s)%n", k, cause))
                .ticker(ticker::read)
                .expireAfterAccess(5, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        System.out.println("第一次擷取緩存");
        Object object = cache.get(key);

        System.out.println("等待6S後,第二次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(6000, TimeUnit.MILLISECONDS);
        cache.get(key);

        System.out.println("手動删除緩存");
        cache.invalidate(key);

        return object;
    }

    @RequestMapping("/testRefresh")
    public Object testRefresh(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .removalListener((String k, Object graph, RemovalCause cause) ->
                        System.out.printf("執行移除監聽器- Key %s was removed (%s)%n", k, cause))
                .ticker(ticker::read)
                .expireAfterWrite(5, TimeUnit.SECONDS)
                // 指定在建立緩存或者最近一次更新緩存後經過固定的時間間隔,重新整理緩存
                .refreshAfterWrite(4, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        System.out.println("第一次擷取緩存");
        Object object = cache.get(key);

        System.out.println("等待4.1S後,第二次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(4100, TimeUnit.MILLISECONDS);
        cache.get(key);

        System.out.println("等待5.1S後,第三次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(5100, TimeUnit.MILLISECONDS);
        cache.get(key);

        return object;
    }

    @RequestMapping("/testWriter")
    public Object testWriter(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .removalListener((String k, Object graph, RemovalCause cause) ->
                        System.out.printf("執行移除監聽器- Key %s was removed (%s)%n", k, cause))
                .ticker(ticker::read)
                .expireAfterWrite(5, TimeUnit.SECONDS)
                .writer(new CacheWriter<String, Object>() {
                    @Override
                    public void write(String key, Object graph) {
                        // write to storage or secondary cache
                        // 寫入存儲或者二級緩存
                        System.out.printf("testWriter:write - Key %s was write (%s)%n", key, graph);
                        createExpensiveGraph(key);
                    }

                    @Override
                    public void delete(String key, Object graph, RemovalCause cause) {
                        // delete from storage or secondary cache
                        // 删除存儲或者二級緩存
                        System.out.printf("testWriter:delete - Key %s was delete (%s)%n", key, graph);
                    }
                })
                // 指定在建立緩存或者最近一次更新緩存後經過固定的時間間隔,重新整理緩存
                .refreshAfterWrite(4, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        cache.put(key, personService.findOne1());
        cache.invalidate(key);

        System.out.println("第一次擷取緩存");
        Object object = cache.get(key);

        System.out.println("等待4.1S後,第二次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(4100, TimeUnit.MILLISECONDS);
        cache.get(key);

        System.out.println("等待5.1S後,第三次次擷取緩存");
        // 直接指定時鐘
        ticker.advance(5100, TimeUnit.MILLISECONDS);
        cache.get(key);

        return object;
    }

    @RequestMapping("/testStatistics")
    public Object testStatistics(Person person) {
        String key = "name1";
        // 使用者測試,一個時間源,傳回一個時間值,表示從某個固定但任意時間點開始經過的納秒數。
        FakeTicker ticker = new FakeTicker();

        // 基于固定的到期政策進行退出
        // expireAfterAccess
        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .removalListener((String k, Object graph, RemovalCause cause) ->
                        System.out.printf("執行移除監聽器- Key %s was removed (%s)%n", k, cause))
                .ticker(ticker::read)
                .expireAfterWrite(5, TimeUnit.SECONDS)
                // 開啟統計
                .recordStats()
                // 指定在建立緩存或者最近一次更新緩存後經過固定的時間間隔,重新整理緩存
                .refreshAfterWrite(4, TimeUnit.SECONDS)
                .build(k -> createExpensiveGraph(k));

        for (int i = 0; i < 10; i++) {
            cache.get(key);
            cache.get(key + i);
        }
        // 驅逐是異步操作,是以這裡要手動觸發一次回收操作
        ticker.advance(5100, TimeUnit.MILLISECONDS);
        // 手動觸發一次回收操作
        cache.cleanUp();

        System.out.println("緩存命數量:" + cache.stats().hitCount());
        System.out.println("緩存命中率:" + cache.stats().hitRate());
        System.out.println("緩存逐出的數量:" + cache.stats().evictionCount());
        System.out.println("加載新值所花費的平均時間:" + cache.stats().averageLoadPenalty());

        return cache.get(key);
    }

    @RequestMapping("/testPolicy")
    public Object testPolicy(Person person) {
        FakeTicker ticker = new FakeTicker();

        LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .ticker(ticker::read)
                .expireAfterAccess(5, TimeUnit.SECONDS)
                .maximumSize(1)
                .build(k -> createExpensiveGraph(k));

        // 在代碼裡面動态的指定最大Size
        cache.policy().eviction().ifPresent(eviction -> {
            eviction.setMaximum(4 * eviction.getMaximum());
        });

        cache.get("E");
        cache.get("B");
        cache.get("C");
        cache.cleanUp();
        System.out.println(cache.estimatedSize() + ":" + JSON.toJSON(cache.asMap()).toString());

        cache.get("A");
        ticker.advance(100, TimeUnit.MILLISECONDS);
        cache.get("D");
        ticker.advance(100, TimeUnit.MILLISECONDS);
        cache.get("A");
        ticker.advance(100, TimeUnit.MILLISECONDS);
        cache.get("B");
        ticker.advance(100, TimeUnit.MILLISECONDS);
        cache.policy().eviction().ifPresent(eviction -> {
            // 擷取熱點資料Map
            Map<String, Object> hottestMap = eviction.hottest(10);
            // 擷取冷資料Map
            Map<String, Object> coldestMap = eviction.coldest(10);

            System.out.println("熱點資料:" + JSON.toJSON(hottestMap).toString());
            System.out.println("冷資料:" + JSON.toJSON(coldestMap).toString());
        });

        ticker.advance(3000, TimeUnit.MILLISECONDS);
        // ageOf通過這個方法來檢視key的空閑時間
        cache.policy().expireAfterAccess().ifPresent(expiration -> {

            System.out.println(JSON.toJSON(expiration.ageOf("A", TimeUnit.MILLISECONDS)));
        });
        return cache.get("name1");
    }
}      

英文不好,有些翻譯的不準确的請不吝賜教。。。

參考:

  • ​​https://github.com/ben-manes/caffeine/blob/master/README.md​​
  • ​​http://oopsguy.com/2017/10/25/java-caching-caffeine/​​