DB分離レベル: スナップショット vs シリアライザブル

このページは自分がデータベース分離レベルを調べた際のメモを記事にしたものです。ここではスナップショット分離レベルとシリアライザブル分離レベルの違いにフォーカスしています。

ライトスキュー異常

スナップショット分離レベルとシリアライザブルな分離レベルを比較する時によく挙げられるのはライトスキュー異常と言われる現象で、スナップショット分離レベルモデルでは原理的に対応しないシナリオになる。これはスナップショット分離の衝突検知が同じレコードへの変更のみを対象にし、コミット時系列の並びを対象にしないことでパフォーマンスを出す仕様に起因する。よく使われる例は以下の通り。

シリアライズ可能な分離レベルでは、時系列でのトランザクションの単一性を保証するため、T1もしくはT2のどちらか(先に始めた方)を成功させ、もう一つのトランザクションは、ロールバック+リトライや先のトランザクションが終わるまでブロック後のリトライ、もしくはトランザクションを失敗させる。この例はread-write conflictと言われるタイプの衝突になる。

勿論実装者がロックを適用するのもこの問題のメジャーな解決策の一つ。例えばSELECT FOR UPDATEは実行中のトランザクションが読み込んだデータを、トランザクションが終わるまで他のトランザクションから読めないようにロックする。上記の例ではT1がドクターAとドクターBの当直データを読んだ時点でT2からはそのデータをT1が終わるまで読むことができなくなる。これによりT2の読み込みが完了した時点でT1はコミット済みになり、T2はドクターAが当直をキャンセルしたことが読めるので、ドクターBの当直キャンセルの是非を判断することが可能となる。メジャーなDBでは大体READ COMMITTEDREPEATABLE READがデフォルトの分離レベルなので、トランザクション実装にロックを考えずに書くとライトスキュー異常の可能性は排除できない。現実的な影響は以下の段落へ。

現実的な影響

Wikipediaの例では、銀行のトランザクションが挙げられており、顧客が所有している複数の口座全てからほぼ同時に引き出しトランザクションを行うことで、残高より多い金額の引き出しが可能となるケースが説明されている。条件として顧客が複数の口座を所有している場合、残高のトータルが0以上であれば、特定の口座残高のマイナス(負債状態)が許される状態ではあるが、引き出し時のユーザー体験的に見た場合現実に十分あり得る設定と考えられる。口座所持者がそれぞれ残高$100の口座に$200を引き出すトランザクションを同時に発生させることで、SELECT FOR UPDATEなどの対策が無い場合、スナップショット分離レベルのDBは両トランザクションを成功させてしまい、トータル残高は-$200となってしまう。

一見これらの例は理論的な話に見えるかも知れないが実際に攻撃手法として使用されている例が報告されている。Stanford InfoLabのメンバーによる論文:「ACIDRain: Concurrency-Related Attacks on Database-Backed Web Applications」によるとビットコイン交換所が倒産する程のデータ崩壊やEコマースサイトへのギフトカードの超過額使用、商品インベントリデータの崩壊などの確認が報告されている。