MySQLのトランザクション分離レベル:ACID・異常現象を整理

a colorful toy on a table SQL

複数のユーザーが同時にデータを更新するシステムでは、「コミット前のデータを別のトランザクションが読んでしまう」「2回読んだら値が変わっていた」といった不整合が起きます。これを制御するのがトランザクションの「分離レベル」です。この記事では、トランザクションが持つACID特性と、SQLで定義された4つの分離レベルの挙動を整理します。ダーティリード・ノンリピータブルリード・ファントムリードの違いと、MySQLのデフォルト(REPEATABLE READ)で何が防げるかを表で確認できます。

このシナリオで考える

口座Aから口座Bへ1万円を振り込む処理を例として使います。残高を減らすUPDATEと残高を増やすUPDATEの2つで1件の振込が完結します。この2つが「必ず両方成功、または両方失敗」で終わるよう保証する仕組みがトランザクションです。

CREATE TABLE accounts (
  id      INT PRIMARY KEY,
  name    VARCHAR(50),
  balance INT NOT NULL
);

INSERT INTO accounts VALUES
  (1, '山田', 50000),
  (2, '鈴木', 30000);

振込処理の骨格は次のようになります。

START TRANSACTION;

UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;

COMMIT;
-- エラーが起きた場合
ROLLBACK;

ACIDとは:トランザクションの4つの特性

ACIDは、信頼できるトランザクション処理に必要な4つの特性の頭文字です。

特性 読み 意味
Atomicity 原子性 すべて成功、またはすべて失敗。途中で止まっても中間状態を残さない
Consistency 一貫性 トランザクション前後でNOT NULL・外部キーなどのDB制約を満たした状態を保つ
Isolation 分離性 並列実行中のトランザクションが互いに干渉しない(どの程度かは分離レベルで決まる)
Durability 永続性 COMMITしたデータはシステム障害後も失われない

4つのうち「分離性をどの程度保証するか」を調整するのが分離レベルです。分離性を高めるほど安全になりますが、ロックの競合が増えて同時実行性が下がります。

3種類の異常現象:何が問題になるのか

分離レベルを理解するには、まず3つの異常現象を把握しておく必要があります。

ダーティリード(dirty read)

別のトランザクションがUPDATEしたが、まだCOMMITしていないデータを読んでしまう現象です。相手がROLLBACKすると、読んだ値は「存在しなかったことになった」データになります。

ノンリピータブルリード(non-repeatable read)

同一トランザクション内で同じ行を2回SELECTしたとき、間に別トランザクションがCOMMITしたため値が変わってしまう現象です。「同じ行を読んだのに値が違う」という状況がこれにあたります。

ファントムリード(phantom read)

同一トランザクション内で同じ条件のSELECTを2回実行したとき、間に別トランザクションがINSERT/DELETEしてCOMMITしたため、返ってくる行数が変わってしまう現象です。幽霊(phantom)のように行が現れたり消えたりする点から名付けられています。

4つの分離レベルと防げる異常現象

SQLには4段階の分離レベルが定義されています。○は発生する可能性あり、✕は防げることを示します。

分離レベル ダーティリード ノンリピータブルリード ファントムリード
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ(MySQL既定)
SERIALIZABLE

REPEATABLE READのファントムリードが「△」なのは、MySQLのInnoDBエンジンが「ギャップロック」という仕組みで通常のケースでは防いでいるためです。ロッキングリード(SELECT ... FOR UPDATE)を使う場面では挙動が変わる場合があります。

READ UNCOMMITTED

最も緩い設定です。COMMITされていないデータも読めるため、ダーティリードが発生します。実務ではほぼ使われません。

READ COMMITTED

COMMITされたデータのみを読みます。PostgreSQLのデフォルトでもあります。ダーティリードは防げますが、SELECTのたびに最新スナップショットを取得するため、ノンリピータブルリードが起きます。

REPEATABLE READ(MySQLのデフォルト)

トランザクション開始時点のスナップショットを読み続けます。同じ行を何度SELECTしても同じ値が返ってくるため、ノンリピータブルリードを防ぎます。InnoDBのギャップロックによりファントムリードも通常は防ぎます。

SERIALIZABLE

最も強い設定です。すべてのSELECTに共有ロックをかけ、トランザクションを直列実行したのと同等の結果を保証します。デッドロックのリスクが上がり、スループットが低下するため、特定の要件がある場合にのみ使います。

MySQLでの確認と変更方法

-- 現在の分離レベルを確認
SHOW VARIABLES LIKE 'transaction_isolation';
+------------------------+-----------------+
| Variable_name          | Value           |
+------------------------+-----------------+
| transaction_isolation  | REPEATABLE-READ |
+------------------------+-----------------+
-- セッション単位で変更(次のトランザクションから適用)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 次の1トランザクションだけに適用
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- ...
COMMIT;

グローバルに変更する場合は SET GLOBAL TRANSACTION ISOLATION LEVEL ... を使います。my.cnftransaction-isolation = READ-COMMITTED と書けば再起動後も維持されます。

よくある落とし穴と注意点

  • 「REPEATABLE READなのに別セッションのINSERT結果が見えた」: SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE はスナップショットではなく最新行を読みます。ロッキングリードを使うと通常のREPEATABLE READとは挙動が変わります。
  • 「READ COMMITTEDで集計値が毎回変わった」: バッチ処理で集計クエリを複数回実行する場合、READ COMMITTEDでは実行のたびに最新スナップショットを取得します。途中で他のトランザクションがCOMMITすると集計値がずれます。集計の一貫性が必要な場合はREPEATABLE READを選びましょう。
  • 「分離レベルを上げれば安全」とは限らない: SERIALIZABLEは安全ですが、ロック競合が増えデッドロックが発生しやすくなります。多くのWebアプリケーションではMySQLのデフォルト(REPEATABLE READ)で十分です。

まとめ

ACIDのうち「分離性(Isolation)」をどこまで保証するかを制御するのが分離レベルです。MySQLのデフォルトはREPEATABLE READで、ダーティリードとノンリピータブルリードを防ぎ、ファントムリードもInnoDBのギャップロックでほぼ防ぎます。変更する際は性能とのトレードオフを意識しましょう。

参考リンク

アイキャッチ画像: Photo by Shubham Dhage on Unsplash

タイトルとURLをコピーしました