アプリケーションデータのキャッシング
このガイドでは、QuarkusアプリケーションのCDI管理されたBeanでアプリケーションデータのキャッシングを有効にする方法について説明します。
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 17+がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.9.9
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
シナリオ
Quarkusアプリケーションで、ユーザーが今後3日間の天気予報を取得できるREST APIを公開したいとします。問題は、一度に1日分のリクエストしか受け付けず、応答に時間がかかる外部の気象サービスに依存しなければならないことです。天気予報は12時間に一度更新されるので、サービスのレスポンスをキャッシュすればAPIのパフォーマンスは間違いなく向上します。
これをQuarkusの単一のアノテーションを使用して行います。
In this guide, we use the default Quarkus Cache backend (Caffeine). You can use Infinispan or Redis instead. Refer to the Infinispan cache backend reference to configure the Infinispan backend. Refer to the Redis cache backend reference to configure the Redis backend. |
ソリューション
次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。
Gitレポジトリをクローン git clone https://github.com/quarkusio/quarkus-quickstarts.git
するか、 アーカイブ をダウンロードします。
ソリューションは cache-quickstart
ディレクトリ にあります。
Mavenプロジェクトの作成
まず、以下のコマンドで新しいQuarkusプロジェクトを作成します。
Windowsユーザーの場合:
-
cmdを使用する場合、(バックスラッシュ
\
を使用せず、すべてを同じ行に書かないでください)。 -
Powershellを使用する場合は、
-D
パラメータを二重引用符で囲んでください。例:"-DprojectArtifactId=cache-quickstart"
このコマンドはプロジェクトを生成し、 cache
と rest-jackson
エクステンションモジュールをインポートします。
すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに cache
エクステンションを追加することができます。
quarkus extension add cache
./mvnw quarkus:add-extension -Dextensions='cache'
./gradlew addExtension --extensions='cache'
これにより、ビルドファイルに以下の内容が追加されます。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
</dependency>
implementation("io.quarkus:quarkus-cache")
REST APIの作成
まずは、外部気象サービスへの非常に遅い呼び出しをシミュレートするサービスを作成してみましょう。以下の内容で src/main/java/org/acme/cache/WeatherForecastService.java
を作成します。
package org.acme.cache;
import java.time.LocalDate;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class WeatherForecastService {
public String getDailyForecast(LocalDate date, String city) {
try {
Thread.sleep(2000L); (1)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return date.getDayOfWeek() + " will be " + getDailyResult(date.getDayOfMonth() % 4) + " in " + city;
}
private String getDailyResult(int dayOfMonthModuloFour) {
switch (dayOfMonthModuloFour) {
case 0:
return "sunny";
case 1:
return "cloudy";
case 2:
return "chilly";
case 3:
return "rainy";
default:
throw new IllegalArgumentException();
}
}
}
1 | 遅さはここに由来します。 |
また、ユーザーが次の3日間の天気予報を求めたときに送信されるレスポンスを含むクラスも必要です。 src/main/java/org/acme/cache/WeatherForecast.java
をこのように作成します:
package org.acme.cache;
import java.util.List;
public class WeatherForecast {
private List<String> dailyForecasts;
private long executionTimeInMs;
public WeatherForecast(List<String> dailyForecasts, long executionTimeInMs) {
this.dailyForecasts = dailyForecasts;
this.executionTimeInMs = executionTimeInMs;
}
public List<String> getDailyForecasts() {
return dailyForecasts;
}
public long getExecutionTimeInMs() {
return executionTimeInMs;
}
}
あとは、RESTリソースを作成するだけです。 この内容で src/main/java/org/acme/cache/WeatherForecastResource.java
ファイルを作成します。
package org.acme.cache;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestQuery;
@Path("/weather")
public class WeatherForecastResource {
@Inject
WeatherForecastService service;
@GET
public WeatherForecast getForecast(@RestQuery String city, @RestQuery long daysInFuture) { (1)
long executionStart = System.currentTimeMillis();
List<String> dailyForecasts = Arrays.asList(
service.getDailyForecast(LocalDate.now().plusDays(daysInFuture), city),
service.getDailyForecast(LocalDate.now().plusDays(daysInFuture + 1L), city),
service.getDailyForecast(LocalDate.now().plusDays(daysInFuture + 2L), city));
long executionEnd = System.currentTimeMillis();
return new WeatherForecast(dailyForecasts, executionEnd - executionStart);
}
}
1 | daysInFuture クエリパラメーターが省略された場合、3 日間の天気予報は現在の日から始まります。それ以外の場合は、現在の日に daysInFuture の値を加えたものから始まります。 |
完了です!うまくいっているか確認してみましょう。
まず、プロジェクトディレクトリからDevモードでアプリケーションを実行します。
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
次に、ブラウザから http://localhost:8080/weather?city=Raleigh
を呼び出します。6秒ほど長い時間が経過すると、アプリケーションはこのような回答をします:
{"dailyForecasts":["MONDAY will be cloudy in Raleigh","TUESDAY will be chilly in Raleigh","WEDNESDAY will be rainy in Raleigh"],"executionTimeInMs":6001}
コードを実行する日によってレスポンスの内容が異なる場合があります。 |
何度同じURLを呼び出してみても、必ず6秒でレスポンスが返却されます。
キャッシュの有効化
Quarkusアプリケーションが動いたので、外部の気象サービスのレスポンスをキャッシュすることで、レスポンスタイムを大幅に改善してみましょう。 WeatherForecastService
クラスを次のように修正します。
package org.acme.cache;
import java.time.LocalDate;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.cache.CacheResult;
@ApplicationScoped
public class WeatherForecastService {
@CacheResult(cacheName = "weather-cache") (1)
public String getDailyForecast(LocalDate date, String city) {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return date.getDayOfWeek() + " will be " + getDailyResult(date.getDayOfMonth() % 4) + " in " + city;
}
private String getDailyResult(int dayOfMonthModuloFour) {
switch (dayOfMonthModuloFour) {
case 0:
return "sunny";
case 1:
return "cloudy";
case 2:
return "chilly";
case 3:
return "rainy";
default:
throw new IllegalArgumentException();
}
}
}
1 | このアノテーション(もちろん関連するインポートも)を追加しただけです。 |
http://localhost:8080/weather?city=Raleigh
をもう一度呼び出して確認してみてください。返事が来るまでにまだ長い時間待たされています。これはサーバーが再起動したばかりでキャッシュが空になっているので正常です。
ちょっと待って!? WeatherForecastService
のアップデート後、サーバーが勝手に再起動した?はい、これは、 live coding
と呼ばれる開発者のためのQuarkusの驚くべき機能の一つです。
前回の呼び出しでキャッシュが読み込まれたので、同じ URL を呼び出してみてください。今度は、 executionTimeInMs
の値が 0 に近い超高速な応答が返ってくるはずです。
URL http://localhost:8080/weather?city=Raleigh&daysInFuture=1
を使って未来のある日から始めるとどうなるか見てみましょう。要求された日のうち2つはすでにキャッシュに読み込まれていたので、2秒後に回答が得られるはずです。
また、同じURLを別の都市で呼び出してみて、再度キャッシュの動作を確認することもできます。最初の呼び出しには6秒ほどかかり、次の呼び出しにはすぐに出ます。
おめでとうございます!たった1行のコードでQuarkusアプリケーションにアプリケーションデータのキャッシングを追加しました!
Quarkusアプリケーションのデータキャッシング機能について詳しく知りたいですか?以下のセクションでは、この機能について知っておくべきことをすべて紹介します。
アノテーションを利用したキャッシング
Quarkusは、CDI管理されたBeanで使用できる、キャッシュ機能を有効にするアノテーションのセットを提供します。
プライベートメソッドではアノテーションのキャッシュは許可されていません。package-private (明示的な修飾子を持たない) を含む他のアクセス修飾子では問題なく動作します。 |
@CacheResult
可能な限り、メソッドボディを実行せずにキャッシュからメソッドの結果を読み込みます。
@CacheResult
でアノテーションされたメソッドが呼び出されると、Quarkusはキャッシュキーを計算し、それを使用して、そのメソッドがすでに呼び出されているかどうかをキャッシュでチェックします。キャッシュキーの計算方法については、このガイドの キャッシュキー構築ロジック のセクションを参照してください。 キャッシュ内に値が見つかった場合、その値が返され、アノテーションされたメソッドが実際に実行されることはありません。 値が見つからない場合、アノテーションされたメソッドが呼び出され、返された値は計算されたキーを使用してキャッシュに格納されます。
CacheResult
でアノテーションされたメソッドは、キャッシュミスのロックメカニズムによって保護されています。複数の同時呼び出しが同じ欠落キーからキャッシュ値を取得しようとした場合、メソッドは一度だけ呼び出されます。最初の同時呼び出しはメソッドの呼び出しをトリガし、その後の同時呼び出しはキャッシュされた結果を取得するためにメソッドの呼び出しの終了を待ちます。 lockTimeout
パラメーターを使用すると、所定の遅延後にロックを中断することができます。ロックのタイムアウトは既定では無効になっており、ロックが中断されることはありません。詳細は、パラメーター Javadoc を参照してください。
このアノテーションは、 void
を返すメソッドでは使用できません。
Quarkusは、基礎となるCaffeineプロバイダとは異なり、 |
@CacheInvalidate
キャッシュからエントリーを削除します。
@CacheInvalidate
のアノテーションが付いたメソッドが呼び出されると、Quarkus はキャッシュキーを計算し、それを使ってキャッシュから既存のエントリを削除しようとします。 キャッシュキーの計算方法については、このガイドの キャッシュキー構築ロジック のセクションを参照してください。 もしキーがどのキャッシュエントリも特定できない場合は、何も起こりません。
@CacheKey
メソッドの引数が @CacheKey
でアノテーションされている場合、 @CacheResult
または @CacheInvalidate
でアノテーションされたメソッドの呼び出し時にキャッシュキーの一部として識別されます。
このアノテーションはオプションで、メソッドの引数の一部がキャッシュキーの一部ではない場合にのみ使用されるべきです。
キャッシュキー構築ロジック
キャッシュキーはアノテーションAPIにより、以下のロジックで構築されます。
-
io.quarkus.cache.CacheKeyGenerator
が@CacheResult
または@CacheInvalidate
アノテーションで宣言されている場合、キャッシュ・キーの生成に使用されます。いくつかのメソッド引数の存在する可能性のある@CacheKey
アノテーションは無視されます。 -
このメソッドに引数がなければ、キャッシュキーはキャッシュ名から作成される
io.quarkus.cache.DefaultCacheKey
のインスタンスとなります。 -
このメソッドが正確に1つの引数を持つ場合、この引数はキャッシュキーとなります。
-
メソッドに複数の引数があり、
@CacheKey
でアノテーションされたものが1つだけある場合、このアノテーションされた引数がキャッシュキーとなります。 -
メソッドが
@CacheKey
でアノテーションされた複数の引数を持つ場合、キャッシュキーはこれらのアノテーションされた引数から構築されるio.quarkus.cache.CompositeCacheKey
のインスタンスになります。 -
メソッドが複数の引数を持ち、そのどれにも
@CacheKey
でアノテーションされていない場合、キャッシュキーはすべてのメソッド引数から構築されるio.quarkus.cache.CompositeCacheKey
のインスタンスになります。
キーの一部である非プリミティブメソッドの各引数は、キャッシュが期待通りに動作するために、 |
キャッシュキーが複数のメソッド引数から構築される場合、それらが明示的に @CacheKey
で識別されているかどうかに関わらず、構築ロジックはメソッドシグネチャ内のこれらの引数の順序に依存します。一方、引数名は全く使用されず、キャッシュキーには何の影響も与えません。
package org.acme.cache;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheResult;
@ApplicationScoped
public class CachedService {
@CacheResult(cacheName = "foo")
public Object load(String keyElement1, Integer keyElement2) {
// Call expensive service here.
}
@CacheInvalidate(cacheName = "foo")
public void invalidate1(String keyElement2, Integer keyElement1) { (1)
}
@CacheInvalidate(cacheName = "foo")
public void invalidate2(Integer keyElement2, String keyElement1) { (2)
}
@CacheInvalidate(cacheName = "foo")
public void invalidate3(Object notPartOfTheKey, @CacheKey String keyElement1, @CacheKey Integer keyElement2) { (3)
}
@CacheInvalidate(cacheName = "foo")
public void invalidate4(Object notPartOfTheKey, @CacheKey Integer keyElement2, @CacheKey String keyElement1) { (4)
}
}
1 | このメソッドを呼び出すと、キー要素名が入れ替わっていても load メソッドでキャッシュされた値が無効になります。 |
2 | このメソッドを呼び出すと、キー要素の順序が異なるため、 load メソッドでキャッシュされた値が無効になることはありません。 |
3 | このメソッドを呼び出すと、キー要素の順序が同じなので、 load メソッドでキャッシュされた値が無効になります。 |
4 | このメソッドを呼び出すと、キー要素の順序が異なるため、 load メソッドでキャッシュされた値が無効になることはありません。 |
CacheKeyGenerator
でキャッシュキーを生成する
メソッドの引数以外もキャッシュ・キーに含めたい場合があります。これは、 io.quarkus.cache.CacheKeyGenerator
インターフェイスを実装し、その実装を @CacheResult
または @CacheInvalidate
アノテーションの keyGenerator
フィールドで宣言することで実現できます。
このクラスは、CDI Bean を表すか、public no-args コンストラクタを宣言する必要があります。 それが CDI Bean を表している場合、キー・ジェネレータはキャッシュ・キーの計算中に注入されます。 そうでない場合は、キャッシュ・キーの計算ごとに、その既定のコンストラクタを使用してキー・ジェネレータの新しいインスタンスが作成されます。
CDIの場合,そのBean型の集合にそのクラスをもつBeanが正確に一つ存在しなければならず,そうでない場合,ビルドは失敗します。
Beanのスコープに関連付けられたコンテキストは, CacheKeyGenerator#generate()
メソッドが呼び出されるとき,有効でなければなりません。
スコープが @Dependent
の場合, CacheKeyGenerator#generate()
メソッドが完了すると,Bean インスタンスは破棄されます。
以下のキージェネレータはCDI Beanとして注入されます。
package org.acme.cache;
import java.lang.reflect.Method;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.quarkus.cache.CacheKeyGenerator;
import io.quarkus.cache.CompositeCacheKey;
@ApplicationScoped
public class ApplicationScopedKeyGen implements CacheKeyGenerator {
@Inject
AnythingYouNeedHere anythingYouNeedHere; (1)
@Override
public Object generate(Method method, Object... methodParams) { (2)
return new CompositeCacheKey(anythingYouNeedHere.getData(), methodParams[1]); (3)
}
}
1 | キージェネレータにCDI Beanをインジェクトすることで、外部データをキャッシュキーに含めることができます。 |
2 | Method を使っている間、メソッドのいくつかは高価になることがあるため注意してください。 |
3 | インデックスからアクセスする前に、このメソッドが十分な引数を持っていることを確認してください。そうでないと、キャッシュキーの計算中に IndexOutOfBoundsException が投げられるかもしれません。 |
以下のキージェネレータは、デフォルトのコンストラクタを使用してインスタンス化されます。
package org.acme.cache;
import java.lang.reflect.Method;
import io.quarkus.cache.CacheKeyGenerator;
import io.quarkus.cache.CompositeCacheKey;
public class NotABeanKeyGen implements CacheKeyGenerator {
// CDI injections won't work here because it's not a CDI bean.
@Override
public Object generate(Method method, Object... methodParams) {
return new CompositeCacheKey(method.getName(), methodParams[0]); (1)
}
}
1 | メソッド名をキャッシュキーに含めることは、 Method の他のメソッドとは異なり、高価なものではありません。 |
どちらの種類のキージェネレーターも、同様の方法で使用することができます。
package org.acme.cache;
import jakarta.enterprise.context.ApplicationScoped;
import org.acme.cache.ApplicationScopedKeyGen;
import org.acme.cache.NotABeanKeyGen;
import io.quarkus.cache.CacheKey;
import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheResult;
@ApplicationScoped
public class CachedService {
@CacheResult(cacheName = "foo", keyGenerator = ApplicationScopedKeyGen.class) (1)
public Object load(@CacheKey Object notUsedInKey, String keyElement) { (2)
// Call expensive service here.
}
@CacheInvalidate(cacheName = "foo", keyGenerator = NotABeanKeyGen.class) (3)
public void invalidate(Object keyElement) {
}
@CacheInvalidate(cacheName = "foo", keyGenerator = NotABeanKeyGen.class)
@CacheInvalidate(cacheName = "bar")
public void invalidate(Integer param0, @CacheKey BigDecimal param1) { (4)
}
}
1 | このキージェネレーターはCDI Beanです。 |
2 | キー・ジェネレータが @CacheResult アノテーションで宣言されているため、 @CacheKey アノテーションは無視されます。 |
3 | このキージェネレーターはCDI Beanではありません。 |
4 | @CacheKey アノテーションは foo キャッシュデータが無効化されると無視されますが、bar キャッシュデータが無効化されると param1 がキャッシュキーになります。 |
プログラムAPIを使ったキャッシング
Quarkusは、アノテーションAPIを使用して宣言された任意のキャッシュから値を保存、取得、削除するために使用できるプログラムAPIも提供しています。プログラムAPIからのすべての操作はノンブロッキングで、フードの下で Mutiny に依存しています。
キャッシュされたデータにプログラムでアクセスする前に、 io.quarkus.cache.Cache
のインスタンスを取得する必要があります。以下のセクションでは、その方法をご紹介します。
@CacheName
アノテーションとともに Cache
をインジェクトする
io.quarkus.cache.CacheName
は、フィールド、コンストラクタパラメータ、またはメソッドのパラメータに使用して、 Cache
をインジェクトすることができます。
package org.acme.cache;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.smallrye.mutiny.Uni;
@ApplicationScoped
public class CachedExpensiveService {
@Inject (1)
@CacheName("my-cache")
Cache cache;
public Uni<String> getNonBlockingExpensiveValue(Object key) { (2)
return cache.get(key, k -> { (3)
/*
* Put an expensive call here.
* It will be executed only if the key is not already associated with a value in the cache.
*/
});
}
public String getBlockingExpensiveValue(Object key) {
return cache.get(key, k -> {
// Put an expensive call here.
}).await().indefinitely(); (4)
}
}
1 | これはオプションです。 |
2 | このメソッドは、ノンブロッキングである Uni<String> タイプを返します。 |
3 | k 引数には、キャッシュキーの値が含まれています。 |
4 | 呼び出しをノンブロッキングにする必要がない場合は、このようにしてブロッキングしてキャッシュ値を取得することができます。 |
CacheManager
から Cache
を取得しています。
Cache
のインスタンスを取得するもう一つの方法は、まず io.quarkus.cache.CacheManager
をインジェクトし、その名前から目的の Cache
を取得するやり方です。
package org.acme.cache;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheManager;
import java.util.Optional;
@Singleton
public class CacheClearer {
private final CacheManager cacheManager;
public CacheClearer(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public void clearCache(String cacheName) {
Optional<Cache> cache = cacheManager.getCache(cacheName);
if (cache.isPresent()) {
cache.get().invalidateAll().await().indefinitely();
}
}
}
プログラムキャッシュキーの構築
プログラムキャッシュキーを構築する前に、アノテーションメソッドが呼び出されたときにアノテーションAPIによってキャッシュキーがどのように構築されるかを知っておく必要があります。これについては、このガイドの <<cache-keys-building-logic> セクションで説明されています。
アノテーションAPIを使って保存されたキャッシュ値を、プログラムAPIを使って取得または削除したい場合、両方のAPIで同じキーが使われていることを確認する必要があります。
CaffeineCache
からすべてのキーを取得します。
特定の CaffeineCache
からのキャッシュキーは、以下に示すように、変更不可能な Set
として取得できます。セットに対するイテレーションが進行している間にキャッシュエントリが変更された場合、セットは変更されないままとなります。
package org.acme.cache;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.cache.CaffeineCache;
import java.util.Set;
@ApplicationScoped
public class CacheKeysService {
@CacheName("my-cache")
Cache cache;
public Set<Object> getAllCacheKeys() {
return cache.as(CaffeineCache.class).keySet();
}
}
CaffeineCache
にデータを追加する
CaffeineCache
にデータを追加するには CaffeineCache#put(Object, CompletableFuture)
メソッドを使います。 このメソッドは CompletableFuture
をキャッシュ内の与えられたキーと関連付けます。キャッシュに以前からそのキーに関連する値が入っていた場合、古い値はこの CompletableFuture
に置き換えられます。非同期計算が失敗した場合、そのエントリは自動的に削除されます。
package org.acme.cache;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.cache.CaffeineCache;
import java.util.concurrent.CompletableFuture;
@ApplicationScoped
public class CacheService {
@CacheName("my-cache")
Cache cache;
@PostConstruct
public void initialize() {
cache.as(CaffeineCache.class).put("foo", CompletableFuture.completedFuture("bar"));
}
}
CaffeineCache
からすべてのキーを取得する
特定の CaffeineCache
からのキャッシュ値は、以下に示すように存在する場合に取得できます。 与えられたキーがキャッシュに含まれている場合、このメソッドは指定されたキーがマッピングされた CompletableFuture
を返します。 その CompletableFuture
は計算中であるか、あるいは既に終了している可能性があります。 そうでなければ、このメソッドは null
を返します。
package org.acme.cache;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheName;
import io.quarkus.cache.CaffeineCache;
import java.util.concurrent.CompletableFuture;
@ApplicationScoped
public class CacheKeysService {
@CacheName("my-cache")
Cache cache;
public CompletableFuture<Object> getIfPresent(Object key) {
return cache.as(CaffeineCache.class).getIfPresent(key);
}
}
リアルタイムで CaffeineCache
の有効期限ポリシーや最大サイズを変更
CaffeineCache
の有効期限ポリシーは、そのポリシーが Quarkus の設定で最初に指定されている場合、Quarkus アプリの実行中に変更することができます。同様に、 CaffeineCache
の最大サイズも、設定に定義された初期最大サイズでキャッシュが構築された場合、リアルタイムで変更できます。
package org.acme.cache;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import io.quarkus.cache.Cache;
import io.quarkus.cache.CacheManager;
import io.quarkus.cache.CaffeineCache;
import java.time.Duration;
import java.util.Optional;import javax.inject.Singleton;
@Singleton
public class CacheConfigManager {
private final CacheManager cacheManager;
public CacheConfigManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public void setExpireAfterAccess(String cacheName, Duration duration) {
Optional<Cache> cache = cacheManager.getCache(cacheName);
if (cache.isPresent()) {
cache.get().as(CaffeineCache.class).setExpireAfterAccess(duration); (1)
}
}
public void setExpireAfterWrite(String cacheName, Duration duration) {
Optional<Cache> cache = cacheManager.getCache(cacheName);
if (cache.isPresent()) {
cache.get().as(CaffeineCache.class).setExpireAfterWrite(duration); (2)
}
}
public void setMaximumSize(String cacheName, long maximumSize) {
Optional<Cache> cache = cacheManager.getCache(cacheName);
if (cache.isPresent()) {
cache.get().as(CaffeineCache.class).setMaximumSize(maximumSize); (3)
}
}
}
1 | この行は、キャッシュが expire-after-access という設定値で構築された場合にのみ機能します。そうでない場合は、 IllegalStateException がスローされます。 |
2 | この行は、キャッシュが expire-after-write という設定値で構築された場合にのみ機能します。そうでない場合は、 IllegalStateException がスローされます。 |
3 | この行は、キャッシュが maximum-size という設定値で構築された場合にのみ機能します。そうでない場合は、 IllegalStateException がスローされます。 |
|
基礎となるキャッシュプロバイダの設定
このエクステンションは、基礎となるキャッシュプロバイダとして Caffeine を使用しています。Caffeine は高性能で最適に近いキャッシングライブラリです。
Caffeine設定プロパティ
Quarkusアプリケーションのデータキャッシュエクステンションをバックアップする各Caffeineキャッシュは、 application.properties
ファイルの以下のプロパティーを使用して設定することができます。デフォルトでは、設定されていない場合、キャッシュはどのようなタイプのエヴィクションも実行しません。
以下のすべてのプロパティーで |
ビルド時に固定される構成プロパティ - 他のすべての構成プロパティは実行時にオーバーライド可能
Configuration property |
型 |
デフォルト |
---|---|---|
string |
|
|
Whether or not the cache extension is enabled. Environment variable: Show more |
boolean |
|
Default configuration applied to all Caffeine caches (lowest precedence) |
型 |
デフォルト |
Minimum total size for the internal data structures. Providing a large enough estimate at construction time avoids the need for expensive resizing operations later, but setting this value unnecessarily high wastes memory. Environment variable: Show more |
int |
|
Maximum number of entries the cache may contain. Note that the cache may evict an entry before this limit is exceeded or temporarily exceed the threshold while evicting. As the cache size grows close to the maximum, the cache evicts entries that are less likely to be used again. For example, the cache may evict an entry because it hasn’t been used recently or very often. Environment variable: Show more |
長 |
|
Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, or the most recent replacement of its value. Environment variable: Show more |
||
Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, the most recent replacement of its value, or its last read. Environment variable: Show more |
||
Whether or not metrics are recorded if the application depends on the Micrometer extension. Setting this value to Environment variable: Show more |
boolean |
|
Minimum total size for the internal data structures. Providing a large enough estimate at construction time avoids the need for expensive resizing operations later, but setting this value unnecessarily high wastes memory. Environment variable: Show more |
int |
|
Maximum number of entries the cache may contain. Note that the cache may evict an entry before this limit is exceeded or temporarily exceed the threshold while evicting. As the cache size grows close to the maximum, the cache evicts entries that are less likely to be used again. For example, the cache may evict an entry because it hasn’t been used recently or very often. Environment variable: Show more |
長 |
|
Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, or the most recent replacement of its value. Environment variable: Show more |
||
Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry’s creation, the most recent replacement of its value, or its last read. Environment variable: Show more |
||
Whether or not metrics are recorded if the application depends on the Micrometer extension. Setting this value to Environment variable: Show more |
boolean |
期間フォーマットについて
To write duration values, use the standard 数字で始まる簡略化した書式を使うこともできます:
その他の場合は、簡略化されたフォーマットが解析のために
|
キャッシュの設定は以下のようになります。
quarkus.cache.caffeine."foo".initial-capacity=10 (1)
quarkus.cache.caffeine."foo".maximum-size=20
quarkus.cache.caffeine."foo".expire-after-write=60S
quarkus.cache.caffeine."bar".maximum-size=1000 (2)
1 | foo キャッシュの設定を行っています。 |
2 | bar キャッシュの設定を行っています。 |
マイクロメーターメトリクスの有効化
アノテーションキャッシング API を使って宣言された各キャッシュは、マイクロメーターのメトリクスを使ってモニターすることができます。
キャッシュ・メトリクスの収集は、アプリケーションが |
デフォルトでは、キャッシュメトリクスの収集は無効になっています。 application.properties
ファイルから有効にすることができます。
quarkus.cache.caffeine."foo".metrics-enabled=true
全ての計測手法のように、メトリクスの収集にはわずかなオーバーヘッドが伴い、アプリケーションのパフォーマンスに影響を与える可能性があります。 |
収集されたメトリクスには、以下のようなキャッシュの統計情報が含まれています。
-
キャッシュ内のエントリーのおおよその数
-
キャッシュに追加されたエントリーの数
-
ヒットとミスに関する情報を含む、キャッシュルックアップの実行回数
-
エヴィクションの回数とエヴィクションエントリの重み
以下は、 quarkus-micrometer-registry-prometheus
エクステンションに依存するアプリケーションで利用可能なキャッシュメトリクスの例です。
# HELP cache_size The number of entries in this cache. This may be an approximation, depending on the type of cache.
# TYPE cache_size gauge
cache_size{cache="foo",} 8.0
# HELP cache_puts_total The number of entries added to the cache
# TYPE cache_puts_total counter
cache_puts_total{cache="foo",} 12.0
# HELP cache_gets_total The number of times cache lookup methods have returned a cached value.
# TYPE cache_gets_total counter
cache_gets_total{cache="foo",result="hit",} 53.0
cache_gets_total{cache="foo",result="miss",} 12.0
# HELP cache_evictions_total cache evictions
# TYPE cache_evictions_total counter
cache_evictions_total{cache="foo",} 4.0
# HELP cache_eviction_weight_total The sum of weights of evicted entries. This total does not include manual invalidations.
# TYPE cache_eviction_weight_total counter
cache_eviction_weight_total{cache="foo",} 540.0
アノテーション付きBeanの例
暗黙の簡易キャッシュキー
package org.acme.cache;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheResult;
@ApplicationScoped
public class CachedService {
@CacheResult(cacheName = "foo")
public Object load(Object key) { (1)
// Call expensive service here.
}
@CacheInvalidate(cacheName = "foo")
public void invalidate(Object key) { (1)
}
@CacheInvalidateAll(cacheName = "foo")
public void invalidateAll() {
}
}
1 | @CacheKey アノテーションがないので、キャッシュキーは暗黙的です。 |
明示的な複合キャッシュキー
package org.acme.cache;
import jakarta.enterprise.context.Dependent;
import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheKey;
import io.quarkus.cache.CacheResult;
@Dependent
public class CachedService {
@CacheResult(cacheName = "foo")
public String load(@CacheKey Object keyElement1, @CacheKey Object keyElement2, Object notPartOfTheKey) { (1)
// Call expensive service here.
}
@CacheInvalidate(cacheName = "foo")
public void invalidate(@CacheKey Object keyElement1, @CacheKey Object keyElement2, Object notPartOfTheKey) { (1)
}
@CacheInvalidateAll(cacheName = "foo")
public void invalidateAll() {
}
}
1 | キャッシュキーは明示的に 2 つの要素で構成されています。メソッドシグネチャには、キーの一部ではない第三引数も含まれています。 |
デフォルトのキャッシュキー
package org.acme.cache;
import jakarta.enterprise.context.Dependent;
import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheResult;
@Dependent
public class CachedService {
@CacheResult(cacheName = "foo")
public String load() { (1)
// Call expensive service here.
}
@CacheInvalidate(cacheName = "foo")
public void invalidate() { (1)
}
@CacheInvalidateAll(cacheName = "foo")
public void invalidateAll() {
}
}
1 | メソッドには引数がないため、キャッシュ名から派生した一意のデフォルトキャッシュキーが使用されます。 |
単一メソッドでの複数アノテーション
package org.acme.cache;
import jakarta.inject.Singleton;
import io.quarkus.cache.CacheInvalidate;
import io.quarkus.cache.CacheInvalidateAll;
import io.quarkus.cache.CacheResult;
@Singleton
public class CachedService {
@CacheInvalidate(cacheName = "foo")
@CacheResult(cacheName = "foo")
public String forceCacheEntryRefresh(Object key) { (1)
// Call expensive service here.
}
@CacheInvalidateAll(cacheName = "foo")
@CacheInvalidateAll(cacheName = "bar")
public void multipleInvalidateAll(Object key) { (2)
}
}
1 | このメソッドを使用して、指定されたキーに対応するキャッシュエントリーを強制的に更新することができます。 |
2 | このメソッドは、一度の呼び出しで foo および bar キャッシュからのすべてのエントリーを無効にします。 |
すべてのアプリケーションキャッシュの消去
package org.acme.cache;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import io.quarkus.cache.CacheManager;
@Singleton
public class CacheClearer {
private final CacheManager cacheManager;
public CacheClearer(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public void clearAllCaches() {
for (String cacheName : cacheManager.getCacheNames()) {
cacheManager.getCache(cacheName).get().invalidateAll().await().indefinitely();
}
}
}
ネガティブキャッシングとnull
(高価な)リモートコールの結果をキャッシュしたい場合があります。リモートコールが失敗した場合、結果や例外をキャッシュするのではなく、 次の呼び出しでリモートコールを再試行したい場合があります。
シンプルなアプローチとしては、例外をキャッチして null
を返すことで、呼び出し元がそれに応じて行動できるようにすることができます。
public void caller(int val) {
Integer result = callRemote(val); (1)
if (result != null) {
System.out.println("Result is " + result);
else {
System.out.println("Got an exception");
}
}
@CacheResult(cacheName = "foo")
public Integer callRemote(int val) {
try {
Integer val = remoteWebServer.getResult(val); (2)
return val;
} catch (Exception e) {
return null; (3)
}
}
1 | リモートを呼び出すためにメソッドを実行 |
2 | リモートコールを行い、その結果を返却 |
3 | 例外が発生時にリターンする |
このアプローチには不幸な副作用があります。先に述べたように、Quarkusは null
の値をキャッシュすることもできます。つまり、同じパラメーター値を持つ callRemote()
への次の呼び出しは、キャッシュの外で応答され、 null
が返され、リモートコールは行われないということです。これはシナリオによっては望ましいことかもしれませんが、通常は結果が返ってくるまでリモートコールを再試行したいものです。
例外をバブルアップさせる
リモートコールの結果をキャッシュ(マーカー)しないようにするには、コールされたメソッドから例外をバブルアップし、呼び出し側でキャッチする必要があります。
public void caller(int val) {
try {
Integer result = callRemote(val); (1)
System.out.println("Result is " + result);
} catch (Exception e) {
System.out.println("Got an exception");
}
@CacheResult(cacheName = "foo")
public Integer callRemote(int val) throws Exception { (2)
Integer val = remoteWebServer.getResult(val); (3)
return val;
}
1 | リモートを呼び出すためにメソッドを実行 |
2 | 例外がバブルアップする場合がある |
3 | これは、あらゆる種類のリモート例外を投げることができます |
リモートへの呼び出しが例外をスローした場合、キャッシュは結果を保存しないので、 同じパラメーター値を持つ callRemote()
への後続の呼び出しがキャッシュから応答されることはありません。その代わりに、リモートへの呼び出しを再度試みることになります。
ネイティブ化
Cache エクステンションは、ネイティブ実行可能ファイルの構築をサポートしています。
実行時の速度を最適化するために、Caffeineはキャッシュ設定に応じて選択される多くのキャッシュ実装クラスを組み込んでいます。すべてのクラスを登録するのは非常にコストがかかるため、リフレクションのためにすべてのクラスを登録していません(登録されていないクラスはネイティブ実行可能ファイルに含まれません)。
ここでは、最も一般的な実装を登録していますが、キャッシュの設定によっては、以下のようなエラーが発生する場合があります。
2021-12-08 02:32:02,108 ERROR [io.qua.run.Application] (main) Failed to start application (with profile prod): java.lang.ClassNotFoundException: com.github.benmanes.caffeine.cache.PSAMS (1)
at java.lang.Class.forName(DynamicHub.java:1433)
at java.lang.Class.forName(DynamicHub.java:1408)
at com.github.benmanes.caffeine.cache.NodeFactory.newFactory(NodeFactory.java:111)
at com.github.benmanes.caffeine.cache.BoundedLocalCache.<init>(BoundedLocalCache.java:240)
at com.github.benmanes.caffeine.cache.SS.<init>(SS.java:31)
at com.github.benmanes.caffeine.cache.SSMS.<init>(SSMS.java:64)
at com.github.benmanes.caffeine.cache.SSMSA.<init>(SSMSA.java:43)
1 | PSAMS は、Caffeineの数あるキャッシュ実装クラスのひとつなので、この部分は変わるかもしれません。 |
このエラーが発生した場合は、アプリケーションクラスに以下のアノテーションを追加することで、簡単に修正することができます(あるいは、このアノテーションをホストするためだけに、 Reflections
のような新しいクラスを作成することもできます)。
@RegisterForReflection(classNames = { "com.github.benmanes.caffeine.cache.PSAMS" }) (1)
1 | 配列なので、複数のキャッシュ実装が必要な構成の場合、一度に登録することができます。 |
This annotation will register the cache implementation classes for reflection and this will include the classes into the native executable.
More details about the @RegisterForReflection
annotation can be found on the native application tips page.