最近遊んだJavaライブラリがRocksDBのJNIバインディングを使っているのを見て、けっこう簡単に導入できるの?という感じを期待して
試してみることにしました。
RocksDBとは?
そもそも、RocksDBについて。
RocksDB | A persistent key-value store | RocksDB
Facebook、GoogleのLevelDBを採用したキーバリュー型ストア「RocksDB」公開。マルチコアと高速ストレージに最適化 - Publickey
Facebook、Key-Valueストア「RocksDB」をオープンソース化:C++ライブラリとして構築 - @IT
GoogleのLevelDBを使い、CPUコア多数持つサーバーでのスケーラビリティ、高速なストレージの効率的な利用、IOバウンド、インメモリなどのワークロードを
サポートしたKey Value Storeだそうです。
そう、Key Value Store。
なので、オペレーション的にはKey&Valueなものが目立ちます。
ドキュメントは、GitHubのWikiを見るのが良さそうです。
Home · facebook/rocksdb Wiki · GitHub
基本的な操作はこちら。
RocksDB JNI
RocksDB自体はC++で書かれているのですが、これのJava向けのAPI(JNI使用)が提供されています。
https://github.com/facebook/rocksdb/tree/v5.9.2/java
RocksJava Basics · facebook/rocksdb Wiki · GitHub
あまり詳細なサンプルなどはないようですが、雰囲気を見つつ書いていってみます。
使ってみる
Maven依存関係。
<dependency> <groupId>org.rocksdb</groupId> <artifactId>rocksdbjni</artifactId> <version>5.9.2</version> </dependency>
あと、テストライブラリも用意。
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.0.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.9.0</version> </dependency>
テストコードの雛形はこちら。
src/test/java/org/littlewings/rocksdbjni/RocksDBJniTest/RocksDBJniTest.java
package org.littlewings.rocksdbjni.RocksDBJniTest; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; import org.rocksdb.ColumnFamilyDescriptor; import org.rocksdb.ColumnFamilyHandle; import org.rocksdb.ColumnFamilyOptions; import org.rocksdb.DBOptions; import org.rocksdb.Options; import org.rocksdb.RocksDB; import org.rocksdb.RocksDBException; import static org.assertj.core.api.Assertions.assertThat; public class RocksDBJniTest { // ここに、テストを書く! }
初めてのRocksDB
Wikiを見つつ、書いてみたのがこちら。
@Test public void gettingStarted() { // RocksDB.loadLibrary(); try (Options options = new Options().setCreateIfMissing(true); RocksDB rocks = RocksDB.open(options, "sample-db")) { // put rocks.put("key".getBytes(StandardCharsets.UTF_8), "value".getBytes(StandardCharsets.UTF_8)); // get assertThat(rocks.get("key".getBytes(StandardCharsets.UTF_8))).isEqualTo("value".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get("missing".getBytes(StandardCharsets.UTF_8))).isNull(); // get(配列受け取り) byte[] valueBytes = new byte[5]; assertThat(rocks.get("key".getBytes(StandardCharsets.UTF_8), valueBytes)) .isEqualTo(5); assertThat(valueBytes).isEqualTo("value".getBytes(StandardCharsets.UTF_8)); // exist StringBuilder builder = new StringBuilder(); assertThat(rocks.keyMayExist("key".getBytes(StandardCharsets.UTF_8), builder)).isTrue(); assertThat(builder.toString()).isEqualTo("value"); builder = new StringBuilder(); assertThat(rocks.keyMayExist("missing".getBytes(StandardCharsets.UTF_8), builder)).isFalse(); assertThat(builder.toString()).isEmpty(); // delete rocks.delete("key".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get("key".getBytes(StandardCharsets.UTF_8))).isNull(); } catch (RocksDBException e) { throw new RuntimeException(e); } }
RocksDB#loadLibraryはネイティブライブラリのロードのためのものですが、この記述自体はRocksDBのstaticイニシャライザーにも実装されていますし、
その他の各種クラス利用時にも呼び出されていたりします。
RocksDB.loadLibrary();
https://github.com/facebook/rocksdb/blob/v5.9.2/java/src/main/java/org/rocksdb/RocksDB.java#L34-L36
https://github.com/facebook/rocksdb/blob/v5.9.2/java/src/main/java/org/rocksdb/Options.java#L24-L26
なので、明示的な呼び出しは不要?ライブラリロード先のパスをデフォルト以外のものにする場合は、パスを指定可能なオーバーロードされたバージョンを
使用するとよいです。
なお、このRockdsDBのJARに含まれているネイティブライブラリは、こちらになります。
$ jar -tvf ~/.m2/repository/org/rocksdb/rocksdbjni/5.9.2/rocksdbjni-5.9.2.jar | grep -E 'dll|so' 8304735 Tue Dec 19 10:26:10 JST 2017 librocksdbjni-linux32.so 8162410 Tue Dec 19 10:14:02 JST 2017 librocksdbjni-linux64.so 9137088 Wed Jan 03 11:58:08 JST 2018 librocksdbjni-linux-ppc64le.so 6688256 Wed Jan 03 19:46:16 JST 2018 librocksdbjni-win64.dll
RocksDBインスタンスの取得は、Optionを作成後にRocksDB#openで行います。
try (Options options = new Options().setCreateIfMissing(true); RocksDB rocks = RocksDB.open(options, "sample-db")) {
RocksDB#openに渡すパスは、RocksDBのファイルを配置するためのディレクトリです。
今回の指定だと、カレントディレクトリにこういうのができあがります。
$ find sample-db -type f sample-db/LOG sample-db/MANIFEST-000001 sample-db/CURRENT sample-db/OPTIONS-000005 sample-db/LOCK sample-db/000003.log sample-db/IDENTITY
WikiにはOptions#setCreateIfMissingの指定は書いてありますが、Options指定なしのRocksDB#openを呼び出すと、暗黙的に指定してくれていたりします。
https://github.com/facebook/rocksdb/blob/v5.9.2/java/src/main/java/org/rocksdb/RocksDB.java#L155-L161
操作は、キーも値もbyte配列で行います。
// put rocks.put("key".getBytes(StandardCharsets.UTF_8), "value".getBytes(StandardCharsets.UTF_8)); // get assertThat(rocks.get("key".getBytes(StandardCharsets.UTF_8))).isEqualTo("value".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get("missing".getBytes(StandardCharsets.UTF_8))).isNull();
get時に、byte配列を渡してその中身を詰めてもらうというやり方もできます。
// get(配列受け取り) byte[] valueBytes = new byte[5]; assertThat(rocks.get("key".getBytes(StandardCharsets.UTF_8), valueBytes)) .isEqualTo(5); assertThat(valueBytes).isEqualTo("value".getBytes(StandardCharsets.UTF_8));
キーの存在確認…の割に、値も同時に取れたりします。しかもStringBuilderで。
// exist StringBuilder builder = new StringBuilder(); assertThat(rocks.keyMayExist("key".getBytes(StandardCharsets.UTF_8), builder)).isTrue(); assertThat(builder.toString()).isEqualTo("value"); builder = new StringBuilder(); assertThat(rocks.keyMayExist("missing".getBytes(StandardCharsets.UTF_8), builder)).isFalse(); assertThat(builder.toString()).isEmpty();
delete。
// delete rocks.delete("key".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get("key".getBytes(StandardCharsets.UTF_8))).isNull();
そして、最後にcloseします。今回は、try-with-resources文でクローズしています。
Memory Management
Wikiにも記載がありますが、AbstractNativeReference(RocksDBなどの親クラス)#finalizeでもcloseを呼び出すように実装されてはいますが、
それに頼るべきでない旨も合わせて書いてあります。
実際、deprecateみたいですし、そもそもちゃんとcloseすべきですよね。
https://github.com/facebook/rocksdb/blob/v5.9.2/java/src/main/java/org/rocksdb/AbstractNativeReference.java#L67-L75
その他、merge、multiGet、deleteRange、iterator、snapshotなどなどあるようですが、今回はパス…。
Column Family
WikiのJava APIの部分にちょっとだけ載っていた割には、サンプルも特になかったのでチャレンジという意味でColumn Familyも触ってみることにしました。
RocksJava Basics · facebook/rocksdb Wiki · GitHub
Column Familyを使うと、類似のキー、値をグループ化して管理することができます。
Column Families · facebook/rocksdb Wiki · GitHub
Column Familyを指定せずにアクセスした場合は、「デフォルトの」Column Familyとして扱われているようです。
では、Javaコードで書いてみましょう。
@Test public void withColumnFamily() { try (ColumnFamilyOptions columnFamilyOptions = new ColumnFamilyOptions().optimizeUniversalStyleCompaction()) { List<ColumnFamilyDescriptor> columnFamilyDescriptors = Arrays.asList( new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, columnFamilyOptions), new ColumnFamilyDescriptor("column-family1".getBytes(), columnFamilyOptions), new ColumnFamilyDescriptor("column-family2".getBytes(), columnFamilyOptions) ); List<ColumnFamilyHandle> columnFamilyHandles = new ArrayList<>(); try (DBOptions options = new DBOptions().setCreateIfMissing(true).setCreateMissingColumnFamilies(true); RocksDB rocks = RocksDB.open(options, "column-family-db", columnFamilyDescriptors, columnFamilyHandles)) { // column handle ColumnFamilyHandle defaultColumnFamilyHandle = columnFamilyHandles.get(0); ColumnFamilyHandle columnFamily1Handle = columnFamilyHandles.get(1); ColumnFamilyHandle columnFamily2Handle = columnFamilyHandles.get(2); // put rocks.put(defaultColumnFamilyHandle, "key".getBytes(StandardCharsets.UTF_8), "value-default".getBytes(StandardCharsets.UTF_8)); rocks.put(columnFamily1Handle, "key".getBytes(StandardCharsets.UTF_8), "value-column-family1".getBytes(StandardCharsets.UTF_8)); rocks.put(columnFamily2Handle, "key".getBytes(StandardCharsets.UTF_8), "value-column-family2".getBytes(StandardCharsets.UTF_8)); // get assertThat(rocks.get(defaultColumnFamilyHandle, "key".getBytes(StandardCharsets.UTF_8))).isEqualTo("value-default".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(columnFamily1Handle, "key".getBytes(StandardCharsets.UTF_8))).isEqualTo("value-column-family1".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(columnFamily2Handle, "key".getBytes(StandardCharsets.UTF_8))).isEqualTo("value-column-family2".getBytes(StandardCharsets.UTF_8)); // delete rocks.delete(defaultColumnFamilyHandle, "key".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(defaultColumnFamilyHandle, "key".getBytes(StandardCharsets.UTF_8))) .isNull(); assertThat(rocks.get(columnFamily1Handle, "key".getBytes(StandardCharsets.UTF_8))) .isEqualTo("value-column-family1".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(columnFamily2Handle, "key".getBytes(StandardCharsets.UTF_8))) .isEqualTo("value-column-family2".getBytes(StandardCharsets.UTF_8)); // default column family rocks.put(defaultColumnFamilyHandle, "key1".getBytes(StandardCharsets.UTF_8), "value1".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get("key1".getBytes(StandardCharsets.UTF_8))).isEqualTo("value1".getBytes(StandardCharsets.UTF_8)); rocks.put("key2".getBytes(StandardCharsets.UTF_8), "value2".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(defaultColumnFamilyHandle, "key2".getBytes(StandardCharsets.UTF_8))).isEqualTo("value2".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get("key2".getBytes(StandardCharsets.UTF_8))).isEqualTo("value2".getBytes(StandardCharsets.UTF_8)); } catch (RocksDBException e) { throw new RuntimeException(e); } } }
Column Familyを使用する場合は、まずColumn Familyの定義を行うところから始めます。
try (ColumnFamilyOptions columnFamilyOptions = new ColumnFamilyOptions().optimizeUniversalStyleCompaction()) { List<ColumnFamilyDescriptor> columnFamilyDescriptors = Arrays.asList( new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, columnFamilyOptions), new ColumnFamilyDescriptor("column-family1".getBytes(), columnFamilyOptions), new ColumnFamilyDescriptor("column-family2".getBytes(), columnFamilyOptions) );
今回は3カラムを用意。ここでも、Column Family名の指定はbyte配列となります。
RocksDB#DEFAULT_COLUMN_FAMILYというのは、「default」という名前のColumn Familyです。
https://github.com/facebook/rocksdb/blob/v5.9.2/java/src/main/java/org/rocksdb/RocksDB.java#L22
空のListを用意していますが、これはRocksDB#open時に中身を詰めてもらうことになります。
List<ColumnFamilyHandle> columnFamilyHandles = new ArrayList<>();
Column Familyを使用する場合、RocksDB#open時にはDBOptionsを渡す必要があります。この時に、Column Familyの定義(Descriptor)と先ほど作成した
空のListを渡しておきます。
try (DBOptions options = new DBOptions().setCreateIfMissing(true).setCreateMissingColumnFamilies(true); RocksDB rocks = RocksDB.open(options, "column-family-db", columnFamilyDescriptors, columnFamilyHandles)) {
すると、RocksDBのインスタンスが取得できるとともに、空のListにColumn FamilyのHandleが詰められています。
// column handle ColumnFamilyHandle defaultColumnFamilyHandle = columnFamilyHandles.get(0); ColumnFamilyHandle columnFamily1Handle = columnFamilyHandles.get(1); ColumnFamilyHandle columnFamily2Handle = columnFamilyHandles.get(2);
Handleという変数名でRocksDB中のソースコード中も出てくるのですが、なんかC系っぽいですね。
以降は、このHandleを使って操作を行います。
put、get時に、このHandleを第1引数に指定してアクセスを行います。
// put rocks.put(defaultColumnFamilyHandle, "key".getBytes(StandardCharsets.UTF_8), "value-default".getBytes(StandardCharsets.UTF_8)); rocks.put(columnFamily1Handle, "key".getBytes(StandardCharsets.UTF_8), "value-column-family1".getBytes(StandardCharsets.UTF_8)); rocks.put(columnFamily2Handle, "key".getBytes(StandardCharsets.UTF_8), "value-column-family2".getBytes(StandardCharsets.UTF_8)); // get assertThat(rocks.get(defaultColumnFamilyHandle, "key".getBytes(StandardCharsets.UTF_8))).isEqualTo("value-default".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(columnFamily1Handle, "key".getBytes(StandardCharsets.UTF_8))).isEqualTo("value-column-family1".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(columnFamily2Handle, "key".getBytes(StandardCharsets.UTF_8))).isEqualTo("value-column-family2".getBytes(StandardCharsets.UTF_8));
Column Familyを指定する場合は、同じキーであってもColumn Family単位で管理されることになるため、別々に扱われることになります。
なので、あるColumn Familyに紐づく値を削除しても、他のColumn Familyは特に変わりません。
// delete rocks.delete(defaultColumnFamilyHandle, "key".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(defaultColumnFamilyHandle, "key".getBytes(StandardCharsets.UTF_8))) .isNull(); assertThat(rocks.get(columnFamily1Handle, "key".getBytes(StandardCharsets.UTF_8))) .isEqualTo("value-column-family1".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(columnFamily2Handle, "key".getBytes(StandardCharsets.UTF_8))) .isEqualTo("value-column-family2".getBytes(StandardCharsets.UTF_8));
また、Column Familyを指定せずに単純にput、getなどを行った場合は、「default」のColumn Familyを使用していると見なされます。
// default column family rocks.put(defaultColumnFamilyHandle, "key1".getBytes(StandardCharsets.UTF_8), "value1".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get("key1".getBytes(StandardCharsets.UTF_8))).isEqualTo("value1".getBytes(StandardCharsets.UTF_8)); rocks.put("key2".getBytes(StandardCharsets.UTF_8), "value2".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get(defaultColumnFamilyHandle, "key2".getBytes(StandardCharsets.UTF_8))).isEqualTo("value2".getBytes(StandardCharsets.UTF_8)); assertThat(rocks.get("key2".getBytes(StandardCharsets.UTF_8))).isEqualTo("value2".getBytes(StandardCharsets.UTF_8));
なんとなく、雰囲気はわかりましたね。
まとめ
RocksDBのJNIバイディングを試してみました。
RocksDB自体、初めて使ったのですが、けっこう簡単に使えて良かったですね。データベースとまではいかなくても、永続化したい&Key Value Store的なもので
アクセスできればよいくらいのものについては、使ってみてもいいかなと思います。