DB分離レベル: スナップショット vs シリアライザブル
このページは自分がデータベース分離レベルを調べた際のメモを記事にしたものです。ここではスナップショット分離レベルとシリアライザブル分離レベルの違いにフォーカスしています。
ライトスキュー異常
スナップショット分離レベルとシリアライザブルな分離レベルを比較する時によく挙げられるのはライトスキュー異常と言われる現象で、スナップショット分離レベルモデルでは原理的に対応しないシナリオになる。これはスナップショット分離の衝突検知が同じレコードへの変更のみを対象にし、コミット時系列の並びを対象にしないことでパフォーマンスを出す仕様に起因する。よく使われる例は以下の通り。
-
当直のドクターを更新するトランザクション
-
システムは当直のドクターが最低でも一人いることを保証する必要がある。
-
T1 (トランザクション1)は他に当直のドクターがいればドクターAの当直を取り消したい。
-
T2は他に当直のドクターがいればドクターBの当直を取り消したい。
-
T1とT2が時系列上オーバラップするタイミングで起きる。
トランザクション動作:
-
T1はDBのスナップショットからドクターA、ドクターB両方が当直であることを読む。
-
T2はDBのスナップショットからドクターA、ドクターB両方が当直であることを読む。
-
T1はドクターAの当直を消す更新をする。(成功する。)
-
T2はドクターBの当直を消す更新をする。(成功する。)
-
結果、システムの必要条項である最低でも一人の当直ドクターが保証されていない事態になる。
-
シリアライズ可能な分離レベルでは、時系列でのトランザクションの単一性を保証するため、T1もしくはT2のどちらか(先に始めた方)を成功させ、もう一つのトランザクションは、ロールバック+リトライや先のトランザクションが終わるまでブロック後のリトライ、もしくはトランザクションを失敗させる。この例はread-write conflictと言われるタイプの衝突になる。
勿論実装者がロックを適用するのもこの問題のメジャーな解決策の一つ。例えばSELECT FOR UPDATE
は実行中のトランザクションが読み込んだデータを、トランザクションが終わるまで他のトランザクションから読めないようにロックする。上記の例ではT1がドクターAとドクターBの当直データを読んだ時点でT2からはそのデータをT1が終わるまで読むことができなくなる。これによりT2の読み込みが完了した時点でT1はコミット済みになり、T2はドクターAが当直をキャンセルしたことが読めるので、ドクターBの当直キャンセルの是非を判断することが可能となる。メジャーなDBでは大体READ COMMITTED
かREPEATABLE READ
がデフォルトの分離レベルなので、トランザクション実装にロックを考えずに書くとライトスキュー異常の可能性は排除できない。現実的な影響は以下の段落へ。
現実的な影響
Wikipediaの例では、銀行のトランザクションが挙げられており、顧客が所有している複数の口座全てからほぼ同時に引き出しトランザクションを行うことで、残高より多い金額の引き出しが可能となるケースが説明されている。条件として顧客が複数の口座を所有している場合、残高のトータルが0以上であれば、特定の口座残高のマイナス(負債状態)が許される状態ではあるが、引き出し時のユーザー体験的に見た場合現実に十分あり得る設定と考えられる。口座所持者がそれぞれ残高$100
の口座に$200
を引き出すトランザクションを同時に発生させることで、SELECT FOR UPDATE
などの対策が無い場合、スナップショット分離レベルのDBは両トランザクションを成功させてしまい、トータル残高は-$200
となってしまう。
一見これらの例は理論的な話に見えるかも知れないが実際に攻撃手法として使用されている例が報告されている。Stanford InfoLabのメンバーによる論文:「ACIDRain: Concurrency-Related Attacks on Database-Backed Web Applications」によるとビットコイン交換所が倒産する程のデータ崩壊やEコマースサイトへのギフトカードの超過額使用、商品インベントリデータの崩壊などの確認が報告されている。