天天看點

高性能緩存類庫Caffeine介紹介紹特性緩存加載淘汰政策緩存移除緩存更新緩存寫入推薦使用結語

介紹

Caffeine 是一個高性能、出色的緩存類庫,基于Java 8。它的性能非常的出色,API也比較友好,本篇,我們就來介紹一下Caffeine 使用。

特性

Caffeine使用的是一個記憶體緩存,是基于Google 的 Guava與ConcurrentLinkedHashMap進行實作的。

Maven位址:

<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>2.7.0</version>
</dependency>
           

我們首先來看一個其使用的demo:

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));
           

就這樣,我們即可建立一個緩存結構,其中createExpensiveGraph()方法是我們自行定義的,用于生成緩存的邏輯,其他的方法我們将會在後面進行依次介紹。

Caffeine 提供了多種建構方式建立一個緩存,我們來看一下它的主要特性:

  • 自動加載資料放入緩存,支援異步方式
  • 基于緩存容量的淘汰政策,當存儲的元素超過最大值的時候,根據使用元素最近的使用頻率政策進行淘汰
  • 基于過期時間的淘汰政策
  • 異步重新整理政策
  • key值自動包裝成弱引用
  • 元素值自動包裝成弱引用或軟引用
  • 通知淘汰元素政策
  • 向外部存儲資源寫入元素
  • 統計緩存通路資訊

以上就是Caffeine 的主要特性,接下來,我們就對上面的特性中比較常用的幾個,進行展開詳細介紹一下。

緩存加載

緩存加載是Caffeine 的最基礎特性,其支援四中模式的加載政策:手工加載、同步加載、異步加載、異步手動加載。

手動加載

首先,我們來看一下手動加載:

//Build a manual cache
Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(1000)
    .build();

// Lookup an entry, or null if not found
Graph graph = cache.getIfPresent(key);

// Lookup and compute an entry if absent, or null if not computable
graph = cache.get(key, k -> createExpensiveGraph(key));

// Insert or update an entry
cache.put(key, graph);

// Remove an entry
cache.invalidate(key);

//Cache entry view
cache.asMap();
           

上面的demo就是手動加載一個緩存元素的流程,Cache 接口允許精确的控制檢索、更新與廢棄一個元素。

在建構過程中,我們可以指定key值的失效時間,以及緩存的最大容量。

使用cache.get(key, k -> createExpensiveGraph(key))時,會首先檢查緩存中是否已經key值對應的元素值,如果不存在,通過createExpensiveGraph()這個自定義的方法來初始化一個元素,放入緩存中來。

同步加載

使用手工加載的方式可以給我們帶來更大的靈活性,但是總是手動去加載緩存,有時未免有些不便,這種情況下,我們可以使用自動同步加載的方式。

//Build a loading synchronously cache
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

// Lookup and compute an entry if absent, or null if not computable
Graph graph = cache.get(key);

// Lookup and compute entries that are absent
Map<Key, Graph> graphs = cache.getAll(keys);
           

同步加載模式的建構與手動加載模式的建構方式與使用非常相似,隻不過使用是LoadingCache接口進行的建構。

LoadingCache支援批量擷取緩存,可以通過getAll()方法,預設的情況下,getAll()方法的實作将會循環調用get()方法,擷取緩存值,如果批量擷取的key值過多時,需要考慮性能;同時,也可以通過實作CacheLoader.loadAll()方法,自行實作批量擷取緩存的邏輯。

手動異步加載

//Build a manual asynchronous cache
AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(1000)
    .buildAsync();

// Lookup and asynchronously compute an entry if absent
CompletableFuture<Graph> graph = cache.get(key, k -> createExpensiveGraph(key));
           

AsyncCache 是 Cache 的一個變體類,其内部使用一個Executor進行實作,通過get()方法傳回一個CompletableFuture類,通過這個方法可以建構響應式程式設計模型。

内部預設使用的executor 是 ForkJoinPool.commonPool(),可以通過重寫Caffeine.executor(Executor)來自行指定線程池。

異步加載

看完了上面幾種緩存加載方式,我們接下來看一下最後一種加載方式,也是我個人最推薦使用的加載方式,異步加載:

//Build a loading asynchronous cache
AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(1000)
    .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));

// Lookup and asynchronously compute an entry if absent
CompletableFuture<Graph> graph = cache.get(key);

// Lookup and asynchronously compute entries that are absent
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);
           

異步加載是通過AsyncLoadingCache接口實作的,建構方式基本與同步加載相同,隻不過需要注意的是,其額外提供了buildAsync((key, executor))的構造方式,可以支援傳入Executor執行器。

淘汰政策

我們在最前面提到過,Caffeine是支援淘汰政策的,其支援三種政策:基于大小、基于過期時間、基于引用,我們分别來介紹一下。

基于大小

// Evict based on the number of entries in the cache
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(1000)
    .build(key -> createExpensiveGraph(key));
           

當建構一個緩存的時候,我們可以通過maximumSize()方法來指定緩存的最大容量,當到達容量門檻值時,Caffeine 将基于Window TinyLfu政策進行緩存淘汰。根據Caffeine 的文檔描述,它并沒有采用比較常見的LRU淘汰政策,是因為LRU的淘汰政策命中率比較低,有可能會觸發全量掃描。采用Window TinyLfu的原因是其擁有較高的命中率,同時較少的記憶體使用。

