カフェイン入門

1前書き

この記事では、https://github.com/ben-manes/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>

あなたは caffeine の最新バージョンを見つけることができます%22(Maven Central上)

3キャッシュへのデータ投入

カフェインの キャッシュ作成のための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);
  • 引数としてキーとともに Function を取る 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. 同期ロード

このキャッシュのロード方法は、手動戦略の get メソッドと同様に、値の初期化に使用される Function、 を取ります。

それがどのように使えるのか見てみましょう。

まず最初に、キャッシュを初期化する必要があります。

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

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があり、link:/java-completablefutureについて読むことができます。

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 メソッドを呼び出すこと は言及する価値があります。これは、キャッシュの追い出しが非同期的に実行されるためです。このメソッドは、追い出しの完了を待つのに役立ちます。

キャッシュのサイズを取得するために 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. 時間ベースの立ち退き

この立ち退き戦略は、エントリの有効期限に基づいており、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での参照に関する詳細は ここ を参照してください。

各オプションを有効にするには、 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 ** の違いを理解する必要があります。期限切れのエントリが要求されると、ビルド Function によって新しい値が計算されるまで実行がブロックされます。

しかし、そのエントリがリフレッシュに適している場合は、キャッシュは古い値を返し、 値を非同期に 再ロードします。

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://github.com/eugenp/tutorials/tree/master/libraries[over Github]から入手できます。