1. 序章

この記事では、 Caffeine —Java用の高性能キャッシングライブラリを見ていきます。

キャッシュとMapの基本的な違いの1つは、キャッシュが保存されたアイテムを削除することです。

エビクションポリシーは、いつでもどのオブジェクトを削除するかを決定します。 このポリシーは、キャッシュのヒット率に直接影響します。これは、キャッシュライブラリの重要な特性です。

Caffeineは、 Window TinyLfu エビクションポリシーを使用します。これにより、ほぼ最適なヒット率が提供されます。

2. 依存

caffeine依存関係をpom.xmlに追加する必要があります。

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.5.5</version>
</dependency>

カフェインの最新バージョンはMavenCentralで見つけることができます。

3. キャッシュへの入力

Caffeineのキャッシュポピュレーションの3つの戦略、手動、同期読み込み、非同期読み込みに焦点を当てましょう。

まず、キャッシュに格納する値のタイプのクラスを作成しましょう。

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 メソッドは、計算をアトミックに実行します。 これは、複数のスレッドが同時に値を要求した場合でも、計算が1回だけ行われることを意味します。 そのため、getIfPresentよりもgetを使用する方が望ましいです。

場合によっては、キャッシュされた値を手動で無効にする必要があります。

cache.invalidate(key);
dataObject = cache.getIfPresent(key);

assertNull(dataObject);

3.2. 同期読み込み

キャッシュをロードするこの方法は、手動戦略の 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));

CompleteableFuture を返すという事実を考慮に入れて、同じ方法で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()));

CompleteableFuture には豊富で便利なAPIがあり、この記事の詳細を読むことができます。

4. 価値の排除

カフェインには、価値の排除のための3つの戦略があります。サイズベース、時間ベース、および参照ベースです。

4.1. サイズベースのエビクション

このタイプのエビクションは、キャッシュの構成されたサイズ制限を超えたときにエビクションが発生することを前提としています。 サイズを取得する2つの方法があります-キャッシュ内のオブジェクトをカウントするか、それらの重みを取得します。

キャッシュ内のオブジェクトをカウントする方法を見てみましょう。 キャッシュが初期化されると、そのサイズはゼロに等しくなります。

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());

2番目の値をキャッシュに追加できます。これにより、最初の値が削除されます。

cache.get("B");
cache.cleanUp();

assertEquals(1, cache.estimatedSize());

キャッシュサイズを取得する前にcleanUpメソッドを呼び出すことは言及する価値があります。 これは、キャッシュエビクションが非同期で実行され、このメソッドがエビクションの完了を待つのに役立つためです。

計量器関数を渡して、キャッシュのサイズを取得することもできます。

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. 時間ベースのエビクション

この排除戦略は、エントリの有効期限に基づくであり、次の3つのタイプがあります。

  • アクセス後に期限切れ—最後の読み取りまたは書き込みが発生してから期間が経過するとエントリが期限切れになります
  • 書き込み後に期限切れ—最後の書き込みが発生してから期間が経過するとエントリが期限切れになります
  • カスタムポリシー—有効期限は、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. 参照ベースのエビクション

ガベージコレクションのキャッシュキーや値を許可するようにキャッシュを構成できます。 これを行うには、キーと値の両方に対して WeakRefence の使用を構成し、値のガベージコレクションに対してのみSoftReferenceを構成できます。

WeakRefence の使用法では、オブジェクトへの強力な参照がない場合に、オブジェクトのガベージコレクションが可能です。 SoftReference を使用すると、JVMのグローバルなLeast-Recently-Used戦略に基づいてオブジェクトをガベージコレクションできます。 Javaでの参照の詳細については、ここを参照してください。

Cafeine.weakKeys() Cafeine.weakValues()、 Cafeine.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の違いを理解する必要があります。 期限切れのエントリが要求されると、ビルド関数によって新しい値が計算されるまで実行がブロックされます。

ただし、エントリが更新の対象である場合、キャッシュは古い値を返し、値を非同期にリロードします

6. 統計学

カフェインには、キャッシュ使用量に関する統計を記録する手段があります。

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 の実装を作成するサプライヤー StatsCounter。 このオブジェクトは、統計関連の変更が行われるたびにプッシュされます。

7. 結論

この記事では、Java用のCaffeineキャッシングライブラリについて理解しました。 キャッシュを構成して設定する方法と、必要に応じて適切な有効期限または更新ポリシーを選択する方法を確認しました。

ここに示されているソースコードは、Githubから入手できます。