CLOVER🍀

That was when it all began.

メモリ、ファイルをコレクションの格納先にできる、MapDBを使ってみる

MapやQueueのデータの保存先をメモリ、Off-Heap、ファイルで選べる、MapDBというライブラリがあるようです。

MapDB
http://www.mapdb.org/

サイトの説明には、組み込みデータベースとか書かれていますね。

主な特徴は、こんな感じ?

  • ヒープ、Off-Heap、ファイルにデータの保存先を指定可能(次のバージョンからは、sun.misc.Unsafeを使ったものも追加されそう)
  • 400Kバイト程度のJARファイル
  • エントリの有効期限の設定
  • トランザクションサポート
  • キャッシュサポート

などなど。キャッシュソフトウェアの置き換えにどうぞ、と書かれていますが、どうなんでしょうね。分散系の機能はなさそうです。

詳しくは、ドキュメントやサンプルを参照…。

ドキュメント
http://www.mapdb.org/doc/index.html

Getting started
http://www.mapdb.org/doc/getting-started.html

サンプル
https://github.com/jankotek/MapDB/tree/master/src/test/java/examples

で終わるのもなんなので、ちょっと試してみます。

参考にしたのは、サンプルとJavadocです。Javadocは、ちょっと前までオフィシャルサイトで見れていたはずなのですが、デザインが最近変わったようで、合わせて見れなくなりました…。とりあえず、以下を見ています。

http://javadox.com/org.mapdb/mapdb/1.0.6/org/mapdb/package-tree.html

Maven依存関係

MapDBを使用するために必要な依存関係は、以下になります。

    <dependency>
      <groupId>org.mapdb</groupId>
      <artifactId>mapdb</artifactId>
      <version>1.0.6</version>
    </dependency>

他に依存関係はありません。内部的に使用されているロギングフレームワークは…java.util.loggingです。

あとは、サンプルの都合上、JUnitとAssertJを使用します。

基本的な使い方

importするもの

パッケージが「org.mapdb」しかないので、今回は以下のようにしました。

import org.mapdb.*;
DBの作成

まずは、DBのインスタンスを取得します。

        DB db =
            DBMaker
            .newMemoryDB()
            .transactionDisable()
            .make();

DBMakerから、「new〜DB」という名前のメソッドを呼び出し、最後にmakeすることでDBのインスタンスを取得できます。この時、「new〜DB」の「〜」の部分を変えることで、データの保存先が変わります。

ここでは、インメモリです。あと、トランザクションは不要なのでオフにしておきました(これは必須ではありません)。

コレクションの取得/利用

作成したDBから使いたいコレクションと名前を指定してgetすることで、コレクションを取得できます。

        HTreeMap<String, String> map = db.getHashMap("in-memory-hashmap");

HashMapと言いつつ、HTreeMapという型になっていますが、JavaのMapを実装しています。

        assertThat(map)
            .isInstanceOf(java.util.Map.class);
        assertThat(map)
            .isInstanceOf(java.util.concurrent.ConcurrentMap.class);

というわけで、普通のMapと同じように使えます。

        map.put("key1", "value1");
        map.put("key2", "value2");

        assertThat(map.get("key1"))
            .isEqualTo("value1");
        assertThat(map.get("key2"))
            .isEqualTo("value2");
        assertThat(map.get("key3"))
            .isNull();
DBのクローズ

最後は、DBインスタンスをcloseします。

        db.close();

残念ながら、DBはCloseableインターフェースは実装していません。

もっとサンプルを

Heap DBの利用

データの保存先を、Heap DBというものに変えてみます。

        DB db =
            DBMaker
            .newHeapDB()
            .transactionDisable()
            .make();

        HTreeMap<String, String> map = db.getHashMap("in-heap-hashmap");

        assertThat(map)
            .isInstanceOf(java.util.Map.class);
        assertThat(map)
            .isInstanceOf(java.util.concurrent.ConcurrentMap.class);

        map.put("key1", "value1");
        map.put("key2", "value2");

        assertThat(map.get("key1"))
            .isEqualTo("value1");
        assertThat(map.get("key2"))
            .isEqualTo("value2");
        assertThat(map.get("key3"))
            .isNull();

        db.close();

この部分を除いて、先ほどのコードと同じです。

        DB db =
            DBMaker
            .newHeapDB()
            .transactionDisable()
            .make();

DBMakerで呼び出す起点が、newHeapDBメソッドになりましたね。

Memory DBとHeap DBの違いは?

こう見ると、Memory DBとHeap DBに違いがわかりませんが、シリアライズできないオブジェクトを放り込むと違いが出てきます。

        class Person {
            String name;
            Person(String name) { this.name = name; }
        }

        DB db1 =
            DBMaker
            .newMemoryDB()
            .transactionDisable()
            .make();

        DB db2 =
            DBMaker
            .newHeapDB()
            .transactionDisable()
            .make();

        HTreeMap<String, Person> map1 = db1.getHashMap("in-memory-hashmap");
        HTreeMap<String, Person> map2 = db2.getHashMap("in-heap-hashmap");

        try {
            map1.put("person1", new Person("Taro"));
            failBecauseExceptionWasNotThrown(java.io.IOError.class);
        } catch (java.io.IOError e) {
            assertThat(e).hasMessageContaining("java.io.NotSerializableException: ");
        }

        map2.put("person1", new Person("Taro"));

        db1.close();
        db2.close();

