カフェイン入門

カフェインの紹介

1. 前書き

この記事では、Caffeinehigh-performance caching library for Javaについて見ていきます。

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

任意の時点でのeviction policy decides which objects should be deleted。 このポリシーdirectly affects the cache’s hit rate —キャッシュライブラリの重要な特性。

カフェインは、near-optimal hit rateを提供するWindow TinyLfuエビクションポリシーを使用します。

2. 依存

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


    com.github.ben-manes.caffeine
    caffeine
    2.5.5

caffeineon Maven Centralの最新バージョンを見つけることができます。

3. キャッシュへの入力

カフェインのthree strategies for cache populationに焦点を当てましょう:手動、同期読み込み、非同期読み込み。

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

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 cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .maximumSize(100)
  .build();

さて、we can get some value from the cache using the getIfPresent method。 値がキャッシュに存在しない場合、このメソッドはnullを返します。

String key = "A";
DataObject dataObject = cache.getIfPresent(key);

assertNull(dataObject);

putメソッドを使用して手動でpopulate the cacheを実行できます。

cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);

assertNotNull(dataObject);

We can also get the value using the get method。引数としてキーとともにFunctionを取ります。 キーがキャッシュに存在しない場合、この関数はフォールバック値を提供するために使用されます。キーは計算後にキャッシュに挿入されます。

dataObject = cache
  .get(key, k -> DataObject.get("Data for A"));

assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());

getメソッドは、計算をアトミックに実行します。 これは、複数のスレッドが同時に値を要求した場合でも、計算が1回だけ行われることを意味します。 そのため、using get is preferable to getIfPresentが使用されます。

手動でinvalidate some cached valuesする必要がある場合があります。

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

assertNull(dataObject);

3.2. 同期読み込み

キャッシュをロードするこの方法は、手動戦略のget方法と同様に、値の初期化に使用されるFunction,を取ります。 それをどのように使用できるか見てみましょう。

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

LoadingCache 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 dataObjectMap
  = cache.getAll(Arrays.asList("A", "B", "C"));

assertEquals(3, dataObjectMap.size());

値は、buildメソッドに渡された基になるバックエンド初期化Functionから取得されます。 This makes it possible to use the cache as the main facade for accessing values.

3.3. 非同期読み込み

この戦略works the same as the previous but performs operations asynchronously and returns a CompletableFutureは、実際の値を保持します。

AsyncLoadingCache cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .buildAsync(k -> DataObject.get("Data for " + k));

同じ方法で、CompletableFutureを返すという事実を考慮してuse the get and getAll methodsを実行できます。

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があり、in this articleについて詳しく読むことができます。

4. 価値観の立ち退き

カフェインにはthree strategies for value evictionがあります:サイズベース、時間ベース、および参照ベース。

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

このタイプのエビクションは、eviction occurs when the configured size limit of the cache is exceededを想定しています。 two ways of getting the sizeがあります—キャッシュ内のオブジェクトをカウントするか、それらの重みを取得します。

count objects in the cacheを実行する方法を見てみましょう。 キャッシュが初期化されると、そのサイズはゼロに等しくなります。

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

call the cleanUp method before getting the cache sizeであることは言及する価値があります。 これは、キャッシュエビクションが非同期で実行され、このメソッドがhelps to await the completion of the evictionであるためです。

pass a weigherFunctionを使用して、キャッシュのサイズを取得することもできます。

LoadingCache 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. 時間ベースの立ち退き

このエビクション戦略はbased on the expiration time of the entryであり、次の3つのタイプがあります。

  • Expire after access —最後の読み取りまたは書き込みが発生してから期間が経過するとエントリの有効期限が切れます

  • Expire after write —最後の書き込みが発生してから期間が経過するとエントリの有効期限が切れます

  • Custom policy —有効期限は、Expiryの実装によってエントリごとに個別に計算されます

expireAfterAccessメソッドを使用して、アクセス後の有効期限戦略を構成しましょう。

LoadingCache 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() {
    @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. 参照ベースのエビクション

garbage-collection of cache keys and/or valuesを許可するようにキャッシュを構成できます。 これを行うには、キーと値の両方にWeakRefenceの使用法を構成し、値のガベージコレクションにのみSoftReferenceを構成できます。

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

各オプションを有効にするには、Caffeine.weakKeys()Caffeine.weakValues(),、およびCaffeine.softValues()を使用する必要があります。

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

ここで、difference between expireAfter and refreshAfterを理解する必要があります。 期限切れのエントリが要求されると、ビルドFunctionによって新しい値が計算されるまで実行がブロックされます。

ただし、エントリが更新の対象である場合、キャッシュは古い値とasynchronously reload the valueを返します。

6. 統計

カフェインにはrecording statistics about cache usageの平均があります:

LoadingCache 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キャッシングライブラリに精通しました。 キャッシュを構成および設定する方法、および必要に応じて適切な有効期限または更新ポリシーを選択する方法を見ました。

ここに示されているソースコードは利用可能なover on Githubです。