トランザクションが存在しないDBにてその弱点をどのようにカバーするか
by @dekokun on 2013/09/30 23:30
Tagged as: NoSQL.
NoSQL系のDBをメインDBとして使用する場合、最もネックになってくるのはトランザクション周りかと思います。 DBとしてトランザクション機能が提供されていない中でその弱点をどのようにソフトウェアでカバーするかをまとめます。
なお、以下は机上の空論である部分が多く、これから知見を得る度に追記していこうかと思います。
なお、基本的にMongoDB, DynamoDBあたりのドキュメント指向DBを想定した記事です。完全なるKVS系(memcachedとか)では、以下記事があてはまらない場合もあるかと思います。
カバーすべきトランザクションの性質
トランザクションの持つべき性質として、「ACID」と呼ばれる以下4つの性質があります。
参考:ACID (コンピュータ科学) - Wikipedia
原子性
- トランザクションに含まれるタスクが全て実行されるか全く実行されないかを保証する性質
- 「書き込み失敗したりしたらロールバックすればデータが中途半端に残らなくていいやー」というアレ。これがあると極めて楽ですね。
一貫性
- 常にデータベースの整合性が保たれていることを保証する性質
- これについてはイマイチ私はよくわかってないのですが、トランザクションの前後で外部制約やユニーク制約などが満たされているという認識でいいのかな。
独立性
- トランザクション中に行われる過程が他の操作から隠蔽されること
- 「デッドロック以外は、他のスレッドからどのようにDB更新されるかあまり気にしなくていいよ〜」という感じ。楽ですね。まぁ、実際に左記のような態度で開発に望むと色々と大変なことになりますが。ロック待ちから開放されないとか、ファントム・リードが発生しておかしなことになるとか。
- たいていのDBは性能とのトレードオフから完全な独立性は保証しない設定になっている
- トランザクション分離レベルの変更によってどの程度独立性を保つか規定が可能
- ちなみに最近知ったのだが、MySQLとPostgreSQL及びOracleでデフォルトの分離レベルが異なる。MySQLはデフォルトがREPEATABLE READであるのに対して他の2つはREAD COMMITTED
永続性
- トランザクションが完了した操作は、永続的になることが確定していること
- 一度トランザクションが完了したら、旧にDBが落ちたりした後に復旧させたりしても対象の操作をした結果はDBに反映されている状態となっているものですね。便利ですね。
- たいていのRDBMSは、トランザクション完了時のトランザクションログ(MySQLでいうところのバイナリログ)への書き込みとロールフォワードでこれを実現していますね。
トランザクションをソフトウェアでどのように(ある程度)実現するか
今回は、上記の機能をソフトウェアでどのように実現するかを考えていきます。
なお、以下の文章はどのDBについてなのか規定していませんが、RDBMSでいうところの「テーブル、レコード」(ドキュメント系のDBでは「コレクション、ドキュメント」など)のことは、一律「テーブル」、「レコード」と呼ぶことにします。
原子性の対策
どの操作が成功したか、失敗したかを記録し、失敗した操作が1つでもあった場合は他の成功した操作を巻き戻す
- 王道ですね。
- 弱点
- 実装が複雑
- 「2をたす」のを巻き戻す場合は「2を引く」でいいと思いますが、例えば「名前を太郎から二郎に変更」などを行った場合、巻き戻す場合に「二郎」にしていいかどうかというのは議論がわかれるところ。別スレッドが「太郎から三郎に変更」など行っていた場合は巻き戻しによってその変更が消えてしまうことに
- 以下に全体的に言える話として、「」
操作の冪等性を担保しながら失敗した際は再実行する
- Version Numberパターンなどの、「そのレコードが、特定の条件を満たせば更新(以前読み込んだデータとバージョン番号が同じだったら更新)」というような楽観的ロックを使用して操作の冪等性を担保した形で1つ1つの操作を規定し、まとまった操作の中の1つでも失敗したら全ての操作をもう一度再実行する
- 弱点
- 「(主キー以外の)条件を指定して更新を行う」ことができないDBでは実施できない
- 常に失敗するような操作が操作のまとまりの中に存在すると、下手すりゃ無限ループになる。かといって「◯◯回やっても失敗したら再実行をやめる」とすると、中途半端に操作されたままの状態になってしまうし…
- Version Numberパターンを使用した場合、他のスレッドなどから対象のレコードを操作されるてバージョンが変化すると、対象の行が更新できずに、整合性が失われる
1トランザクションが1行の更新のみになるように設計する
- これならそもそも1回の更新が成功するか失敗するかのみであるので、原子性を考える必要がない
- 弱点
- 1つのレコードの更新がアトミックにできないDBでは実施できない
- そもそも、全てのデータ更新を、1行の更新だけにすることは不可能
トランザクションテーブルを使用した多相コミット
- トランザクション用のテーブルを使用して、まぁ頑張る方法。
- 詳細な方法は複雑なので、まぁ「多相コミット」とかで検索してください
- なんらかの原因により処理が途中で終わってしまっても、「どの処理が完了していないか」「その処理の中で完了していないのはどのレコードか」というのが分かる
- 弱点
- 1つのレコードの更新がアトミックにできないDBでは実施できない
- 実装が複雑
一貫性の対策
要検討。「ソフトウェア側でバリデーションを行う」くらいですか?
独立性の対策
楽観ロック
- Version Numberパターンなどを使用した楽観ロックを使用し、書き込み件数が0件だったらバージョン番号の取得部分から再実行(もしくは書き込み失敗としてエラーを返す)
- 弱点
- 「(主キー以外の)条件を指定して更新を行う」ことができないDBでは実施できない
- 書き込みが何件行われたかが分からないDBでは実施できない
永続性の対策
こちらに関しては、完全にDBの機能に依存しますかねぇ。自前でディスクに書き込んでいくという手はあるかもしれないが…性能面でも実装面でも難しいでしょうね。そんなことするくらいなら素直にRDBMS使うべきですね。
トランザクションログ的な機能を有効にする
- そういう機能があるなら有効にしましょう
- 弱点
- トランザクションログ的な機能がないDBでは実施できない
- 自動フェイルオーバー機能を備えて複数マスターになりうるようなDBの場合、1台が落ちたら他のDBがマスターになり、新しいマスターに反映されていないデータは消失する
書き込みの際に、「ディスクに書き込まれたことを保証する」ようなオプションをつける
- そういう機能があるなら有効にしましょう
- 弱点
- そのような機能がないDBでは実施できない
- 自動フェイルオーバー機能を備えて複数マスターになりうるようなDBの場合、1台が落ちたら他のDBがマスターになり、新しいマスターに反映されていないデータは消失する
- RiakやMongoDBでは、スレーブに書き込まれたことを保証する書き込みもありましたね
まとめ
- 基本的にはトランザクションを考えなくてもいい部分と必要な部分に分け、必要な部分だけ上記対策を施しましょう
- 上記を全部実施するくらいならRDBMS使ったほうが幸せなのではないかなとは思います
- トランザクション及び高可用性が必要だったら、MySQL Clusterを使うとか(MySQL Clusterのこと、全然詳しくないですが)