本文咱們將介紹Caffeine-一個Java高性能緩存庫。緩存和Map之間的一個根本區別是緩存會將儲存的元素逐出。逐出策略決定了在什麼時間應該刪除哪些對象,逐出策略直接影響緩存的命中率,這是緩存庫的關鍵特徵。Caffeine使用Window TinyLfu逐出策略,該策略提供了接近最佳的命中率。java
首先在pom.xml文件中添加Caffeine相關依賴:git
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.5.5</version>
</dependency>
複製代碼
您能夠在Maven Central上找到最新版本的Caffeine。github
讓咱們集中討論Caffeine的三種緩存填充策略:手動,同步加載和異步加載。緩存
首先,讓咱們建立一個用於存儲到緩存中的DataObject類:異步
class DataObject {
private final String data;
private static int objectCounter = 0;
// standard constructors/getters
public static DataObject get(String data) {
objectCounter++;
return new DataObject(data);
}
}
複製代碼
在這種策略中,咱們手動將值插入緩存中,並在後面檢索它們。ide
讓咱們初始化緩存:函數
Cache<String, DataObject> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();
複製代碼
如今,咱們可使用getIfPresent方法從緩存中獲取值。若是緩存中不存在該值,則此方法將返回null:性能
String key = "A";
DataObject dataObject = cache.getIfPresent(key);
assertNull(dataObject);
複製代碼
咱們可使用put方法手動將值插入緩存:ui
cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);
assertNotNull(dataObject);
複製代碼
咱們還可使用get方法獲取值,該方法將Lambda函數和鍵做爲參數。若是緩存中不存在此鍵,則此Lambda函數將用於提供返回值,而且該返回值將在計算後插入緩存中:atom
dataObject = cache
.get(key, k -> DataObject.get("Data for A"));
assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());
複製代碼
get方法以原子方式(atomically)執行計算。這意味着計算將只進行一次,即便多個線程同時請求該值。這就是爲何使用get比getIfPresent更好。
有時咱們須要手動使某些緩存的值無效:
cache.invalidate(key);
dataObject = cache.getIfPresent(key);
assertNull(dataObject);
複製代碼
這種加載緩存的方法具備一個函數,該函數用於初始化值,相似於手動策略的get方法。讓咱們看看如何使用它。
首先,咱們須要初始化緩存:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
複製代碼
如今,咱們可使用get方法檢索值:
DataObject dataObject = cache.get(key);
assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());
複製代碼
咱們還可使用getAll方法得到一組值:
Map<String, DataObject> dataObjectMap
= cache.getAll(Arrays.asList("A", "B", "C"));
assertEquals(3, dataObjectMap.size());
複製代碼
從傳遞給build方法的初始化函數中檢索值。這樣就能夠經過緩存在來裝飾訪問值。
該策略與先前的策略相同,可是異步執行操做,並返回保存實際值的CompletableFuture:
AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(k -> DataObject.get("Data for " + k));
複製代碼
考慮到它們返回CompletableFuture的事實,咱們能夠以相同的方式使用get和getAll方法:
String key = "A";
cache.get(key).thenAccept(dataObject -> {
assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());
});
cache.getAll(Arrays.asList("A", "B", "C"))
.thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));
複製代碼
CompletableFuture具備豐富而有用的API,您能夠在本文中瞭解更多信息。
Caffeine具備三種元素逐出策略:基於容量,基於時間和基於引用。
這種逐出發生在超過配置的緩存容量大小限制時。有兩種獲取容量當前佔用量的方法,計算緩存中的對象數量或獲取它們的權重。
讓咱們看看如何處理緩存中的對象。初始化高速緩存時,其大小等於零:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumSize(1)
.build(k -> DataObject.get("Data for " + k));
assertEquals(0, cache.estimatedSize());
複製代碼
當咱們添加一個值時,大小顯然會增長:
cache.get("A");
assertEquals(1, cache.estimatedSize());
複製代碼
咱們能夠將第二個值添加到緩存中,從而致使刪除第一個值:
cache.get("B");
cache.cleanUp();
assertEquals(1, cache.estimatedSize());
複製代碼
值得一提的是,在獲取緩存大小以前,咱們先調用cleanUp方法。這是由於緩存逐出是異步執行的,而且此方法有助於等待逐出操做的完成。
咱們還能夠傳遞一個***weigher***函數來指定緩存值的權重大小:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumWeight(10)
.weigher((k,v) -> 5)
.build(k -> DataObject.get("Data for " + k));
assertEquals(0, cache.estimatedSize());
cache.get("A");
assertEquals(1, cache.estimatedSize());
cache.get("B");
assertEquals(2, cache.estimatedSize());
複製代碼
當權重超過10時,將按照時間順序從緩存中刪除多餘的值:
cache.get("C");
cache.cleanUp();
assertEquals(2, cache.estimatedSize());
複製代碼
此逐出策略基於元素的到期時間,並具備三種類型:
讓咱們使用expireAfterAccess方法配置訪問後過時策略:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
複製代碼
要配置寫後過時策略,咱們使用expireAfterWrite方法:
cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
.build(k -> DataObject.get("Data for " + k));
複製代碼
要初始化自定義策略,咱們須要實現Expiry接口:
cache = Caffeine.newBuilder().expireAfter(new Expiry<String, DataObject>() {
@Override
public long expireAfterCreate( String key, DataObject value, long currentTime) {
return value.getData().length() * 1000;
}
@Override
public long expireAfterUpdate( String key, DataObject value, long currentTime, long currentDuration) {
return currentDuration;
}
@Override
public long expireAfterRead( String key, DataObject value, long currentTime, long currentDuration) {
return currentDuration;
}
}).build(k -> DataObject.get("Data for " + k));
複製代碼
咱們能夠將緩存配置爲容許垃圾回收緩存的鍵或值。爲此,咱們將爲鍵和值配置WeakRefence的用法,而且咱們只能爲值的垃圾收集配置爲SoftReference。
當對象沒有任何強引用時,WeakRefence用法容許對對象進行垃圾回收。 SoftReference容許根據JVM的全局「最近最少使用」策略對對象進行垃圾收集。有關Java引用的更多詳細信息,請參見此處。
咱們應該使用Caffeine.weakKeys(),Caffeine.weakValues()和Caffeine.softValues()來啓用每一個選項:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
.build(k -> DataObject.get("Data for " + k));
cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.softValues()
.build(k -> DataObject.get("Data for " + k));
複製代碼
能夠將緩存配置爲在定義的時間段後自動刷新元素。讓咱們看看如何使用refreshAfterWrite方法執行此操做:
Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
複製代碼
在這裏,咱們應該瞭解expireAfter和refreshAfter之間的區別。前者當請求過時元素時,執行將阻塞,直到build()計算出新值爲止。
可是後者將返回舊值並異步計算出新值並插入緩存中,此時被刷新的元素的過時時間將從新開始計時計算。
Caffeine能夠記錄有關緩存使用狀況的統計信息:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumSize(100)
.recordStats()
.build(k -> DataObject.get("Data for " + k));
cache.get("A");
cache.get("A");
assertEquals(1, cache.stats().hitCount());
assertEquals(1, cache.stats().missCount());
複製代碼
咱們將recordStats傳遞給它,recordStats建立StatsCounter的實現。每次與統計相關的更改都將推送給此對象。
在本文中,咱們熟悉了Java的Caffeine緩存庫。咱們瞭解瞭如何配置和填充緩存,以及如何根據須要選擇適當的過時或刷新策略。
原文地址:www.baeldung.com/java-cachin…
🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟
歡迎訪問筆者博客:blog.dongxishaonian.tech
關注筆者公衆號,推送各種原創/優質技術文章 ⬇️