Edited at

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

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


トランザクションとは

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

テーブルにplayer1とplayer2のデータを追加します。

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

② BでもREAD UNCOMMITTEDに設定します。そしてplayer1のデータを更新します(未コミット)。

1c2153f5-c99a-031b-f763-3b7740c123aa.png

③ Aで再度テーブルを検索します。

1e3a3d5f-dda2-424c-ebfd-fee40ce43d2f

この時点でBで更新したデータがコミットされていないにもかかわらず、Aでそのデータが読み取れました。

つまり、ダーティリードが発生しました。


READ COMMITTED

コミットされた変更を他のトランザクションから参照できる設定です。

READ COMMITTEDが多くのデータベース(Oracle、PostgreSQL、SQL Server)でデフォルトのトランザクション分離レベルになっています。

① Aで分離レベルをREAD COMMITTEDに設定して、テーブルを検索します。

543b9755-4e01-20c2-84b4-ca0ee2c1e273.png

② BでREAD UNCOMMITTEDに設定してplayer1のデータを更新します(未コミット)。

d86cec39-3a3d-6cbb-44b5-81d6a43b4d02.png

③ Aで再度テーブルを検索します。

50545/8bb1d425-f5b8-24b2-5d26-7064b11c0df9.png

ダーティリードが発生しなくなります。

④ Bでコミットをします。

8f1d44ed-622e-ed00-cdd9-1f77e1f264df.png

⑤ Aで再度テーブルを検索します。

edd476e6-7da7-3e72-b133-c80d013c0261.png

player1の変更が参照できたことによって前回と違う結果になりました。

つまり、ファジーリードが発生しました。

⑥ Aでコイン数の合計値を検索します。

3b959ae6-4cef-deb0-90a6-6bced91aef8f.png

⑦ Bでテーブルにplayer3のデータを追加してコミットします。

98bf539f-bf97-b855-aa23-d92d6d47ce05.png

⑧ Aでコイン数の合計値を再度検索します。

b2d5f473-7c04-d3cd-c57a-3e6cbf6a285c.png

player3のデータが参照できたことによって前回と違う結果になりました。

つまり、ファントムリードが発生しました。


REPEATABLE READ

コミットされた追加・削除を他のトランザクションから参照できる設定です。

MySQLのデフォルトのトランザクション分離レベルになっています。

テーブルを初期状態に戻します。

① Aでトランザクション分離レベルをREPEATABLE READに設定して、テーブルを検索します。

3714bc1d-eef2-eb84-7cb5-d3bc0153ada1.png

② BでREPEATABLE READに設定してテーブルを更新しコミットします。

MINGW64__f_punk_docker 2018-08-22 19.05.22.png

③ Aで再度テーブルを検索します。

80bb063d-bf6e-5033-8e92-602ed8aac1d9.png.png

同じ結果になります。ファジーリードが発生しなくなります。

④ Aでトランザクションを終了させて再度検索します。

697a5d60-3058-db4c-3e7b-c81993c5c81b.png

Bで更新したplayer2のデータを参照できました。

⑤ Aでコイン数の合計値を検索します。

00fee73a-ce20-a819-c78e-3aa1c6d38b4f.png

⑥ Bでテーブルにplayer3のデータを追加してコミットします。

71367b4c-f0cc-c56b-b154-45f646d39511.png

⑦ Aでコイン数の合計値を再度検索します。

4b0d5d9d-ba2a-e3d8-37d3-c68661d9e744.png

同じ結果になります。ファントムリードも発生しません。

※ MySQL(InnoDB)では、MVCC(MultiVersion Concurrency Control)という技術でファントムリードを防いでいます。


SERIALIZABLE

強制的にトランザクションを順序付けて処理する一番高いトランザクション分離レベルです。

最も安全にデータを操作できるが、相対的に性能は低いため、めったに使われないです。

テーブルを初期状態に戻します。

① Aでトランザクション分離レベルをSERIALIZABLEに設定して、テーブルを検索します。

4ccdd822-b815-54bb-ba71-a62240a95a72.png

② Bでテーブルにplayer3のデータを追加します。

2d4a4211-99a8-91ae-e836-12a46e45f692.png

ロック競合が起きてデータの追加が失敗しました。

③ Bでテーブルを検索します。

30852477-3f9a-e97d-65ce-c1249344b099.png

問題なく実行しました。

④ Aでトランザクションを終了させます。

491651f5-9622-0e67-09ed-115bf5ff4416.png

⑤ Bでテーブルにplayer3のデータを追加します。

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:強制的にトランザクションを順序付けて処理する(直列化)


    • 読み取るすべての行に共有ロックをかける


    • ダーティリードファジーリードファントムリードが全て発生しない