トランザクション分離レベルについてのまとめ

普段あまり気にすることがないトランザクション分離レベルが忘れがちなので、ちゃんとまとめてみようと思って書きました。

## トランザクションとは
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)でトランザクション分離レベルを実際に試してみます。

### 準備
プレイヤーのコイン数を表す簡単なテーブルを作ります。
テーブル構造は次のようになります。
![f47d160e-be98-de61-56f0-6948e9d32d99.png](https://qiita-image-store.s3.amazonaws.com/0/50545/f47d160e-be98-de61-56f0-6948e9d32d99.png)
テーブルにplayer1とplayer2のデータを追加します。
![3d965213-622c-931a-a6ba-7774febf9c72.png](https://qiita-image-store.s3.amazonaws.com/0/50545/3d965213-622c-931a-a6ba-7774febf9c72.png)
2つのターミナルからMySQLに接続します。クライアントAとクライアントBとします(以下はAとBと呼ぶ)。
これで準備は完了です。

### READ UNCOMMITTED
`READ UNCOMMITTED`は一番低いレベルです。
コミットされていない変更を他のトランザクションから参照できる設定です。

① Aで現在接続中のセッションのトランザクション分離レベルを`READ UNCOMMITTED`に設定して、テーブルを検索します。
![31f2c540-7ad8-e6ea-e1e1-0bbfde82b107.png](https://qiita-image-store.s3.amazonaws.com/0/50545/31f2c540-7ad8-e6ea-e1e1-0bbfde82b107.png)
② Bで`READ COMMITTED`に設定します。そして`player1`のデータを更新します(未コミット)。
![1c2153f5-c99a-031b-f763-3b7740c123aa.png](https://qiita-image-store.s3.amazonaws.com/0/50545/1c2153f5-c99a-031b-f763-3b7740c123aa.png)
③ Aで再度テーブルを検索します。
![1e3a3d5f-dda2-424c-ebfd-fee40ce43d2f](https://qiita-image-store.s3.amazonaws.com/0/50545/1e3a3d5f-dda2-424c-ebfd-fee40ce43d2f.png)
この時点でBで更新したデータがコミットされていないにもかかわらず、Aでそのデータが読み取れました。
つまり、**ダーティリード**が発生しました。

### READ COMMITTED
コミットされた変更を他のトランザクションから参照できる設定です。
`READ COMMITTED`が多くのデータベース(Oracle、PostgreSQL、SQL Server)でデフォルトのトランザクション分離レベルになっています。

① Aで分離レベルを`READ COMMITTED`に設定して、テーブルを検索します。
![543b9755-4e01-20c2-84b4-ca0ee2c1e273.png](https://qiita-image-store.s3.amazonaws.com/0/50545/543b9755-4e01-20c2-84b4-ca0ee2c1e273.png)
② Bで`READ COMMITTED`に設定して`player1`のデータを更新します(未コミット)。
![d86cec39-3a3d-6cbb-44b5-81d6a43b4d02.png](https://qiita-image-store.s3.amazonaws.com/0/50545/d86cec39-3a3d-6cbb-44b5-81d6a43b4d02.png)
③ Aで再度テーブルを検索します。
![50545/8bb1d425-f5b8-24b2-5d26-7064b11c0df9.png](https://qiita-image-store.s3.amazonaws.com/0/50545/8bb1d425-f5b8-24b2-5d26-7064b11c0df9.png)
**ダーティリード**が発生しなくなります。
④ Bでコミットをします。
![8f1d44ed-622e-ed00-cdd9-1f77e1f264df.png](https://qiita-image-store.s3.amazonaws.com/0/50545/8f1d44ed-622e-ed00-cdd9-1f77e1f264df.png)
⑤ Aで再度テーブルを検索します。
![edd476e6-7da7-3e72-b133-c80d013c0261.png](https://qiita-image-store.s3.amazonaws.com/0/50545/edd476e6-7da7-3e72-b133-c80d013c0261.png)
`player1`の変更が参照できたことによって前回と違う結果になりました。
つまり、**ファジーリード**が発生しました。
⑥ Aでコイン数の合計値を検索します。
![3b959ae6-4cef-deb0-90a6-6bced91aef8f.png](https://qiita-image-store.s3.amazonaws.com/0/50545/3b959ae6-4cef-deb0-90a6-6bced91aef8f.png)
⑦ Bでテーブルに`player3`のデータを追加してコミットします。
![98bf539f-bf97-b855-aa23-d92d6d47ce05.png](https://qiita-image-store.s3.amazonaws.com/0/50545/98bf539f-bf97-b855-aa23-d92d6d47ce05.png)
⑧ Aでコイン数の合計値を再度検索します。
![b2d5f473-7c04-d3cd-c57a-3e6cbf6a285c.png](https://qiita-image-store.s3.amazonaws.com/0/50545/b2d5f473-7c04-d3cd-c57a-3e6cbf6a285c.png)
`player3`のデータが参照できたことによって前回と違う結果になりました。
つまり、**ファントムリード**が発生しました。

### REPEATABLE READ
コミットされた追加・削除を他のトランザクションから参照できる設定です。
MySQLのデフォルトのトランザクション分離レベルになっています。

テーブルを初期状態に戻します。
① Aでトランザクション分離レベルを`REPEATABLE READ`に設定して、テーブルを検索します。
![3714bc1d-eef2-eb84-7cb5-d3bc0153ada1.png](https://qiita-image-store.s3.amazonaws.com/0/50545/3714bc1d-eef2-eb84-7cb5-d3bc0153ada1.png)
② Bで`REPEATABLE READ`に設定してテーブルを更新しコミットします。
![MINGW64__f_punk_docker 2018-08-22 19.05.22.png](https://qiita-image-store.s3.amazonaws.com/0/50545/7c02d6c7-9544-9730-e967-fa95d68b777b.png)
③ Aで再度テーブルを検索します。
![80bb063d-bf6e-5033-8e92-602ed8aac1d9.png.png](https://qiita-image-store.s3.amazonaws.com/0/50545/80bb063d-bf6e-5033-8e92-602ed8aac1d9.png)
同じ結果になります。**ファジーリード**が発生しなくなります。
④ Aでトランザクションを終了させて再度検索します。
![697a5d60-3058-db4c-3e7b-c81993c5c81b.png](https://qiita-image-store.s3.amazonaws.com/0/50545/697a5d60-3058-db4c-3e7b-c81993c5c81b.png)
Bで更新した`player2`のデータを参照できました。
⑤ Aでコイン数の合計値を検索します。
![00fee73a-ce20-a819-c78e-3aa1c6d38b4f.png](https://qiita-image-store.s3.amazonaws.com/0/50545/00fee73a-ce20-a819-c78e-3aa1c6d38b4f.png)
⑥ Bでテーブルに`player3`のデータを追加してコミットします。
![71367b4c-f0cc-c56b-b154-45f646d39511.png](https://qiita-image-store.s3.amazonaws.com/0/50545/71367b4c-f0cc-c56b-b154-45f646d39511.png)
⑦ Aでコイン数の合計値を再度検索します。
![4b0d5d9d-ba2a-e3d8-37d3-c68661d9e744.png](https://qiita-image-store.s3.amazonaws.com/0/50545/4b0d5d9d-ba2a-e3d8-37d3-c68661d9e744.png)
同じ結果になります。**ファントムリード**も発生しません。

※ MySQL(InnoDB)では、[MVCC(MultiVersion Concurrency Control)](https://qiita.com/nkriskeeic/items/24b7714b749d38bba87b)という技術で**ファントムリード**を防いでいます。

### SERIALIZABLE
強制的にトランザクションを順序付けて処理する一番高いトランザクション分離レベルです。
最も安全にデータを操作できるが、相対的に性能は低いため、めったに使われないです。

テーブルを初期状態に戻します。
① Aでトランザクション分離レベルを`SERIALIZABLE`に設定して、テーブルを検索します。
![4ccdd822-b815-54bb-ba71-a62240a95a72.png](https://qiita-image-store.s3.amazonaws.com/0/50545/4ccdd822-b815-54bb-ba71-a62240a95a72.png)
② Bでテーブルに`player3`のデータを追加します。
![2d4a4211-99a8-91ae-e836-12a46e45f692.png](https://qiita-image-store.s3.amazonaws.com/0/50545/2d4a4211-99a8-91ae-e836-12a46e45f692.png)
ロック競合が起きてデータの追加が失敗しました。
③ Bでテーブルを検索します。
![30852477-3f9a-e97d-65ce-c1249344b099.png](https://qiita-image-store.s3.amazonaws.com/0/50545/30852477-3f9a-e97d-65ce-c1249344b099.png)
問題なく実行しました。
④ Aでトランザクションを終了させます。
![491651f5-9622-0e67-09ed-115bf5ff4416.png](https://qiita-image-store.s3.amazonaws.com/0/50545/491651f5-9622-0e67-09ed-115bf5ff4416.png)
⑤ Bでテーブルに`player3`のデータを追加します。
![f08dc5b0-4ea4-53c4-d39b-77b3d70bc597.png](https://qiita-image-store.s3.amazonaws.com/0/50545/f08dc5b0-4ea4-53c4-d39b-77b3d70bc597.png)
問題なく実行しました。

`SERIALIZABLE`の場合は読み取るすべての行に共有ロックをかけます。そのため、**ファントムリード**も**ファジーリード**も発生しません。

## まとめ
* トランザクションは1つの作業単位として扱う一連の操作の集まり
* トランザクション分離レベルはACID特性のI(Isolation、分離性)に関する概念で、他のトランザクションに影響を与えるレベルを意味する
* `READ UNCOMMITTED`：コミットされていない変更を他のトランザクションから参照できる
  * **ダーティリード**、**ファジーリード**、**ファントムリード**が全て発生する
* `READ COMMITTED`：コミットされた変更を他のトランザクションから参照できる
  * Oracle、PostgreSQL、SQL Serverのデフォルトのトランザクション分離レベル
  * **ファジーリード**、**ファントムリード**が発生する
* `REPEATABLE READ`：コミットされた追加・削除を他のトランザクションから参照できる
  * MySQLのデフォルトのトランザクション分離レベル
  * **ファントムリード**が発生する
  * MySQL(InnoDB)は`REPEATABLE READ`でも**ファントムリード**が発生しない
* `SERIALIZABLE`：強制的にトランザクションを順序付けて処理する(直列化)
  * 読み取るすべての行に共有ロックをかける
  * **ダーティリード**、**ファジーリード**、**ファントムリード**が全て発生しない