mapdb導入
先日に「clojure向けの組み込みデータベースの選定」を行い、「mapdbを使う」という結論となった。そして「2016年2月後期分の試作ゲームを公開」にて実際に使ってみた。そのまとめ。
まとめ
- 非常に使いやすい。これはおすすめ。もっと使われるべき。
- ライセンスはapache2
- 現在も開発/メンテが続いている。
- 並行処理の隔離を自分で適切に行う必要のあるタイプ。
- パフォーマンスは未測定。しかしそもそもパフォーマンスを求めるタイプのものではない。
- まだ運用期間が短い為、耐障害性についても不明。こちらはかなり気になる…。
セットアップ
mavenに登録されているので、セットアップは
:dependencies
に[org.mapdb/mapdb "X.Y.Z"]
を追加するだけでよいバージョン選択
- 現時点では、安定版である
1.0.9
と、現在も開発が進んでいる3.0.0-M2
とがあった- どっちにするか非常に迷うが、パフォーマンスや機能の充実よりも不具合の少なさを期待し、今回は
1.0.9
を選択した。
- どっちにするか非常に迷うが、パフォーマンスや機能の充実よりも不具合の少なさを期待し、今回は
- 現時点では、安定版である
使い方
最初は文章で説明を書いていたが、分かりづらすぎたのでコードにする事にした。省略した部分もあるが大体こんな感じ
(ns hoge.db (:refer-clojure :exclude [get set!]) (:require [clojure.java.io :as io] [clojure.edn :as edn]) (:import [org.mapdb DB DBMaker] [java.util Map])) (defonce the-locker (Object.)) (defmacro with-lock [& bodies] `(locking the-locker ~@bodies)) (defonce db-entity (atom nil)) (defonce ^:dynamic ^DB db nil) (defmacro with-db [& bodies] `(binding [db @db-entity] (when db ~@bodies))) ;;; サイズを小さくする為に、core.async等で定期的に実行する必要がある ;;; (postgresでのvacuum相当だと思われる) (defn compact! [] (with-db (.compact db))) ;;; 使う前にこれを呼ぶ (defn init! [path & [password]] (with-lock (when-not @db-entity (let [db-maker (DBMaker/newFileDB (io/file path))] (.closeOnJvmShutdown db-maker) (.compressionEnable db-maker) ;(.transactionDisable db-maker) ; transactionは必須(後述) (when password (.encryptionEnable db-maker password)) (let [db (.make db-maker)] (reset! db-entity db)))))) ;;; 使い終わったらこれを呼ぶ ;;; (サーバ用途等で、プロセス終了時までずっと使い続ける場合は、 ;;; .closeOnJvmShutdown しているので、呼ばなくてよい) (defn term! [] (with-lock (with-db (when-not (.isClosed db) (try (.close db) (catch Throwable e nil))) (reset! db-entity nil)))) ;;; テーブル取得。実際の利用時には適切にキャッシュすべき (defn get-table [k] (with-lock (with-db (.getHashMap db (name k))))) (defn get [^Map table k] (with-lock (with-db ;; TODO: 実際はリードエラー対応等を入れる事 (edn/read-string (.get table (name k)))))) (defn set! [^Map table k v] (with-lock (with-db ;; TODO: 実際はシリアライズ不可なものが含まれてないか等の確認が必要 (.put table (name k) (pr-str v)) (.commit db) ; ※後述 ))) (defn update! [^Map table k f] (with-lock (with-db ;; TODO: getおよびset!と同じ問題に注意 (let [old-v (edn/read-string (.get table (name k))) new-v (f old-v)] (.put table (name k) (pr-str v)) (.commit db) ; ※後述 new-v)))) (comment (require '[hoge.db :as db]) (db/init! "path/to/db.dat") (def table (db/get-table :table-name)) (db/get table :hoge) ; => nil (db/set! table :hoge [1 :a 2]) (db/get table :hoge) ; => [1 :a 2] (db/set! table :hoge 3) (db/update! table :hoge inc) (db/get table :hoge) ; => 4 ;; 他スレッドとの競合(不整合)が起こらない事を保証しつつ、 ;; 二つのエントリを参照した結果をテーブルに保存する (db/with-lock (let [a1 (or (db/get table :fuga) 0) a2 (or (db/get table :hoge) 0) result (+ a1 a2)] (db/set! table :fuga result))))
解説
重要な事は大体 http://www.mapdb.org/doc/getting-started.html に書いてある。これだけ読めば基本的には問題はない
(DBMaker/newFileDB (io/file path))
で、DBMaker
のインスタンスを生成。引数は保存/読み込みを行うファイル。- 引数に指定したファイルが
hoge.dat
だった場合、hoge.dat
hoge.dat.p
hoge.dat.t
の三つのファイルが作られる。よく分からないが、そういうもののようだ。 - 「一つのデータファイルを開くのは一つのプロセスに限る事。複数のプロセスから同時に開いたらcorruptする」と上記urlに書いてある。なので、呼び出し元でこの事を保証しておいた方がよい(多重起動防止コードを入れる等)
- 引数に指定したファイルが
上記の
DBMaker
のインスタンスには、.closeOnJvmShutdown
.compressionEnable
等の各種のオプションが設定できる。詳細は http://www.mapdb.org/apidocs/org/mapdb/DBMaker.html#method_summary あたりを参照だが、重要なもののみちょっと解説しておく.closeOnJvmShutdown
: JVMが終了する際に.close
を呼ぶようにする。これの実装は.addShutdownHookなので、終了方法によっては実行されないケースがあるが、そういう場合は他の方法でもどうしようもないと思う。- 何回か試した限りでは、このcloseが実行されずに終了した場合でも、データベースファイルが破損するという事はないようだった(某dbmはすぐ破損する事で有名だった…)。
.transactionDisable
: これは基本的には設定すべきではない。これを設定するとパフォーマンスが向上する代償として、「二つのテーブルに同時に変更を行い、コミットする」ような操作の途中で、何らかの要因によって処理が中断した際に「片方のテーブルにだけ変更が反映されてしまっている」状況が起こりえてしまうようになる。これを設定しなければ、とりあえずそのような状況は起こらない(コミット前かコミット後かのどちらかになる事を保証する)、との事。- 上記コード例では
set!
update!
内に.commit
を埋め込んでしまっているので意味がないが、サンプルコードという事で勘弁してください
- 上記コード例では
(.make db-maker-instance)
で、上記の各種の設定を行ったDBMaker
のインスタンスから、DB
のインスタンスを生成する。このDB
のインスタンスがいわゆるRDBMSでの「データベース」に相当する。(.getHashMap db-instance "table-name")
で、java.util.Map
のインターフェースを持つ、いわゆるRDBMSでの「テーブル」に相当するインスタンスを生成できる。- このインスタンス生成時に型やシリアライザとかの設定を行う事で、特定のオブジェクトを直接保存できるようになる(いわゆるテーブル内のカラムの型指定に相当)。が、clojureから扱う場合はedn等を使って、自前で文字列にシリアライズして保存/読み込みするようにした方が手軽だと思う。ednな値ならなんでもつっこめる。
- それでもシリアライザ指定とかしたい場合は http://stackoverflow.com/questions/28719945/using-clojures-data-structure-with-mapdb や http://d.hatena.ne.jp/Kazuhira/20150201/1422795499 を参照
- テーブル内のエントリの有効期限設定についても http://d.hatena.ne.jp/Kazuhira/20150201/1422795499 を参照
- インターフェースが
java.util.Map
なので、.keySet
を取ったりも普通にできる
- このインスタンス生成時に型やシリアライザとかの設定を行う事で、特定のオブジェクトを直接保存できるようになる(いわゆるテーブル内のカラムの型指定に相当)。が、clojureから扱う場合はedn等を使って、自前で文字列にシリアライズして保存/読み込みするようにした方が手軽だと思う。ednな値ならなんでもつっこめる。
注意点
いわゆる「真のトランザクション」をmapdbは提供していないので、自前で適切に並行処理を隔離する必要がある。
定期的に
.compact
を実行する必要がある。- 昔のpostgresのvacuumが、データ量によってはものすごい時間がかかってメンテが終わらないという悪夢を起こしていた事で悪名高いが、これもそうなのかは不明…。しかしもしそうだったとしても実行しない訳には…。
- データ量が少ない内は、core.asyncとかで適当に一時間に一回実行するとかだけでいいと思う