普段あまり気にすることがないトランザクション分離レベルが忘れがちなので、ちゃんとまとめてみようと思って書きました。
トランザクションとは
1つの作業単位として扱う一連の操作の集まりです。
トランザクション内の操作が全て実行されるか、または全て実行されないことを保証してくれます。
ACID特性
信頼性のあるトランザクションシステムの持つべき性質として定義した概念です。
原子性 (Atomicity)
トランザクション内の操作が全て実行されるか、または全て実行されないかのどちらかになります。中途半端な状態は有り得ないです。つまり、トランザクションはそれ以上細かい単位に分割することができない作業単位であるということです。
一貫性 (Consistency)
トランザクションの実行前と後でデータに矛盾がなく整合性が保たれる性質です。
分離性 (Isolation)
トランザクション中に行われる操作は他のトランザクションに影響を与えない性質です。つまり、それぞれのトランザクションは分離された状態で操作を行わなければなりません。
永続性 (Durability)
トランザクションが完了すると、その処理結果は永続的となります。たとえシステム障害が発生してもデータが失われることがない性質です。
トランザクション分離レベルは分離性に関する概念
分離性は他のトランザクションに影響を与えない性質ということで、他のトランザクションに影響を与えると幾つかの問題が発生します。
トランザクションAとトランザクションBが同時に一つのテーブルを操作するとします。
ダーティリード (Dirty Read)
トランザクションBでコミットされていないデータをトランザクションAで読み取ってしまう問題が起きます。
ファジーリード/ノンリピータブルリード (Fuzzy Read / Non-Repeatable Read)
トランザクションAでデータを複数回読み取っている途中で、トランザクションBでデータを更新してコミットした場合、トランザクションAで違う結果のデータを読み取ってしまう問題が起きます。(非再現リードとも呼ぶ)
ファントムリード (Phantom Read)
トランザクションAで一定の範囲のレコードに対して処理を行っている途中で、トランザクションBでデータを追加・削除してコミットした場合、トランザクションAで幻影のようにデータが反映されるため、処理の結果が変わってしまう問題が起きます。
これらの問題をどの程度許容するかがトランザクション分離レベルです。
トランザクション分離レベルの種類
分離レベルは4種類あります。
それぞれの分離レベルで発生する問題は以下のように。
分離レベル | ダーティリード | ファジーリード | ファントムリード |
---|---|---|---|
READ UNCOMMITTED | ○ | ○ | ○ |
READ COMMITTED | × | ○ | ○ |
REPEATABLE READ | × | × | ○ |
SERIALIZABLE | × | × | × |
○:発生する ×:発生しない |
※ InnoDBはREPEATABLE READ
でもファントムリードが発生しません。
MySQLで実際に試す
MySQL(InnoDB)でトランザクション分離レベルを実際に試してみます。
準備
プレイヤーのコイン数を表す簡単なテーブルを作ります。
テーブル構造は次のようになります。
テーブルにplayer1とplayer2のデータを追加します。
2つのターミナルからMySQLに接続します。クライアントAとクライアントBとします(以下はAとBと呼ぶ)。
これで準備は完了です。
READ UNCOMMITTED
READ UNCOMMITTED
は一番低いレベルです。
コミットされていない変更を他のトランザクションから参照できる設定です。
① Aで現在接続中のセッションのトランザクション分離レベルをREAD UNCOMMITTED
に設定して、テーブルを検索します。
② BでREAD COMMITTED
に設定します。そしてplayer1
のデータを更新します(未コミット)。
③ Aで再度テーブルを検索します。
この時点でBで更新したデータがコミットされていないにもかかわらず、Aでそのデータが読み取れました。
つまり、ダーティリードが発生しました。
READ COMMITTED
コミットされた変更を他のトランザクションから参照できる設定です。
READ COMMITTED
が多くのデータベース(Oracle、PostgreSQL、SQL Server)でデフォルトのトランザクション分離レベルになっています。
① Aで分離レベルをREAD COMMITTED
に設定して、テーブルを検索します。
② BでREAD COMMITTED
に設定してplayer1
のデータを更新します(未コミット)。
③ Aで再度テーブルを検索します。
ダーティリードが発生しなくなります。
④ Bでコミットをします。
⑤ Aで再度テーブルを検索します。
player1
の変更が参照できたことによって前回と違う結果になりました。
つまり、ファジーリードが発生しました。
⑥ Aでコイン数の合計値を検索します。
⑦ Bでテーブルにplayer3
のデータを追加してコミットします。
⑧ Aでコイン数の合計値を再度検索します。
player3
のデータが参照できたことによって前回と違う結果になりました。
つまり、ファントムリードが発生しました。
REPEATABLE READ
コミットされた追加・削除を他のトランザクションから参照できる設定です。
MySQLのデフォルトのトランザクション分離レベルになっています。
テーブルを初期状態に戻します。
① Aでトランザクション分離レベルをREPEATABLE READ
に設定して、テーブルを検索します。
② BでREPEATABLE READ
に設定してテーブルを更新しコミットします。
③ Aで再度テーブルを検索します。
同じ結果になります。ファジーリードが発生しなくなります。
④ Aでトランザクションを終了させて再度検索します。
Bで更新したplayer2
のデータを参照できました。
⑤ Aでコイン数の合計値を検索します。
⑥ Bでテーブルにplayer3
のデータを追加してコミットします。
⑦ Aでコイン数の合計値を再度検索します。
同じ結果になります。ファントムリードも発生しません。
※ MySQL(InnoDB)では、MVCC(MultiVersion Concurrency Control)という技術でファントムリードを防いでいます。
SERIALIZABLE
強制的にトランザクションを順序付けて処理する一番高いトランザクション分離レベルです。
最も安全にデータを操作できるが、相対的に性能は低いため、めったに使われないです。
テーブルを初期状態に戻します。
① Aでトランザクション分離レベルをSERIALIZABLE
に設定して、テーブルを検索します。
② Bでテーブルにplayer3
のデータを追加します。
ロック競合が起きてデータの追加が失敗しました。
③ Bでテーブルを検索します。
問題なく実行しました。
④ Aでトランザクションを終了させます。
⑤ Bでテーブルにplayer3
のデータを追加します。
問題なく実行しました。
SERIALIZABLE
の場合は読み取るすべての行に共有ロックをかけます。そのため、ファントムリードもファジーリードも発生しません。
まとめ
- トランザクションは1つの作業単位として扱う一連の操作の集まり
- トランザクション分離レベルはACID特性のI(Isolation、分離性)に関する概念で、他のトランザクションに影響を与えるレベルを意味する
-
READ UNCOMMITTED
:コミットされていない変更を他のトランザクションから参照できる- ダーティリード、ファジーリード、ファントムリードが全て発生する
-
READ COMMITTED
:コミットされた変更を他のトランザクションから参照できる- Oracle、PostgreSQL、SQL Serverのデフォルトのトランザクション分離レベル
- ファジーリード、ファントムリードが発生する
-
REPEATABLE READ
:コミットされた追加・削除を他のトランザクションから参照できる- MySQLのデフォルトのトランザクション分離レベル
- ファントムリードが発生する
- MySQL(InnoDB)は
REPEATABLE READ
でもファントムリードが発生しない
-
SERIALIZABLE
:強制的にトランザクションを順序付けて処理する(直列化)- 読み取るすべての行に共有ロックをかける
- ダーティリード、ファジーリード、ファントムリードが全て発生しない