1. 概要

このチュートリアルでは、クロニクルマップを使用してキーと値のペアを格納する方法を説明します。 また、その動作と使用法を示す短い例を作成します。

2. クロニクルマップとは何ですか?

ドキュメントに従って、「ChronicleMapは、低レイテンシおよび/またはマルチプロセスアプリケーション向けに設計された、超高速、メモリ内、ノンブロッキング、Key-Valueストアです」。

一言で言えば、それはオフヒープのKey-Valueストアです。 マップが正しく機能するために大量のRAMを必要としません。使用可能なディスク容量に基づいてマップを拡大できます。 さらに、マルチマスターサーバーセットアップでのデータのレプリケーションをサポートします。

それでは、どのようにセットアップして操作できるかを見てみましょう。

3. Mavenの依存関係

開始するには、chronicle-map依存関係をプロジェクトに追加する必要があります。

<dependency>
    <groupId>net.openhft</groupId>
    <artifactId>chronicle-map</artifactId>
    <version>3.17.2</version>
</dependency>

4. クロニクルマップの種類

マップは、メモリ内マップまたは永続化マップの2つの方法で作成できます。

これらの両方を詳しく見てみましょう。

4.1. インメモリマップ

インメモリクロニクルマップは、サーバーの物理メモリ内に作成されるマップストアです。 これは、マップストアが作成されたJVMプロセス内でのみアクセス可能であることを意味します

簡単な例を見てみましょう:

ChronicleMap<LongValue, CharSequence> inMemoryCountryMap = ChronicleMap
  .of(LongValue.class, CharSequence.class)
  .name("country-map")
  .entries(50)
  .averageValue("America")
  .create();

わかりやすくするために、50の国IDとその名前を格納するマップを作成しています。 コードスニペットでわかるように、 averageValue()構成を除いて、作成は非常に簡単です。 これは、マップエントリ値が取得する平均バイト数を設定するようにマップに指示します。

言い換えると、 マップを作成するとき、クロニクルマップは、シリアル化された形式の値が取得する平均バイト数を決定します。 これは、構成された値マーシャラーを使用して、指定された平均値をシリアル化することによって行われます。 次に、決定されたバイト数を各マップエントリの値に割り当てます。

インメモリマップに関して注意しなければならないことの1つは、JVMプロセスが稼働している場合にのみデータにアクセスできることです。 プロセスが終了すると、ライブラリはデータをクリアします。

4.2. 永続化されたマップ

インメモリマップとは異なり、実装は永続化されたマップをディスクに保存します。 次に、永続化されたマップを作成する方法を見てみましょう。

ChronicleMap<LongValue, CharSequence> persistedCountryMap = ChronicleMap
  .of(LongValue.class, CharSequence.class)
  .name("country-map")
  .entries(50)
  .averageValue("America")
  .createPersistedTo(new File(System.getProperty("user.home") + "/country-details.dat"));

これにより、指定したフォルダーにcountry-details.datというファイルが作成されます。 このファイルが指定されたパスですでに使用可能な場合、ビルダー実装はこのJVMプロセスから既存のデータストアへのリンクを開きます。

永続化されたマップは、次の場合に利用できます。

  • 作成者のプロセスを超えて生き残る。 たとえば、ホットアプリケーションの再デプロイをサポートするため
  • サーバー内でグローバルにします。 たとえば、複数の同時プロセスアクセスをサポートするため
  • ディスクに保存するデータストアとして機能します

5. サイズ構成

キー/値タイプがボックス化されたプリミティブまたは値インターフェイスの場合を除いて、クロニクルマップの作成中に平均値と平均キーを構成する必要があります。 この例では、キータイプLongValuevalueinterface であるため、平均キーを構成していません。

次に、キー/値の平均バイト数を構成するためのオプションを見てみましょう。

  • averageValue() –マップエントリの値に割り当てられる平均バイト数が決定される値
  • averageValueSize() –マップエントリの値に割り当てられる平均バイト数
  • constantValueSizeBySample() –値のサイズが常に同じである場合にマップエントリの値に割り当てられるバイト数
  • averageKey() –マップエントリのキーに割り当てられる平均バイト数が決定されるキー
  • averageKeySize() –マップエントリのキーに割り当てられる平均バイト数
  • constantKeySizeBySample() –キーのサイズが常に同じである場合にマップエントリのキーに割り当てられるバイト数

6. キーと値のタイプ

クロニクルマップを作成するとき、特にキーと値を定義するときは、従う必要のある特定の基準があります。 推奨されるタイプを使用してキーと値を作成すると、マップが最適に機能します。

