トランザクションの分離レベルについてまとめてみました!
分離レベル
MySQLで実際に動かしながら各分離レベルの動作を確認してみます。主なコマンドは以下の通りで、MySQLはデフォルト分離レベルは Repeatable Read となります。
# 分離レベルの確認
select @@transaction_isolation;
# 分離レベルの設定 ※ セッション単位での設定
set session transaction isolation level <分離レベル>;
# トランザクションの開始
start transaction;
begin;
# コミット
commit;
# ロールバック
rollback;
https://dev.mysql.com/doc/refman/8.0/ja/set-transaction.html#set-transaction-isolation-level
未コミット読み取り(Read Uncommitted)
最も低い分離レベルで、あるトランザクションから、他のトランザクションがコミットしていないデータを読み取ることが可能です。
トランAを Read Uncommitted に設定します。
# 分離レベルの設定: Read Uncommitted
set session transaction isolation level read uncommitted;
トランAでトランザクションを開始し、user1のageを確認すると30です。
次に、トランBでトランザクションを開始し、ageを40に更新します。ただしこの段階ではまだコミットしません。
この状態でトランAから再びデータを参照すると、トランBによって更新されたデータを参照することができてしまいます(age=40)。
コミット済み読み取り(Read Committed)
広く採用されている分離レベルです。このレベルでは、あるトランザクションは他のトランザクションによってコミットされたデータのみを読み取ることができます。
デフォルト設定されている主なDB
Oracle、SQL Server、PostgreSQL
トランAを Read Committed に設定します。
# 分離レベルの設定: Read Committed
set session transaction isolation level read committed;
トランAでトランザクションを開始し、user1のageを確認すると30です。
次に、トランBでトランザクションを開始し、ageを40に更新します。ただしこの段階ではまだコミットしません。
この状態でトランAから再びデータを参照すると、トランBによって更新される前のデータを参照することができます(age=30)。
トランBをコミットします。
この状態でトランAから再びデータを参照すると、トランBによって更新されたデータを参照することができます(age=40)。
繰り返し可能読み取り(Repeatable Read)
より高い一貫性のある分離レベルです。あるトランザクションが開始された時点でのデータのスナップショットが作成され、トランザクションが終了するまで、他のトランザクションによる更新は見えません。
デフォルト設定されている主なDB
MySQL
トランAを Repeatable Read に設定します。
# 分離レベルの設定: Repeatable Read
set session transaction isolation level repeatable read;
トランAでトランザクションを開始し、user1のageを確認すると30です。
次に、トランBでトランザクションを開始し、ageを40に更新しコミットします。
この状態でトランAから再びデータを参照すると、トランBによって更新されたデータを参照することができず、更新前のage=30を繰り返し参照することができます。
トランAをコミットした後に参照すると更新後のage=40を参照することができます。
直列化可能(Serializable)
最高レベルの一貫性を保証しようとする分離レベルです。あるトランザクションがデータを更新し、他のトランザクションがそのデータを参照しようとすると待機状態となる可能性があります。データの一貫性としては最高レベルですが、平行性を低下させパフォーマンスへ影響する可能性があります。
トランAを Serializable に設定します。
# 分離レベルの設定: Serializable
set session transaction isolation level serializable;
トランAでトランザクションを開始します。
トランBでトランザクションを開始します。
トランBでデータを追加します。
トランAでデータを参照します。すると待ち状態となり、直ぐに結果が返ってきません。
トランBをコミットします。
トランAで待ちとなっていた結果が返ってきます。
分離の問題
ダーティーリード、ノンリピータブルリード、ファントムリードといった各分離レベルで発生する可能性のある問題についてまとめてみます。分離レベルが下がるほどこれらの問題は発生する可能性が高まります。
ダーティーリード (Dirty Read)
Read Uncommittedが設定されたトランザクション(トランA)でデータ参照し、他のトランザクション(トランB)がそのデータの更新とロールバックした場合、トランAで参照した実際の状態と不一致となり、データの齟齬が発生してしまいます。
以下の例だと、トランAはage=40でデータを読み取っていますが、トランBの更新はロールバックによりなかったことになるのでage=30となります。
ダーティーリードによりデータの正確性や一貫性が損なわれる可能性があるため、一般的にRead Uncommittedは実務ではほとんど使わないと考えて問題ないと思います。
ノンリピータブルリード (Non-repeatable Read)
Read Committed以下の分離レベルが設定されたトランザクション内で同じデータを複数回読み取る間に、最初の読み取りと異なる結果得られることがあります。これは、他のトランザクションがデータを変更しコミットすることで発生する可能性があります。これを防ぐためには、Repeatable Read や Serializableを使用する必要があります。
ただし、ノンリピータブルリードは必ずしも問題とは限りません。これはDBの分離レベルの特性の一部として正しく機能していると見なせます。実際に、多くのDBでは Read Committed がデフォルトの設定となっており、これによりノンリピータブルリードが発生する可能性があります。しかし、多くの実用的なアプリにおいて受け入れられる動作であり、DB設計やアプリ要件に応じて適切な分離レベルを選択することが重要です。
ファントムリード (Phantom Read)
Repetable Read以下の分離レベルが設定されたトランザクションの実行中に、他のトランザクションによって新しい行が挿入されたり、既存の行が削除されたりすることで、後続のクエリでこれらの変更が反映される可能性があります。ノンリピータブルリードとの違いは、データの更新ではなく追加や削除に着目した問題です。
様々な記事でRepetable Readではファントムリードが発生すると記載がありましたが、実際には発生しなかったですね。どうやらMVCCというスナップショットを参照する仕組みが採用されているシステムだと追加・削除についても変更が反映されずファントムリードは発生しないようです。
https://note.com/shift_tech/n/nb6fa93598ced
試しに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以下の分離レベルにする必要がありそうです。
最後に
今回は分離レベルについてまとめてみました!他にもインデックスについてまとめてみたりしているので良ければ見てください!