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();
もっとサンプルを
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秒としています。
トランザクションやキャッシュについては、今回はパスします。
あとは性能は…どうなんでしょうね。以前はベンチマークが公開されていましたが、今は見れなくなっています。十分速そうなら、軽量キャッシュとして選択するのもありかと思うのですが…。