自定義物件池在 Caffeine 框架中實踐

FunTester發表於2024-04-11

前文講到自定義物件池的實現,通常來說都是獲取到物件,使用完之後要主動歸還物件。但是在某些場景下,並不能輕易在程式碼中呼叫 returnObject 方法歸還。此時就需要 Case by Case 具體情況具體分析,解決了。

今天分享一下自定義物件池在本地高效能快取框架 Caffeine 中的使用。從物件池中借出的物件會存放在 Caffeine 快取當中,然後就需要依賴 Caffeine 自己的過期和資源回收策略,決定何時回收物件。

Caffeine 框架提供了一個 API 用於處理物件過期或者被淘汰時業務邏輯,就是 RemovalListenerRemovalListener 可以監視快取中的條目移除,並在移除時執行自定義的邏輯。

下面是使用案例:

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 的回收策略,有三種:

  1. 定時清理:在固定的時間間隔內執行清理操作。
  2. 延遲清理:在快取中的條目過期後一定時間內執行清理操作。
  3. 手動清理:在特定的事件觸發時手動執行清理操作。

我們需要手動指定 Caffeine 回收策略,也就是排程器 schedule 。它負責在快取中管理定期清理過期條目的操作。Caffeine 允許你自定義排程器的實現,以便更靈活地控制快取的行為。

在 Caffeine 中,有四種不同型別的排程器(Scheduler)可供選擇,它們分別是:SystemSchedulerGuardedSchedulerDisabledSchedulerExecutorServiceScheduler。下面我將解釋它們之間的差異:

  1. SystemScheduler
    • SystemScheduler是 Caffeine 的預設排程器。
    • 它使用系統級的排程機制來執行定期清理任務。
    • 這種排程器適用於大多數場景,並且通常表現良好。
  2. GuardedScheduler
    • GuardedScheduler是一個具有保護機制的排程器。
    • 它可以確保任務不會重複執行,即使排程器被多次觸發。
    • 這種排程器適用於需要保證任務不會被重複執行的場景。
  3. DisabledScheduler
    • DisabledScheduler是一個禁用排程器,它不會執行任何清理任務。
    • 當你不希望快取自動執行清理操作時,可以使用這個排程器。
    • 這種排程器適用於不需要自動清理的場景,或者你希望手動控制清理的時機。
  4. 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 自動化
  • 測試理論雞湯
  • 社群風采&影片合集
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章