本文へスキップ
webkida wiki
戻る

【Transaction】分離レベルについて

同時に走るトランザクションが、互いの更新をどこまで見てよいか。それが分離レベルです。同じ SELECT でも、レベルによって見える結果が変わります。MySQL のデフォルトは Repeatable Read です。

手元の MySQL で動かしながら、4つのレベルと、そこで起きうる読み取りの問題を確認します。

用語

分離レベル

同時に動くトランザクションが、他トランザクションの更新をどこまで読んでよいかを決める設定です。セッション単位で切り替えられます。

ダーティーリード(Dirty Read)

他トランザクションがまだコミットしていない変更を読んでしまうことです。あとでロールバックされると、読んだ内容と実データが食い違います。

ノンリピータブルリード(Non-repeatable Read)

同じトランザクション内で同じ行を読み直したとき、他トランザクションのコミット済み更新で値が変わることです。

ファントムリード(Phantom Read)

同じトランザクション内でクエリをやり直したとき、他トランザクションの挿入や削除で、行の集合が変わることです。更新ではなく、行の増え減りに着目します。

MVCC

スナップショットを参照する仕組みです。追加や削除もスナップショット側を見るため、ファントムリードが出ないことがあります。

分離レベル

MySQL で実際に動かしながら、各レベルの動作を確認します。よく使うコマンドは次のとおりです。

-- 分離レベルの確認
select @@transaction_isolation;
-- 分離レベルの設定(セッション単位)
set session transaction isolation level <分離レベル>;
-- トランザクションの開始
start transaction;
begin;
-- コミット
commit;
-- ロールバック
rollback;

設定の詳細は MySQL の SET TRANSACTION を参照してください。

未コミット読み取り(Read Uncommitted)

最も低い分離レベルです。あるトランザクションから、他のトランザクションがまだコミットしていないデータを読めます。

Read Uncommitted の概要

図: トランBが age=40 に更新した直後、コミット前でもトランAから 40 が見える。

トランAのセッションを Read Uncommitted にします。

-- 分離レベルの設定: Read Uncommitted
set session transaction isolation level read uncommitted;

Read Uncommitted の設定

トランAでトランザクションを開始し、user1 の age を読むと 30 です。

トランAの初回参照(age=30)

次にトランBでトランザクションを開始し、age を 40 に更新します。この段階ではまだコミットしません。

トランBが age=40 に更新(未コミット)

この状態でトランAがもう一度読むと、トランBの未コミット更新が見えます(age=40)。

トランAが未コミットの age=40 を読む

コミット済み読み取り(Read Committed)

広く採用されている分離レベルです。あるトランザクションは、他のトランザクションがコミットしたデータだけを読めます。

Read Committed の概要

図: トランBの更新はコミット後にだけトランAへ見える。同じ SELECT でも、コミット前後で age が 30 から 40 に変わる。

デフォルトになっている主な DB は Oracle、SQL Server、PostgreSQL です。

トランAのセッションを Read Committed にします。

-- 分離レベルの設定: Read Committed
set session transaction isolation level read committed;

Read Committed の設定

トランAでトランザクションを開始し、user1 の age を読むと 30 です。

トランAの初回参照(age=30)

次にトランBでトランザクションを開始し、age を 40 に更新します。まだコミットはしません。

トランBが age=40 に更新(未コミット)

この状態でトランAがもう一度読むと、トランBの更新前の値のままです(age=30)。

トランAは未コミット更新を見ない(age=30)

トランBをコミットします。

トランBのコミット

この状態でトランAが再び読むと、トランBが確定した更新が見えます(age=40)。

トランAがコミット後の age=40 を読む

繰り返し可能読み取り(Repeatable Read)

より高い一貫性のある分離レベルです。トランザクション開始時点のスナップショットが作られ、終了するまで他トランザクションの更新は見えません。

Repeatable Read の概要

図: トランBが age=40 をコミットしても、トランAの最中は age=30 のまま。トランAのコミット後に読むと 40 になる。

MySQL のデフォルトは、この Repeatable Read です。

トランAのセッションを Repeatable Read にします。

-- 分離レベルの設定: Repeatable Read
set session transaction isolation level repeatable read;

Repeatable Read の設定

トランAでトランザクションを開始し、user1 の age を読むと 30 です。

トランAの初回参照(age=30)

次にトランBでトランザクションを開始し、age を 40 に更新してコミットします。

トランBが age=40 に更新してコミット

この状態でトランAがもう一度読んでも、トランBの更新は見えません。更新前の age=30 を繰り返し読めます。

トランAはコミット済み更新も見ない(age=30)

トランAをコミットしたあとで読むと、更新後の age=40 が見えます。

トランAコミット後は age=40

