概要
トランザクション分離レベルは、並行トランザクション間でのデータ可視性を制御する。 厳格性を上げるほど一貫性が向上する一方、パフォーマンスは低下する。
分離レベルと発生する問題
分離レベル | Dirty Read | Non-Repeatable Read | Phantom Read | 性能 |
---|---|---|---|---|
READ UNCOMMITTED | ⚠️ 発生 | ⚠️ 発生 | ⚠️ 発生 | 最速 |
READ COMMITTED | ✅ 防止 | ⚠️ 発生 | ⚠️ 発生 | 高速 |
REPEATABLE READ | ✅ 防止 | ✅ 防止 | △ InnoDB防止 | 中速 |
SERIALIZABLE | ✅ 防止 | ✅ 防止 | ✅ 防止 | 最遅 |
※ MySQL InnoDB は REPEATABLE READ でも Next-Key Lock や MVCC により Phantom Read を防止
MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.4 ファントム行
ファントムの発生を回避できるように、InnoDB では通常、インデックス行ロックとギャップロックを組み合わせたネクストキーロックと呼ばれるアルゴリズムが使用されます。 InnoDB は、テーブルインデックスを検索またはスキャンするときに、生成されたインデックスレコード上に共有ロックまたは排他ロックを設定するという方法で、行レベルロックを実行します。 したがって、行レベルロックは、実際にはインデックスレコードロックです。 さらに、あるインデックスレコードに対するネクストキーロックによって、そのインデックスレコードの前の「ギャップ」も影響を受けます。 つまり、ネクストキーロックは、インデックスレコードロックと、そのインデックスレコードの前のギャップに対するギャップロックとを組み合わせたものです。 あるセッションがインデックス内のレコード R 上に共有ロックまたは排他ロックを持っている場合は、別のセッションがインデックスの順番で R の直前にあるギャップに新しいインデックスレコードを挿入できません。
前提: テーブル構造
CREATE TABLE accounts ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), balance DECIMAL(10,2) ); CREATE INDEX idx_balance ON accounts(balance); INSERT INTO accounts (name, balance) VALUES ('Alice', 1000), ('Bob', 2000);
1. Dirty Read(ダーティリード)
未コミットデータの読み取り
何が起きるか
-- Session A SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; SELECT balance FROM accounts WHERE name = 'Alice'; -- 1000 -- Session B START TRANSACTION; UPDATE accounts SET balance = 5000 WHERE name = 'Alice'; -- まだコミットしない -- Session A SELECT balance FROM accounts WHERE name = 'Alice'; -- 5000(未コミット!) -- Session B ROLLBACK; -- 取り消し -- Session A SELECT balance FROM accounts WHERE name = 'Alice'; -- 1000に戻っている COMMIT;
問題: 存在しないデータを読み取り、それを前提に処理してしまう
2. Non-Repeatable Read(反復不可能読み取り)
同じ行を読むと値が変わる
何が起きるか
-- Session A SET TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; SELECT balance FROM accounts WHERE name = 'Alice'; -- 1000 -- Session B UPDATE accounts SET balance = 1500 WHERE name = 'Alice'; COMMIT; -- Session A SELECT balance FROM accounts WHERE name = 'Alice'; -- 1500(変わった!) COMMIT;
問題: トランザクション内で一貫性がない(集計処理などで困る)
REPEATABLE READ なら
-- Session A SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; SELECT balance FROM accounts WHERE name = 'Alice'; -- 1000(スナップショット) -- Session B が更新&コミットしても... -- Session A SELECT balance FROM accounts WHERE name = 'Alice'; -- 1000のまま COMMIT;
3. Phantom Read(ファントムリード)
何が起きるか
-- Session A SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; SELECT COUNT(*) FROM accounts WHERE balance >= 1000; -- 2件 -- Session B INSERT INTO accounts (name, balance) VALUES ('Charlie', 1500); COMMIT; -- Session A SELECT COUNT(*) FROM accounts WHERE balance >= 1000; -- MySQL InnoDB: 2件のまま(MVCCスナップショット読み取りで防止)
ロック取得読み取りの場合
-- Session A START TRANSACTION; SELECT COUNT(*) FROM accounts WHERE balance >= 1000 FOR UPDATE; -- Next-Key Lock取得 -- Session B INSERT INTO accounts (name, balance) VALUES ('Charlie', 1500); -- ブロックされる
MySQL特有: REPEATABLE READ でも防止される。
正確には: REPEATABLE READの通常のSELECTなら、MVCCで防止される。
READ COMMITTEDの場合は発生してしまう。
MVCC(multiversion concurrency control)
MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.3 InnoDB マルチバージョン
InnoDB はマルチバージョンストレージエンジン。
4. SERIALIZABLE(完全直列化)
全ての異常を防止、ただし遅い
SELECT がロックを取る
-- Session A SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION; SELECT * FROM accounts WHERE name = 'Alice'; -- 共有ロック取得 -- Session B UPDATE accounts SET balance = 2000 WHERE name = 'Alice'; -- ブロックされる
Write Skew も防止
-- 制約: Alice + Bob の合計 >= 2000 を維持したい -- Session A (REPEATABLE READ) START TRANSACTION; SELECT SUM(balance) FROM accounts WHERE name IN ('Alice','Bob'); -- 3000 -- OK、Aliceから1500引ける UPDATE accounts SET balance = balance - 1500 WHERE name = 'Alice'; COMMIT; -- Session B (REPEATABLE READ) 同時実行 START TRANSACTION; SELECT SUM(balance) FROM accounts WHERE name IN ('Alice','Bob'); -- 3000 -- OK、Bobから1500引ける UPDATE accounts SET balance = balance - 1500 WHERE name = 'Bob'; COMMIT; -- 結果: 合計500(制約違反!)
SERIALIZABLE なら Session B がブロックされ防止される。
Write Skewとは
例えば予約残り一人という場面で残り一人しか予約できないときに
ほぼ同時に予約チェックが発生し、問題ないと判断されて同時に書き込みが発生した際にダブルブッキングが発生してしまう。
SERIALIZABLEなら読み取り時も共有ロックされるため、Write Skewは発生し得ない。
使い分けガイド
用途 | 推奨レベル | 理由 |
---|---|---|
集計レポート | READ COMMITTED | 最新データ優先 |
銀行振込 | REPEATABLE READ | 一貫性重要 |
在庫引当 | SERIALIZABLE | 競合防止必須 |
アクセスログ記録 | READ UNCOMMITTED | 厳密性不要、リアルタイム性優先 |
MySQL デフォルト: REPEATABLE READ(バランス良好)
次回予告
実際にDocker環境で2セッション同時実行し、各問題を再現する。