JavaでのInfinispanのガイド

1概要

このガイドでは、http://infinispan.org/[Inffinispan]について学びます。これは、同じ分野の他のツールよりも強力な機能セットを備えた、インメモリのキー/値データストアです。

仕組みを理解するために、最も一般的な機能を紹介する簡単なプロジェクトを作成し、それらの使用方法を確認します。

2プロジェクト設定

このように使用するには、__pom.xmlに依存関係を追加する必要があります。

最新版はhttps://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.infinispan%22%20AND%20a%3A%22infinispan-core%22[Maven Centralにあります。]リポジトリ:

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

これで Service を構築できます。

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 timeに相対的であることを示すために、エントリにそれを強制しましょう:

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

lifespan属性に-1を使用すると、キャッシュの有効期限は切れませんが、10秒の idleTime と組み合わせると、この期間内にアクセスされない限り、Infinispanにこのエントリを期限切れにするよう指示します。

4.3. キャッシュ追い出し

Infinispanでは、__eviction設定を使用して、特定のキャッシュ内のエントリ数を制限できます。

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のメインストレージ(この場合はメモリ) 立ち退き - ** エントリを削除してメモリを解放し、

キャッシュ内の最大エントリ数 読み込み中 - ** パッシブ化されたエントリ、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

slowメソッドによって作成されたトランザクションの終了を待って、メインスレッドの時間を確認します。

5結論

この記事では、Infinispanが何であるかを見てきました。これは、アプリケーション内のキャッシュとしての主要な機能です。

いつものように、このコードはhttps://github.com/eugenp/tutorials/tree/master/libraries[over Github]にあります。

前の投稿:Spring Data Elasticsearchの紹介
次の投稿:java.lang.Process APIガイド