JavaでのInfinispanのガイド
1. 概要
このガイドでは、 Infinispan について学習します。これは、同じニッチの他のツールよりも堅牢な機能セットを備えたインメモリキー/値データストアです。
それがどのように機能するかを理解するために、最も一般的な機能を紹介する簡単なプロジェクトを作成し、それらがどのように使用できるかを確認します。
2. プロジェクトの設定
このように使用できるようにするには、pom.xmlに依存関係を追加する必要があります。
最新バージョンは、 MavenCentralリポジトリにあります。
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<version>9.1.5.Final</version>
</dependency>
今後、必要な基盤となるインフラストラクチャはすべてプログラムで処理されます。
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<String, String> event) {
this.printLog("Adding key '" + event.getKey()
+ "' to cache", event);
}
@CacheEntryExpired
public void entryExpired(CacheEntryExpiredEvent<String, String> event) {
this.printLog("Expiring key '" + event.getKey()
+ "' from cache", event);
}
@CacheEntryVisited
public void entryVisited(CacheEntryVisitedEvent<String, String> event) {
this.printLog("Key '" + event.getKey() + "' was visited", event);
}
@CacheEntryActivated
public void entryActivated(CacheEntryActivatedEvent<String, String> event) {
this.printLog("Activating key '" + event.getKey()
+ "' on cache", event);
}
@CacheEntryPassivated
public void entryPassivated(CacheEntryPassivatedEvent<String, String> event) {
this.printLog("Passivating key '" + event.getKey()
+ "' from cache", event);
}
@CacheEntryLoaded
public void entryLoaded(CacheEntryLoadedEvent<String, String> event) {
this.printLog("Loading key '" + event.getKey()
+ "' to cache", event);
}
@CacheEntriesEvicted
public void entriesEvicted(CacheEntriesEvictedEvent<String, String> 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 <K, V> Cache<K, V> buildCache(
String cacheName,
DefaultCacheManager cacheManager,
CacheListener listener,
Configuration configuration) {
cacheManager.defineConfiguration(cacheName, configuration);
Cache<K, V> cache = cacheManager.getCache(cacheName);
cache.addListener(listener);
return cache;
}
構成をCacheManagerに渡し、同じ cacheName を使用して、必要なキャッシュに対応するオブジェクトを取得する方法に注目してください。 キャッシュオブジェクト自体をリスナーに通知する方法にも注意してください。
次に、5つの異なるキャッシュ構成を確認し、それらをセットアップして最大限に活用する方法を確認します。
4.1. シンプルキャッシュ
最も単純なタイプのキャッシュは、メソッド buildCache を使用して、1行で定義できます。
public Cache<String, String> simpleHelloWorldCache(
DefaultCacheManager cacheManager,
CacheListener listener) {
return this.buildCache(SIMPLE_HELLO_WORLD_CACHE,
cacheManager, listener, new ConfigurationBuilder().build());
}
これで、サービスを作成できます。
public String findSimpleHelloWorld() {
String cacheKey = "simple-hello";
return simpleHelloWorldCache
.computeIfAbsent(cacheKey, k -> repository.getHelloWorld());
}
キャッシュの使用方法に注意してください。最初に、必要なエントリがすでにキャッシュされているかどうかを確認します。 そうでない場合は、リポジトリを呼び出してからキャッシュする必要があります。
テストに簡単なメソッドを追加して、メソッドの時間を計りましょう。
protected <T> long timeThis(Supplier<T> 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<String, String> 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では、エビクション構成を使用して特定のキャッシュ内のエントリ数を制限できます:
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. パッシベーションキャッシュ
キャッシュパッシベーションは、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つのエントリだけでキャッシュを保持するために何ステップかかったかに注意してください。 また、ステップの順序(パッシベーション、エビクション、ロード、アクティベーション)に注意してください。 これらの手順の意味を見てみましょう。
- パッシベーション– エントリは、Infinispanのメインストレージ(この場合はメモリ)から離れた別の場所に保存されます
- Eviction – エントリが削除され、メモリが解放され、設定された最大エントリ数がキャッシュに保持されます
- ロード中– 不動態化されたエントリに到達しようとすると、Infinispanは保存されているコンテンツをチェックし、エントリをメモリに再度ロードします
- アクティベーション–エントリが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
低速メソッドによって作成されたトランザクションの終了を待って、メインスレッドの時間を確認します。
5. 結論
この記事では、Infinispanとは何か、そしてアプリケーション内のキャッシュとしての主要な機能について説明しました。
いつものように、コードはGithubのにあります。