Memory DBの方は、シリアライズできないオブジェクトを登録できません。ということは、シリアライズが発生するということですね。

ですので、Serializableを実装したクラスを作成して

    static class Person implements java.io.Serializable {
            String name;
            Person(String name) { this.name = name; }
    }

こちらを使えば、Memory DBでもうまく動くようになります。

        DB db1 =
            DBMaker
            .newMemoryDB()
            .transactionDisable()
            .make();

        DB db2 =
            DBMaker
            .newHeapDB()
            .transactionDisable()
            .make();

        HTreeMap<String, Person> map1 = db1.getHashMap("in-memory-hashmap");
        HTreeMap<String, Person> map2 = db2.getHashMap("in-heap-hashmap");

        map1.put("person1", new Person("Taro"));
        map2.put("person1", new Person("Taro"));

        db1.close();
        db2.close();
Off-Heapを使う

データの保存先が、NIOのByteBuffer#allocateDirectで確保した先になります。切り替え方は、newMemoryDirectDBメソッドを使うことだけです。

        DB db =
            DBMaker
            .newMemoryDirectDB()
            .transactionDisable()
            .make();

        HTreeMap<String, String> map = db.getHashMap("off-heap-hashmap");

        map.put("key1", "value1");
        assertThat(map.get("key1"))
            .isEqualTo("value1");

        db.close();
ファイルを使う

今度は、保存先をファイルに。

        java.io.File file = new java.io.File("store/test.db");

        DB db =
            DBMaker
            .newFileDB(file)
            .closeOnJvmShutdown()
            .transactionDisable()
            .make();

        HTreeMap<String, String> map = db.getHashMap("file-hashmap");

        map.put("key1", "value1");
        assertThat(map.get("key1"))
            .isEqualTo("value1");

        db.close();

closeOnJvmShutdownを入れることで、JavaVM終了時にクローズ処理が入るようです。なお、一時ファイルを使用することもできます。

HTreeMap以外を使う

DBから呼び出す各種getメソッドを使うことで、HashMap、TreeMap、TreeSet、Queue、Stackを切り替えることができます。

        DB db =
            DBMaker
            .newMemoryDB()
            .transactionDisable()
            .make();

        HTreeMap<String, String> hashMap = db.getHashMap("in-memory-hashmap");
        java.util.Set<String> hashSet = db.getHashSet("in-memory-hashset");
        BTreeMap<String, String> treeMap = db.getTreeMap("in-memory-treemap");
        java.util.NavigableSet<String> treeSet = db.getTreeSet("in-memory-treeset");
        java.util.concurrent.BlockingQueue<String> queue = db.getQueue("in-memory-queue");
        java.util.concurrent.BlockingQueue<String> stack = db.getStack("in-memory-stack");

        db.close();
エントリの有効期限を設定する

エントリに対しての書き込み、アクセスに対応する有効期限の設定が可能です。

        DB db =
            DBMaker
            .newMemoryDB()
            .transactionDisable()
            .make();

        HTreeMap<String, String> map =
            db
            .createHashMap("in-memory-hashmap")
            .expireAfterAccess(3, java.util.concurrent.TimeUnit.SECONDS)
            .expireAfterWrite(3, java.util.concurrent.TimeUnit.SECONDS)
            .make();

        map.put("key1", "value1");
        map.put("key2", "value2");

        java.util.concurrent.TimeUnit.SECONDS.sleep(2);

        map.get("key1");

        java.util.concurrent.TimeUnit.SECONDS.sleep(2);

        assertThat(map.get("key1"))
            .isEqualTo("value1");
        assertThat(map.get("key2"))
            .isNull();

        java.util.concurrent.TimeUnit.SECONDS.sleep(4);

        assertThat(map.get("key1"))
            .isNull();

        db.close();

DBからコレクションを取得する際に、get〜ではなく、create系のメソッドでMakerクラスを取得して設定を行います(ここでは、HTreeMapMaker)。

        HTreeMap<String, String> map =
            db
            .createHashMap("in-memory-hashmap")
            .expireAfterAccess(3, java.util.concurrent.TimeUnit.SECONDS)
            .expireAfterWrite(3, java.util.concurrent.TimeUnit.SECONDS)
            .make();

この例では、書き込み後の有効期限、アクセスしての有効期限をそれぞれ3秒としています。

トランザクションやキャッシュについては、今回はパスします。

あとは性能は…どうなんでしょうね。以前はベンチマークが公開されていましたが、今は見れなくなっています。十分速そうなら、軽量キャッシュとして選択するのもありかと思うのですが…。