基本的に怠Diary

主に日常と作ったものを書いていく。

MySQLトランザクション分離レベル完全ガイド

概要

トランザクション分離レベルは、並行トランザクション間でのデータ可視性を制御する。 厳格性を上げるほど一貫性が向上する一方、パフォーマンスは低下する。

分離レベルと発生する問題

分離レベル 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セッション同時実行し、各問題を再現する。