直列化可能(Serializable)

最高レベルの一貫性を保証しようとする分離レベルです。あるトランザクションがデータを更新しているあいだ、他のトランザクションがそのデータを参照しようとすると、待ちになることがあります。一貫性は最も高い一方、並行性は下がり、性能へ影響する可能性があります。

Serializable の概要

図: トランAの SELECT は、トランBがコミットするまで結果を返さない。

トランAのセッションを Serializable にします。

-- 分離レベルの設定: Serializable
set session transaction isolation level serializable;

Serializable の設定

トランAでトランザクションを開始します。

トランAのトランザクション開始

トランBでもトランザクションを開始します。

トランBのトランザクション開始

トランBでデータを追加します。

トランBが行を追加

トランAでデータを参照します。待ち状態になり、すぐに結果は返りません。

トランAの SELECT が待ちになる

トランBをコミットします。

トランBのコミット

トランAで待っていた結果が返ってきます。

待ちが解けてトランAに結果が返る

分離の問題

ダーティーリード、ノンリピータブルリード、ファントムリードは、各分離レベルで起きうる読み取りの問題です。分離レベルが下がるほど、起きる可能性は高まります。

問題起きうるレベル何が起きるか
ダーティーリードRead Uncommitted未コミットの更新が見える
ノンリピータブルリードRead Committed 以下同じ行の再読で値が変わる
ファントムリードRepeatable Read 以下挿入・削除で行の集合が変わる

ダーティーリード(Dirty Read)

Read Uncommitted のトランザクション(トランA)がデータを読んだあと、他のトランザクション(トランB)がそのデータを更新してロールバックすると、食い違いが起きます。トランAが読んだ内容と、実際の状態が一致しません。

次の例では、トランAは age=40 として読んでいます。トランBの更新はロールバックでなかったことになるので、実データは age=30 です。

ダーティーリードの概要

図: トランAが読んだ age=40 は、トランBのロールバックで消える。読んだ値と実データが一致しない。

ノンリピータブルリード(Non-repeatable Read)

Read Committed 以下では、同じトランザクション内で同じデータを複数回読むあいだに、最初と違う結果になることがあります。他トランザクションがデータを変更してコミットすると起きます。防ぐなら Repeatable Read か Serializable です。

ただし、ノンリピータブルリードは必ずしも問題ではありません。分離レベルの特性として、正しく動いているとも見なせます。多くの DB では Read Committed がデフォルトなので、この動きは起きえます。実用上は受け入れられることが多く、DB 設計やアプリ要件に合わせてレベルを選ぶことが大切です。

ファントムリード(Phantom Read)

Repeatable Read 以下では、トランザクションの実行中に他トランザクションが行を挿入したり削除したりすると、あとのクエリにその変更が乗る可能性があります。ノンリピータブルリードとの違いは、既存行の更新ではなく、追加や削除に着目する点です。

ファントムリードの概要

図: 定義上のファントムリード。途中で行が挿入され、件数の見え方が変わる。

「Repeatable Read ではファントムリードが起きる」と書いている記事は多いです。手元では発生しませんでした。MVCC のようにスナップショットを参照する仕組みだと、追加や削除もスナップショット側を見ます。そのためファントムリードは出ないようです。

参考: ファントムリードと MySQL の Repeatable Read

試しに MySQL で再現を試みたところ、ファントムリードは起きませんでした。

-- ① トランAでトランザクションを開始
start transaction;
-- ② トランAでデータ参照(データ1件のみある)
select * from employee;
-- ③ トランBでトランザクションを開始
start transaction;
-- ④ トランBでデータ追加
insert into employee values(2,'user2',20);
-- ⑤ トランBでコミット
commit;
-- ⑥ トランAで再びデータ参照
select * from employee;

ファントムリードの期待値は、⑥でトランBが追加した④の行が見えることです。結果としては参照できませんでした。再現を見るには、Read Committed 以下にする必要がありそうです。

まとめ

分離レベルは、同時実行のトランザクションが互いの更新をどこまで見るかを決めます。MySQL のデフォルトは Repeatable Read です。

Read Uncommitted は未コミットの更新まで見えるため、実務ではほぼ使いません。Read Committed ではノンリピータブルリードが起きえますが、多くの DB が採用している動きでもあります。ファントムリードは Repeatable Read 以下で起きうるとされます。ただし MySQL では MVCC のスナップショットを参照するため、手元では再現しませんでした。

インデックス側の整理は 【SQL Server】インデックスと実行計画について解説! にあります。


この記事をシェア:

前の記事
【システムデザイン】CQRS(コマンド・クエリ責務分離)
次の記事
【転職】20代後半Javaエンジニアのフリーランス転職体験記!