JavaのInfinispanガイド
1. 概要
このガイドでは、同じニッチの他のツールよりも堅牢な機能セットを備えたインメモリキー/値データストアであるInfinispanについて学習します。
それがどのように機能するかを理解するために、最も一般的な機能を紹介する簡単なプロジェクトを作成し、それらの使用方法を確認します。
2. プロジェクトのセットアップ
このように使用できるようにするには、pom.xmlに依存関係を追加する必要があります。
最新バージョンはMaven Centralリポジトリにあります:
org.infinispan
infinispan-core
9.1.5.Final
必要な基盤インフラストラクチャはすべて、これからプログラムで処理されます。
3. CacheManagerセットアップ
CacheManagerは、使用する機能の大部分の基盤です。 宣言されたすべてのキャッシュのコンテナとして機能し、それらのライフサイクルを制御し、グローバル構成を担当します。
Infinispanには、CacheManagerを作成するための非常に簡単な方法が付属しています。
public DefaultCacheManager cacheManager() {
return new DefaultCacheManager();
}
これで、それを使用してキャッシュを構築できるようになりました。
4. キャッシュの設定
キャッシュは、名前と構成によって定義されます。 必要な構成は、クラスパスですでに使用可能なクラスConfigurationBuilderを使用して構築できます。
キャッシュをテストするために、重いクエリをシミュレートする簡単なメソッドを作成します。
public class HelloWorldRepository {
public String getHelloWorld() {
try {
System.out.println("Executing some heavy query");
Thread.sleep(1000);
} catch (InterruptedException e) {
// ...
e.printStackTrace();
}
return "Hello World!";
}
}
また、キャッシュの変更を確認できるように、Infinispanは単純な注釈@Listenerを提供します。
キャッシュを定義するとき、内部で発生するイベントに関心のあるオブジェクトを渡すことができ、Infinispanはキャッシュを処理するときに通知します。
@Listener
public class CacheListener {
@CacheEntryCreated
public void entryCreated(CacheEntryCreatedEvent event) {
this.printLog("Adding key '" + event.getKey()
+ "' to cache", event);
}
@CacheEntryExpired
public void entryExpired(CacheEntryExpiredEvent event) {
this.printLog("Expiring key '" + event.getKey()
+ "' from cache", event);
}
@CacheEntryVisited
public void entryVisited(CacheEntryVisitedEvent event) {
this.printLog("Key '" + event.getKey() + "' was visited", event);
}
@CacheEntryActivated
public void entryActivated(CacheEntryActivatedEvent event) {
this.printLog("Activating key '" + event.getKey()
+ "' on cache", event);
}
@CacheEntryPassivated
public void entryPassivated(CacheEntryPassivatedEvent event) {
this.printLog("Passivating key '" + event.getKey()
+ "' from cache", event);
}
@CacheEntryLoaded
public void entryLoaded(CacheEntryLoadedEvent event) {
this.printLog("Loading key '" + event.getKey()
+ "' to cache", event);
}
@CacheEntriesEvicted
public void entriesEvicted(CacheEntriesEvictedEvent event) {
StringBuilder builder = new StringBuilder();
event.getEntries().forEach(
(key, value) -> builder.append(key).append(", "));
System.out.println("Evicting following entries from cache: "
+ builder.toString());
}
private void printLog(String log, CacheEntryEvent event) {
if (!event.isPre()) {
System.out.println(log);
}
}
}
メッセージを出力する前に、通知対象のイベントが既に発生しているかどうかを確認します。これは、一部のイベントタイプでは、Infinispanが処理前と処理直後の2つの通知を送信するためです。
それでは、キャッシュの作成を処理するメソッドを作成しましょう。
private Cache buildCache(
String cacheName,
DefaultCacheManager cacheManager,
CacheListener listener,
Configuration configuration) {
cacheManager.defineConfiguration(cacheName, configuration);
Cache cache = cacheManager.getCache(cacheName);
cache.addListener(listener);
return cache;
}
構成をCacheManagerに渡し、同じcacheNameを使用して、必要なキャッシュに対応するオブジェクトを取得する方法に注目してください。 キャッシュオブジェクト自体にリスナーを通知する方法にも注意してください。
次に、5つの異なるキャッシュ構成を確認し、それらをセットアップして最大限に活用する方法を確認します。
4.1. シンプルキャッシュ
最も単純なタイプのキャッシュは、メソッドbuildCacheを使用して1行で定義できます。
public Cache simpleHelloWorldCache(
DefaultCacheManager cacheManager,
CacheListener listener) {
return this.buildCache(SIMPLE_HELLO_WORLD_CACHE,
cacheManager, listener, new ConfigurationBuilder().build());
}
これで、Serviceを作成できます。
public String findSimpleHelloWorld() {
String cacheKey = "simple-hello";
return simpleHelloWorldCache
.computeIfAbsent(cacheKey, k -> repository.getHelloWorld());
}
キャッシュの使用方法に注意してください。まず、目的のエントリが既にキャッシュされているかどうかを確認します。 そうでない場合は、Repositoryを呼び出して、キャッシュする必要があります。
テストに簡単なメソッドを追加して、メソッドの時間を計りましょう。
protected long timeThis(Supplier supplier) {
long millis = System.currentTimeMillis();
supplier.get();
return System.currentTimeMillis() - millis;
}
それをテストして、2つのメソッド呼び出しの実行間隔を確認できます。
@Test
public void whenGetIsCalledTwoTimes_thenTheSecondShouldHitTheCache() {
assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld()))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld()))
.isLessThan(100);
}
4.2. 有効期限キャッシュ
すべてのエントリに有効期間があるキャッシュを定義できます。つまり、指定された期間が経過すると、要素がキャッシュから削除されます。 構成は非常に簡単です。
private Configuration expiringConfiguration() {
return new ConfigurationBuilder().expiration()
.lifespan(1, TimeUnit.SECONDS)
.build();
}
次に、上記の構成を使用してキャッシュを構築します。
public Cache expiringHelloWorldCache(
DefaultCacheManager cacheManager,
CacheListener listener) {
return this.buildCache(EXPIRING_HELLO_WORLD_CACHE,
cacheManager, listener, expiringConfiguration());
}
最後に、上記の単純なキャッシュから同様の方法で使用します。
public String findSimpleHelloWorldInExpiringCache() {
String cacheKey = "simple-hello";
String helloWorld = expiringHelloWorldCache.get(cacheKey);
if (helloWorld == null) {
helloWorld = repository.getHelloWorld();
expiringHelloWorldCache.put(cacheKey, helloWorld);
}
return helloWorld;
}
もう一度時間をテストしてみましょう。
@Test
public void whenGetIsCalledTwoTimesQuickly_thenTheSecondShouldHitTheCache() {
assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
.isLessThan(100);
}
実行すると、すぐにキャッシュがヒットすることがわかります。 有効期限がエントリのput時間に関連していることを示すために、エントリに強制的に入れましょう。
@Test
public void whenGetIsCalledTwiceSparsely_thenNeitherHitsTheCache()
throws InterruptedException {
assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
.isGreaterThanOrEqualTo(1000);
Thread.sleep(1100);
assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
.isGreaterThanOrEqualTo(1000);
}
テストの実行後、指定された時間が経過するとキャッシュからエントリが期限切れになることに注意してください。 これを確認するには、リスナーから出力されたログ行を確認します。
Executing some heavy query
Adding key 'simple-hello' to cache
Expiring key 'simple-hello' from cache
Executing some heavy query
Adding key 'simple-hello' to cache
エントリにアクセスしようとすると、エントリは期限切れになることに注意してください。 Infinispanは、アクセスしようとしたとき、またはリーパースレッドがキャッシュをスキャンしたときの2つの瞬間に期限切れのエントリをチェックします。
メイン構成にキャッシュがない場合でも、有効期限を使用できます。 メソッドputは、さらに多くの引数を受け入れます。
simpleHelloWorldCache.put(cacheKey, helloWorld, 10, TimeUnit.SECONDS);
または、固定の寿命の代わりに、エントリに最大idleTimeを与えることができます。
simpleHelloWorldCache.put(cacheKey, helloWorld, -1, TimeUnit.SECONDS, 10, TimeUnit.SECONDS);
ライフスパン属性に-1を使用すると、キャッシュは期限切れになりませんが、10秒のidleTimeと組み合わせると、この時間枠でアクセスされない限り、このエントリを期限切れにするようにInfinispanに指示します。
4.3. キャッシュエビクション
Infinispanでは、eviction configuration:を使用して特定のキャッシュ内のエントリ数を制限できます。
private Configuration evictingConfiguration() {
return new ConfigurationBuilder()
.memory().evictionType(EvictionType.COUNT).size(1)
.build();
}
この例では、このキャッシュの最大エントリを1つに制限しています。つまり、別のエントリを入力しようとすると、キャッシュから削除されます。
繰り返しますが、この方法はすでに説明したものと似ています。
public String findEvictingHelloWorld(String key) {
String value = evictingHelloWorldCache.get(key);
if(value == null) {
value = repository.getHelloWorld();
evictingHelloWorldCache.put(key, value);
}
return value;
}
テストを作成しましょう:
@Test
public void whenTwoAreAdded_thenFirstShouldntBeAvailable() {
assertThat(timeThis(
() -> helloWorldService.findEvictingHelloWorld("key 1")))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(
() -> helloWorldService.findEvictingHelloWorld("key 2")))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(
() -> helloWorldService.findEvictingHelloWorld("key 1")))
.isGreaterThanOrEqualTo(1000);
}
テストを実行すると、アクティビティのリスナーログを確認できます。
Executing some heavy query
Adding key 'key 1' to cache
Executing some heavy query
Evicting following entries from cache: key 1,
Adding key 'key 2' to cache
Executing some heavy query
Evicting following entries from cache: key 2,
Adding key 'key 1' to cache
2番目のキーを挿入したときに最初のキーがキャッシュから自動的に削除されたことを確認してから、2番目のキーも削除して、最初のキーのスペースを確保します。
4.4. パッシベーションキャッシュ
cache passivationは、Infinispanの強力な機能の1つです。 パッシベーションとエビクションを組み合わせることで、情報を失うことなく、多くのメモリを占有しないキャッシュを作成できます。
パッシベーション構成を見てみましょう。
private Configuration passivatingConfiguration() {
return new ConfigurationBuilder()
.memory().evictionType(EvictionType.COUNT).size(1)
.persistence()
.passivation(true) // activating passivation
.addSingleFileStore() // in a single file
.purgeOnStartup(true) // clean the file on startup
.location(System.getProperty("java.io.tmpdir"))
.build();
}
キャッシュメモリにエントリを1つだけ強制しますが、Infinispanに、残りのエントリを削除するのではなく、パッシベーションするように指示します。
複数のエントリを入力しようとするとどうなるか見てみましょう。
public String findPassivatingHelloWorld(String key) {
return passivatingHelloWorldCache.computeIfAbsent(key, k ->
repository.getHelloWorld());
}
テストを作成して実行してみましょう。
@Test
public void whenTwoAreAdded_thenTheFirstShouldBeAvailable() {
assertThat(timeThis(
() -> helloWorldService.findPassivatingHelloWorld("key 1")))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(
() -> helloWorldService.findPassivatingHelloWorld("key 2")))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(
() -> helloWorldService.findPassivatingHelloWorld("key 1")))
.isLessThan(100);
}
それでは、リスナーのアクティビティを見てみましょう。
Executing some heavy query
Adding key 'key 1' to cache
Executing some heavy query
Passivating key 'key 1' from cache
Evicting following entries from cache: key 1,
Adding key 'key 2' to cache
Passivating key 'key 2' from cache
Evicting following entries from cache: key 2,
Loading key 'key 1' to cache
Activating key 'key 1' on cache
Key 'key 1' was visited
1つのエントリのみでキャッシュを保持するために必要なステップ数に注意してください。 また、ステップの順序に注意してください-パッシベーション、エビクション、ロード、そしてアクティベーション。 これらの手順の意味を見てみましょう。
-
Passivation –エントリは、Infinispanのメインストレージ(この場合はメモリ)から離れた別の場所に保存されます
-
Eviction –エントリが削除され、メモリが解放され、設定された最大エントリ数がキャッシュに保持されます
-
パッシベーションされたエントリに到達しようとすると、Loading –、Infinispanは保存されているコンテンツをチェックし、エントリをメモリに再度ロードします
-
Activation –エントリがInfinispanで再びアクセス可能になりました
4.5. トランザクションキャッシュ
Infinispanには、強力なトランザクション制御が付属しています。 データベースの対応物と同様に、複数のスレッドが同じエントリを書き込もうとしている間に整合性を維持するのに役立ちます。
トランザクション機能を使用してキャッシュを定義する方法を見てみましょう。
private Configuration transactionalConfiguration() {
return new ConfigurationBuilder()
.transaction().transactionMode(TransactionMode.TRANSACTIONAL)
.lockingMode(LockingMode.PESSIMISTIC)
.build();
}
テストを可能にするために、2つのメソッドを作成しましょう。1つはトランザクションを迅速に終了し、もう1つは時間がかかります。
public Integer getQuickHowManyVisits() {
TransactionManager tm = transactionalCache
.getAdvancedCache().getTransactionManager();
tm.begin();
Integer howManyVisits = transactionalCache.get(KEY);
howManyVisits++;
System.out.println("I'll try to set HowManyVisits to " + howManyVisits);
StopWatch watch = new StopWatch();
watch.start();
transactionalCache.put(KEY, howManyVisits);
watch.stop();
System.out.println("I was able to set HowManyVisits to " + howManyVisits +
" after waiting " + watch.getTotalTimeSeconds() + " seconds");
tm.commit();
return howManyVisits;
}
public void startBackgroundBatch() {
TransactionManager tm = transactionalCache
.getAdvancedCache().getTransactionManager();
tm.begin();
transactionalCache.put(KEY, 1000);
System.out.println("HowManyVisits should now be 1000, " +
"but we are holding the transaction");
Thread.sleep(1000L);
tm.rollback();
System.out.println("The slow batch suffered a rollback");
}
次に、両方のメソッドを実行するテストを作成し、Infinispanがどのように動作するかを確認しましょう。
@Test
public void whenLockingAnEntry_thenItShouldBeInaccessible() throws InterruptedException {
Runnable backGroundJob = () -> transactionalService.startBackgroundBatch();
Thread backgroundThread = new Thread(backGroundJob);
transactionalService.getQuickHowManyVisits();
backgroundThread.start();
Thread.sleep(100); //lets wait our thread warm up
assertThat(timeThis(() -> transactionalService.getQuickHowManyVisits()))
.isGreaterThan(500).isLessThan(1000);
}
実行すると、コンソールに次のアクティビティが再び表示されます。
Adding key 'key' to cache
Key 'key' was visited
Ill try to set HowManyVisits to 1
I was able to set HowManyVisits to 1 after waiting 0.001 seconds
HowManyVisits should now be 1000, but we are holding the transaction
Key 'key' was visited
Ill try to set HowManyVisits to 2
I was able to set HowManyVisits to 2 after waiting 0.902 seconds
The slow batch suffered a rollback
メインスレッドの時間を確認し、slowメソッドによって作成されたトランザクションの終了を待ちます。
5. 結論
この記事では、Infinispanとは何か、そしてアプリケーション内のキャッシュとしての主要な機能について説明しました。
いつものように、コードはover on Githubで見つけることができます。