推奨されるタイプのいくつかを次に示します。

  • インターフェース
  • ChronicleBytesからByteableインターフェースを実装するクラス
  • ChronicleBytesのBytesMarshallableインターフェイスを実装するクラス。 実装クラスには、パブリックの引数なしコンストラクターが必要です
  • byte[]およびByteBuffer
  • CharSequence String 、および StringBuilder
  • 整数ロング、およびダブル
  • java.io.Externalizableを実装するすべてのクラス。 実装クラスには、パブリックの引数なしコンストラクターが必要です
  • java.io.Serializable を実装するすべてのタイプ(ボックス化されたプリミティブタイプ(上記を除く)および配列タイプを含む)
  • カスタムシリアライザーが提供されている場合は、その他のタイプ

7. クロニクルマップのクエリ

Chronicle Mapは、シングルキークエリとマルチキークエリをサポートしています。

7.1. シングルキークエリ

シングルキークエリは、シングルキーを処理する操作です。 ChronicleMap は、Java MapインターフェースおよびConcurrentMapインターフェースからのすべての操作をサポートします。

LongValue qatarKey = Values.newHeapInstance(LongValue.class);
qatarKey.setValue(1);
inMemoryCountryMap.put(qatarKey, "Qatar");

//...

CharSequence country = inMemoryCountryMap.get(key);

通常のgetおよびput操作に加えて、 ChronicleMapは、エントリの取得および処理中のメモリフットプリントを削減する特別な操作getUsing()を追加します。 これを実際に見てみましょう:

LongValue key = Values.newHeapInstance(LongValue.class);
StringBuilder country = new StringBuilder();
key.setValue(1);
persistedCountryMap.getUsing(key, country);
assertThat(country.toString(), is(equalTo("Romania")));

key.setValue(2);
persistedCountryMap.getUsing(key, country);
assertThat(country.toString(), is(equalTo("India")));

ここでは、同じ StringBuilder オブジェクトを使用して、 getUsing()メソッドに渡すことにより、異なるキーの値を取得しました。 基本的に、同じオブジェクトを再利用して異なるエントリを取得します。 この場合、 getUsing()メソッドは次と同等です。

country.setLength(0);
country.append(persistedCountryMap.get(key));

7.2. マルチキークエリ

複数のキーを同時に処理する必要があるユースケースがあるかもしれません。 このために、 queryContext()機能を使用できます。 queryContext()メソッドは、マップエントリを操作するためのコンテキストを作成します。

最初にマルチマップを作成し、それにいくつかの値を追加しましょう。

Set<Integer> averageValue = IntStream.of(1, 2).boxed().collect(Collectors.toSet());
ChronicleMap<Integer, Set<Integer>> multiMap = ChronicleMap
  .of(Integer.class, (Class<Set<Integer>>) (Class) Set.class)
  .name("multi-map")
  .entries(50)
  .averageValue(averageValue)
  .create();

Set<Integer> set1 = new HashSet<>();
set1.add(1);
set1.add(2);
multiMap.put(1, set1);

Set<Integer> set2 = new HashSet<>();
set2.add(3);
multiMap.put(2, set2);

複数のエントリを処理するには、同時更新によって発生する可能性のある不整合を防ぐために、これらのエントリをロックする必要があります。

try (ExternalMapQueryContext<Integer, Set<Integer>, ?> fistContext = multiMap.queryContext(1)) {
    try (ExternalMapQueryContext<Integer, Set<Integer>, ?> secondContext = multiMap.queryContext(2)) {
        fistContext.updateLock().lock();
        secondContext.updateLock().lock();

        MapEntry<Integer, Set<Integer>> firstEntry = fistContext.entry();
        Set<Integer> firstSet = firstEntry.value().get();
        firstSet.remove(2);

        MapEntry<Integer, Set<Integer>> secondEntry = secondContext.entry();
        Set<Integer> secondSet = secondEntry.value().get();
        secondSet.add(4);

        firstEntry.doReplaceValue(fistContext.wrapValueAsData(firstSet));
        secondEntry.doReplaceValue(secondContext.wrapValueAsData(secondSet));
    }
} finally {
    assertThat(multiMap.get(1).size(), is(equalTo(1)));
    assertThat(multiMap.get(2).size(), is(equalTo(2)));
}

8. クロニクルマップを閉じる

マップの操作が終了したので、マップオブジェクトで close()メソッドを呼び出して、オフヒープメモリとそれに関連するリソースを解放しましょう。

persistedCountryMap.close();
inMemoryCountryMap.close();
multiMap.close();

ここで覚えておくべきことの1つは、マップを閉じる前にすべてのマップ操作を完了する必要があるということです。 そうしないと、JVMが予期せずクラッシュする可能性があります。

9. 結論

このチュートリアルでは、クロニクルマップを使用してキーと値のペアを保存および取得する方法を学習しました。 コミュニティバージョンはほとんどのコア機能で利用できますが、商用バージョンには、複数のサーバー間でのデータレプリケーションやリモート呼び出しなどの高度な機能がいくつかあります。

ここで説明したすべての例は、Githubプロジェクトにあります。