介紹
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的使用及其原理,由于篇幅有限,很多細節的點沒有進行一一介紹,請讀者在實際應用中使用的時候,再對其使用細節進行深入研究,本文隻作為入門基礎介紹。
該緩存經過我們生産環境的驗證,證明是非常可靠、高效的,特别對于并發量要求比較大的應用,如果您的應用也有高并發的需求,請不妨嘗試使用一下,好啦,就介紹到這裡,我們下篇再見~