這種模式的淘汰政策是比較推薦的。

同時,我們還可以使用另外一種方式來指定淘汰政策,使用權重的方式。

// Evict based on the number of vertices in the cache
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumWeight(1000)
    .weigher((Key key, Graph graph) -> graph.vertices().size())
    .build(key -> createExpensiveGraph(key));
           

可以使用maximumWeight()方法來指定最大權重值,但是關于權重的淘汰政策,其官方文檔的描述我沒有太了解到位,該方式也不是特别常用,這裡就不過多的說明,以防誤導讀者,感興趣的小夥伴可以自行查詢 官方文檔的說明。

基于時間

// 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>() {
      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);
      }
      public long expireAfterUpdate(Key key, Graph graph, 
          long currentTime, long currentDuration) {
        return currentDuration;
      }
      public long expireAfterRead(Key key, Graph graph,
          long currentTime, long currentDuration) {
        return currentDuration;
      }
    })
    .build(key -> createExpensiveGraph(key));
    
           

Caffeine 提供了三種基于時間的淘汰政策:

  • expireAfterAccess(long, TimeUnit):緩存元素被建立後,将會在最後一次被讀寫通路後,在指定時間後過期。

    也就是說,如果一個元素被通路頻度極高,那麼其将一直不會過期。

  • expireAfterWrite(long, TimeUnit):緩存元素被建立後,在指定時間後過期。如果你需要緩存不停的更新變化,建議采用這種模式。
  • expireAfter(Expiry):緩存元素被建立後,按照自定義的過期時間政策,進行過期,這種方式相對比較靈活。

基于引用

// Evict when neither the key nor value are strongly reachable
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));
           

Caffeine 支援設定你的緩存元素為軟引用或弱引用,這樣可以更加友善與GC回收元素。需要注意的是,此種方式不支援AsyncLoadingCache 接口建構緩存。

關于這種方式的緩存建構,并不是特别的常用,也并不是特别的推薦,這裡不占用過多篇幅叙述。

緩存移除

剛剛我們了解了緩存的淘汰政策,淘汰政策是我們在建構緩存結構時,進行設定的,同時,我們也可以手動的移除緩存。

Caffeine 為我們提供了方法,來靈活操控移除緩存元素。

// 移除指定的key
cache.invalidate(key)
// 移除指定的key清單
cache.invalidateAll(keys)
// 移除全部key
cache.invalidateAll()
           

如果,你希望在元素被移除的時候,你可以知道它被移除了,那麼,你可以設定一個監聽器,來監聽移除的行為:

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 提供的refreshAfterWrite()方法,來進行實作:

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));
           

refreshAfterWrite()可以指定時間周期,在資料寫入後,在指定時間後進行更新。這裡的更新,與我們上面提到的淘汰機制有所不同,重新整理資料,是一個異步行為,當在重新整理資料的過程中,舊的緩存值依舊可以被讀取到,而對于淘汰政策,當緩存元素失效後,必須等到新的資料寫入完成後,新的緩存資料才可以被讀取的到。

對比于expireAfterWrite()方法,refreshAfterWrite()方法是一個輕量級的資料更新,重新整理的行為隻有當一個元素被查詢的時候,才進行觸發。我們可以在建構緩存時,同時指定expireAfterWrite()方法與refreshAfterWrite()方法,這樣的話,隻有當資料具備重新整理條件的時候才會去重新整理資料,不會盲目去執行重新整理操作。如果資料在重新整理後就一直沒有被再次查詢,那麼該資料也會過期。

緩存寫入

上面說完了緩存的更新操作,接下來,我們再看一下緩存的寫入。

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可以同步的監聽到緩存的建立、變更和删除操作。

需要注意的是:CacheWriter 不能與weakKeys和AsyncLoadingCache一起使用。

推薦使用

上面的篇幅,我們了解了Caffeine的主要特性與使用方式,接下來我們看一個簡單的例子,也是我在生産環境中使用的例子,希望為讀者做一個簡單的參考:

public class CaffeineCacheDemo {
	AsyncLoadingCache<String, Person> asyncLoadingCache = Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .refreshAfterWrite(1, TimeUnit.MINUTES)
            .buildAsync(key -> doQueryFromDB(key));
     
    public Person doQueryFromStorage() {
			//do query from redis
			//redis do something......
			//if not exists, then query from db, and write for redis
			//db query
			//write redis
	}
}
           

這裡我用一個非常簡單的demo示範了Caffeine的使用,在生産環境中,建議使用AsyncLoadingCache這種異步的方式,來加載緩存,我們可以配合redis,來建構一個三級緩存的模型,具體的代碼這裡沒有給出,請讀者自行發揮,根據自己的實際業務邏輯,來建構多級緩存結構,這裡隻是抛磚引玉。

結語

本篇,我們介紹了高性能緩存類庫Caffeine的使用及其原理,由于篇幅有限,很多細節的點沒有進行一一介紹,請讀者在實際應用中使用的時候,再對其使用細節進行深入研究,本文隻作為入門基礎介紹。

該緩存經過我們生産環境的驗證,證明是非常可靠、高效的,特别對于并發量要求比較大的應用,如果您的應用也有高并發的需求,請不妨嘗試使用一下,好啦,就介紹到這裡,我們下篇再見~

繼續閱讀