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]から入手できます。