前文講到自定義物件池的實現,通常來說都是獲取到物件,使用完之後要主動歸還物件。但是在某些場景下,並不能輕易在程式碼中呼叫 returnObject
方法歸還。此時就需要 Case by Case
具體情況具體分析,解決了。
今天分享一下自定義物件池在本地高效能快取框架 Caffeine
中的使用。從物件池中借出的物件會存放在 Caffeine
快取當中,然後就需要依賴 Caffeine
自己的過期和資源回收策略,決定何時回收物件。
Caffeine
框架提供了一個 API
用於處理物件過期或者被淘汰時業務邏輯,就是 RemovalListener
。RemovalListener
可以監視快取中的條目移除,並在移除時執行自定義的邏輯。
下面是使用案例:
public static void main(String[] args) {
// 建立一個帶有RemovalListener的快取
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.removalListener(new MyRemovalListener())
.build();
// 將鍵值對放入快取
cache.put("tester1", "FunTester001");
cache.put("tester2", "FunTester002");
// 從快取中移除一個條目
cache.invalidate("key1");
}
// 自定義的RemovalListener實現
static class MyRemovalListener implements RemovalListener<String, String> {
@Override
public void onRemoval(String key, String value, RemovalCause cause) {
System.out.println("Key: " + key + ", Value: " + value + " has been removed from the cache.");
}
}
但是在我在業務中使用下面的程式碼時,卻發生意外的情況。
Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build()
當物件過期以後,並沒有出發 RemovalListener
執行。所以我查詢官方資源,Caffeine
的回收策略,有三種:
- 定時清理:在固定的時間間隔內執行清理操作。
- 延遲清理:在快取中的條目過期後一定時間內執行清理操作。
- 手動清理:在特定的事件觸發時手動執行清理操作。
我們需要手動指定 Caffeine
回收策略,也就是排程器 schedule
。它負責在快取中管理定期清理過期條目的操作。Caffeine 允許你自定義排程器的實現,以便更靈活地控制快取的行為。
在 Caffeine 中,有四種不同型別的排程器(Scheduler)可供選擇,它們分別是:SystemScheduler
、GuardedScheduler
、DisabledScheduler
和ExecutorServiceScheduler
。下面我將解釋它們之間的差異:
-
SystemScheduler:
-
SystemScheduler
是 Caffeine 的預設排程器。 - 它使用系統級的排程機制來執行定期清理任務。
- 這種排程器適用於大多數場景,並且通常表現良好。
-
-
GuardedScheduler:
-
GuardedScheduler
是一個具有保護機制的排程器。 - 它可以確保任務不會重複執行,即使排程器被多次觸發。
- 這種排程器適用於需要保證任務不會被重複執行的場景。
-
-
DisabledScheduler:
-
DisabledScheduler
是一個禁用排程器,它不會執行任何清理任務。 - 當你不希望快取自動執行清理操作時,可以使用這個排程器。
- 這種排程器適用於不需要自動清理的場景,或者你希望手動控制清理的時機。
-
-
ExecutorServiceScheduler:
-
ExecutorServiceScheduler
是一個基於ScheduledExecutorService
的排程器。 - 它使用
ScheduledExecutorService
來執行定期清理任務。 - 這種排程器適用於需要更精細控制清理任務的執行方式,比如使用自定義的執行緒池、調整執行頻率等。
-
這些排程器提供了不同的選擇,以滿足不同的需求和場景。你可以根據自己的需求來選擇適合的排程器型別,以確保快取的清理操作能夠按照預期的方式執行。
那麼問題來了,Caffeine
預設的排程器是那個呢?為什麼沒有及時回收掉過期的資源呢?
在 Caffeine
原始碼中,我得到了答案,位於 com.github.benmanes.caffeine.cache.Caffeine#getScheduler
,內容如下:
Scheduler getScheduler() {
if (this.scheduler != null && this.scheduler != Scheduler.disabledScheduler()) {
return this.scheduler == Scheduler.systemScheduler() ? this.scheduler : Scheduler.guardedScheduler(this.scheduler);
} else {
return Scheduler.disabledScheduler();
}
}
預設的是 disabledScheduler()
,顧名思義,看起來是關閉了排程器的功能。下一步我們看看實際程式碼 com.github.benmanes.caffeine.cache.DisabledScheduler
內容:
package com.github.benmanes.caffeine.cache;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
enum DisabledScheduler implements Scheduler {
INSTANCE;
private DisabledScheduler() {
}
public Future<Void> schedule(Executor executor, Runnable command, long delay, TimeUnit unit) {
Objects.requireNonNull(executor);
Objects.requireNonNull(command);
Objects.requireNonNull(unit);
return DisabledFuture.INSTANCE;
}
}
我們看看 schedule()
方法,除了驗證了一下引數意外,好像什麼都沒做啊,事實上就是什麼都沒做。實際上依靠 Caffeine
的一些驅逐策略完成的回收,也就是當我們訪問過期資源、手動置無效、手動呼叫清理資源方法才能觸發。
作為對比,我們看一下另外一個排程器的實現 com.github.benmanes.caffeine.cache.SystemScheduler
:
package com.github.benmanes.caffeine.cache;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
enum SystemScheduler implements Scheduler {
INSTANCE;
private SystemScheduler() {
}
public Future<?> schedule(Executor executor, Runnable command, long delay, TimeUnit unit) {
Executor delayedExecutor = CompletableFuture.delayedExecutor(delay, unit, executor);
return CompletableFuture.runAsync(command, delayedExecutor);
}
}
看到是使用了執行緒池實現非同步功能,其中涉及到了一個具有延遲涉及的執行緒池,繞的有點遠了,這裡不再詳細說明。
解決辦法也有了,就是設定有用的排程器,我選擇了 com.github.benmanes.caffeine.cache.SystemScheduler
,實測也是足夠滿足需求的。程式碼如下:
static void main(String[] args) {
def pool = new FunPool<String>(new FunPooledFactory<String>() {// 建立物件池
@Override
String newInstance() {
return "FunTester" + getRandomInt(Integer.MAX_VALUE)// 建立物件
}
})
def listener = new RemovalListener<String, String>() {
@Override
void onRemoval(String key, String value, RemovalCause removalCause) {
output("Key: $key , Value: $value cause: $removalCause")// 列印移除原因
pool.back(value)// 回收物件
}
}
def build = Caffeine<String, String>.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS)// 設定寫入後過期時間
.maximumSize(100) // 設定快取的最大容量
.removalListener(listener) // 設定快取移除監聽器
.scheduler(Scheduler.systemScheduler())
.build()
build.put("tester", pool.borrow())// 放入快取
sleep(2.0)// 等待快取過期
}
控制檯輸出:
12:29:15:054 ForkJoinPool.commonPool-worker-2 Key: tester , Value: FunTester208146637 cause: EXPIRED
成功觸發 RemovalListener
,且將物件歸還給物件池。
- 2021 年原創合集
- 2022 年原創合集
- 2023 年原創合集
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go、Python
- 單元&白盒&工具合集
- 測試方案&BUG&爬蟲&UI 自動化
- 測試理論雞湯
- 社群風采&影片合集