1、簡介
在本文中,我們將瞭解Caffeine,一個用於Java的高效能快取庫。
快取和Map之間的一個根本區別是快取會清理儲存的專案。
一個清理策略會決定在某個給定時間哪些物件應該被刪除,這個策略直接影響快取的命中率——快取庫的一個關鍵特性。
Caffeine使用Window TinyLfu
清理策略,它提供了接近最佳的命中率。
2、依賴
我們需要將Caffeine依賴新增到我們的pom.xml中:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.5.5</version>
</dependency>
您可以在Maven Central上找到最新版本的Caffeine。
3、寫入快取
讓我們關注Caffeine的三種快取寫入策略:手動、同步載入和非同步載入。
首先,讓我們編寫一個類,作為要儲存在快取中的值的型別:
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);
}
}
3.1、手動寫入
在此策略中,我們手動將值寫入快取並稍後讀取它們。
我們先初始化快取:
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
方法手動寫入快取:
cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);
assertNotNull(dataObject);
我們還可以使用get
方法獲取值,該方法接受一個函式和一個鍵作為引數。如果快取中不存在該鍵,則此函式將用於提供兜底值,該值將在執行後寫入快取:
dataObject = cache
.get(key, k -> DataObject.get("Data for A"));
assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());
這個GET方法執行是原子性的。這意味著即使多個執行緒同時請求該值,執行只會進行一次。這就是為什麼使用get
比getIfPresent
更好。
有時我們需要手動使一些快取的值失效:
cache.invalidate(key);
dataObject = cache.getIfPresent(key);
assertNull(dataObject);
3.2、同步載入
這種載入快取的方法需要一個Function
,用於初始化寫入值,類似於手動寫入策略的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
方法的底層後端初始化Function
中讀取到,這樣就可以使用快取作為訪問值的主要入口了。
3.3、非同步載入
此策略的工作原理與前一個相同,但是會非同步執行操作並返回一個CompletableFuture
來儲存實際的值:
AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(k -> DataObject.get("Data for " + k));
我們可以以相同的方式使用get
和getAll
方法,考慮到它們的返回是CompletableFuture
:
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,您可以在本文中閱讀更多相關資訊。
4、快取值的清理
Caffeine有三種快取值的清理策略:基於大小、基於時間和基於引用。
4.1、基於大小的清理
這種型別的清理設計為在超出快取配置的大小限制時發生清理。有兩種獲取大小的方法——計算快取中的物件數,或者獲取它們的權重。
讓我們看看如何計算快取中的物件數。快取初始化時,其大小為零:
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
的Function來定義快取大小的獲取:
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());
4.2、基於時間的清理
這種清理策略基於條目的過期時間,分為三種:
- 訪問後過期——自上次讀取或寫入以來,條目在經過某段時間後過期
- 寫入後過期——自上次寫入以來,條目在經過某段時間後過期
- 自定義策略——由
Expiry
的實現來為每個條目單獨計算到期時間
讓我們使用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));
4.3、基於引用的清理
我們可以配置我們的快取,允許快取的鍵或值或二者一起的垃圾收集。為此,我們需要為鍵和值配置WeakReference
的使用,並且我們可以配置SoftReference
僅用於值的垃圾收集。
WeakReference
的使用允許在沒有對物件的任何強引用時對物件進行垃圾回收。SoftReference
允許基於JVM的全域性LRU(最近最少使用)策略對物件進行垃圾回收。可以在此處找到有關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));
5、快取重新整理
可以將快取配置為在定義的時間段後自動重新整理條目。讓我們看看如何使用refreshAfterWrite方法做到這一點:
Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
在這裡,我們應該明白expireAfter和refreshAfter的一個區別:當請求過期條目時,執行會阻塞,直到build函式計算出新值。但是如果該條目符合重新整理條件,則快取將返回一箇舊值並非同步重新載入該值。
6、統計
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());
我們還可以建立一個StatsCounter
的實現作為引數來傳入recordStats
。每次與統計相關的更改,這個實現物件都將被呼叫。
7、結論
在本文中,我們熟悉了Java的Caffeine快取庫。我們看到了如何配置和存入快取,以及如何根據需要選擇合適的過期或重新整理策略。
原文:https://www.baeldung.com/java-caching-caffeine
翻譯:碼農熊貓
更多技術乾貨,請訪問我的個人網站https://pinmost.com,或關注公眾號【碼農熊貓】