トランザクションの分離レベル
2 本のトランザクションの操作を 1 つずつ進め、READ UNCOMMITTED から SERIALIZABLE までの 4 段階でダーティリード・ファジーリード・ファントム・ロストアップデート・書き込みスキューが起きるかを確かめます。行の版(MVCC)と行ロックの待ち、依存関係の輪による中止も見られます。
起こしてみる現象
分離レベル(2 本とも同じレベル)
T1 が確定前に書き換えた残高を T2 が読み、そのあと T1 が取り消す。T2 は存在しなかった値をもとに動いてしまう。
| # | T1 | T2 |
|---|---|---|
| 1 | UPDATE accounts SET balance = 200 WHERE id = 'A'→ 200 | |
| 2 | SELECT balance FROM accounts WHERE id = 'A'100 | |
| 3 | ROLLBACK取り消し | |
| 4 | COMMIT確定 |
T2 を確定します(確定 1 回目)。T2 の版がほかのトランザクションからも見えるようになります。
ダーティリードは READ COMMITTED で「防げる」
T2 は確定している残高 100 を読みました。
トランザクション(確定は通算 1 回)
行の版(左が古い。下の T1・T2 は、いまその版が見えていることを表します)
口座 A: 残高 100
初期・確定
口座 A: 残高 200
T1・取り消し
分離レベルと現象の一覧
このアプリで 5 つの例をすべての分離レベルで実行した結果です(PostgreSQL と同じ MVCC の動き。READ UNCOMMITTED だけは確定前の値を読むようにしています)。マスを押すとその組み合わせを表示します。
| 現象 | READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE |
|---|---|---|---|---|
| ダーティリード | ||||
| ファジーリード(反復不能読み取り) | ||||
| ファントムリード | ||||
| ロストアップデート | ||||
| 書き込みスキュー |
「標準」は SQL 標準の定義で、その分離レベルが防がなければならない現象かどうかです。標準より強く防ぐ実装もあり、たとえば PostgreSQL の REPEATABLE READ はファントムリードも起こしません。ロストアップデートと書き込みスキューは SQL 標準の 3 つの現象には入っていません。
使い方
- 起こしてみる現象と分離レベルを選ぶと、2 本のトランザクション T1・T2 の操作を上から 1 つずつ実行します。◀ ▶ ボタンや ← → キーでコマ送りできます。
- 右側の「行の版」は、データベースが残している行の版です。点線の枠は確定前の版で、版の下の T1・T2 の印は、いまその版が見えていることを表します。
- 行ロックで待たされた操作は「待ち…」になり、相手が確定か取り消しをしたところで再開します。エラーで中止したトランザクションの残りの操作は実行しません。
- 下の一覧のマスを押すと、その現象と分離レベルの組み合わせを表示します。
分離レベルとは
データベースは、たくさんのトランザクションを同時に動かしながら、1 本ずつ順番に動かしたときと同じ結果になるように見せようとします。ただし完全にそうする(SERIALIZABLE)と、待ちや中止が増えて遅くなります。そこで SQL 標準は、どの程度まで同時実行による食い違いを許すかを 4 段階の分離レベルとして決めています。多くのデータベースの既定は READ COMMITTED(MySQL の InnoDB は REPEATABLE READ)です。
MVCC(多版型同時実行制御)
PostgreSQL や MySQL の InnoDB は、行を書き換えるとき古い版を消さずに新しい版を足し、読む側はスナップショット(ある時点で確定していた版)を読みます。読む操作が書く操作を待たないのが利点です。READ COMMITTED は文ごとにスナップショットを取り直すので、ほかのトランザクションの確定が途中から見えます。REPEATABLE READ はトランザクションの最初の文で取ったスナップショットを使い続けるので、同じ行は何度読んでも同じに見えますが、同時に同じ行を更新するとエラーになります(先に更新したほうが勝つ)。
書き込みスキューは、2 本がそれぞれ同じ条件を確かめたうえで別々の行を書き換えるので、スナップショットでも行ロックでも防げません。PostgreSQL の SERIALIZABLE(SSI)は、「T1 が読んだものを T2 が後から書き換えた」といった依存関係を記録し、それが輪になる(どちらを先にしても説明できない)トランザクションを中止します。このアプリは確定するときに輪を調べる単純な形にしています。中止されたトランザクションは、アプリが最初からやり直す前提です。