トランザクション分離レベル
整理したい概念
- ACID 特性
- トランザクション分離レベル
- 共有ロック、排他ロック
- 悲観ロック、楽観ロック
参考リンク
-
(2021.03, AbemaTV の人) トランザクション分離レベルのケースと対応方法
- 図がシンプルでとても分かりやすい。必読。
-
(2022.07, mpyw) MySQL/Postgres におけるトランザクション分離レベルと発生するアノマリーを整理する
-
(2022.08, mpyw) MySQL / PostgreSQL におけるトランザクション分離レベル(↑の記事と同じ著者の登壇資料)
-
(2020.12, DeNA の人, 660 いいね) 「トランザクション張っておけば大丈夫」と思ってませんか?バグの温床になる、よくある実装パターン
-
(2020.05, もみじあめさん) MySQL の InnoDB でトランザクション分離レベルの違いを試す
唯一のハンズオン記事!
MySQL のハンズオンで調べていく記事。
-
(2021.07, Qiita) ダーティリード、リピータブルリード、ファントムリードをちゃんと理解してからトランザクション分離レベルを理解しよう
ダーティリード、リピータブルリード、ファントムリードをちゃんと理解してからトランザクション分離レベルを理解しよう - Qiita
-
(2020.06, freee のエンジニア) トランザクションの分離性(isolation)の概要
- トランザクションの役割って何?
- 分離性が保証されてないと起きる問題とは?
- 各分離レベルの話
- デッドロックについて
-
(2022.07) Relational Databases Explained
英語解説。
How Relational Databases Work. This post talks about how indexes and transactions work on the inside of relational databases.
手を動かしながらやるのに良い
TiDB のドキュメント
- TiDB トランザクション分離レベル
「トランザクション分離レベルは」英語で「transaction isolation levels」
kumagi さん
トランザクション分離レベルに関する古典的論文
ACID 特性
- Atomicity 原子性
- データの変更(INSERT / UPDATE / DELETE)を伴う一連のデータ操作が「全部成功する」か「全部失敗する」かを保証する性質。
- Consistency 一貫性
- データベースに対する一連のデータ操作の前後で整合性制約を保った状態を保証する性質。
- データベースには、データベースオブジェクト(テーブルを始めとするデータベース内に定義できるオブジェクト)に対して、各種整合性制約(一意制約、NOT NULL 制約、など)を付加することができる。データベースに対する一連のデータ操作の前後で整合性制約を保った状態を保証する性質、仕組みが「一貫性」である。
- Isolation 分離性(、隔離性、独立性)
- データベースに対する一連のデータ操作が複数のユーザから同時に行われる際に、それぞれの処理が矛盾なく行えることを保証する性質。
- Dulability 持続性
- 一連のデータ操作(トランザクション操作)を完了(COMMIT)し、完了通知をユーザが受けた時点で、その操作が永続的となり結果が失われない性質。
- これはシステムの正常時だけにとどまらず、データベースサーバや OS の異常終了、つまりシステム障害に耐えるということ。多くのデータベースの実装では、トランザクション操作をハードディスクの上にログとして記録し、システム障害が発生したらそのログを用いて異常発生前の状態まで復旧することで、持続性を実現している。
2 Phase Lock(2PL)


2 Phase Lock のメリット、デメリット
- メリット
- 実装がシンプル
- (読み書きするやつをとりあえず全部ロックしておけば良いので)
- デメリット
- ロック範囲、ロック時間が長くなりやすい
- (ゆえに重い)
「トランザクション分離レベル」の策定
非効率ながちがちのロックから始まり、段階的にデータ操作におけるデータの整合性の保証に対する妥協レベルを定めよう。
SERIALIZABLE のイメージとしては、シングルスレッドで順次トランザクションを 1 つずつ処理していくというもの。常に動作しているトランザクションの数は 1 つであり(他のトランザクションはロック待ち)、これはパフォーマンス的に実用に耐えない。
そのため、SERIALIZABLE から分離(Isolation)の厳格性を緩めて、自身以外のトランザクションの影響を受けることを許容する分離のレベルが「トランザクション分離レベル」として ANSI によって 4 段階に定義されている。SERIALIZABLE が一番厳格なトランザクション分離レベルである。

古典的なトランザクション分離レベルの定義
-
READ UNCOMMITTED(リードアンコミッテッド、非コミット済み読み取り)
- 意味
- トランザクションにおいて、他者のコミットされていない変更まで読み込むことができる
- 発生する問題
- Dirty Read:
- 他者のトランザクション内のコミットされていない変更まで見えてしまう
- コミットによる変更確定前の「Dirty(汚れた)」なデータを読み出すことから Dirty Read と呼ばれる
- Dirty Read:
- 意味
-
READ COMMITTED(リードコミッテッド、コミット済み読み取り)
- 意味
- 同一トランザクション内において、他者のトランザクションでコミットされた変更を読み込むことができる
- コミットされていない変更は読み込めない
- 発生する問題
- Fuzzy Read(Non Repeatable Read):
- 特定のトランザクションにおいて、ある時点で読み取り(
SELECT)を行った後、他のトランザクションが自身のトランザクション中に変更をコミットし、同一のトランザクション内で再度同じ値を読み取ったとき、最初の時点とは異なる値が読み込まれること。 - つまり、同一トランザクション内でデータベースから読み取る値が同じであることが保証されていない、という問題が起きる。
- 特定のトランザクションにおいて、ある時点で読み取り(
- Fuzzy Read(Non Repeatable Read):
- 意味
-
REPEATABLE READ(リピータブルリード、再読み取り可能読み取り)
- 意味
- 他者のコミットされた変更は全て見えるが、自分のトランザクションの中で一貫性は保たれる
- 同じレコードに対して繰り返される
SELECTは、常に最初と同じ結果を返す - スナップショット分離とも呼ばれる
- 意味
-
SERIALIZABLE(シリアライザブル、直列化可能)
- 意味
- 複数のトランザクションが同時実行されたとしても、いずれかの順番で逐次実行した場合と結果が同じになる(直列化できる)ことが保証されている
- 複数のトランザクションが同時実行されたとき、複数のトランザクションが順次実行された場合と同じ結果になることが保証されている
- イメージ
- SERIALIZABLE のイメージとしては、シングルスレッドで順次トランザクションを 1 つずつ処理していくというもの。常に動作しているトランザクションの数は 1 つであり(他のトランザクションはロック待ち)、これはパフォーマンス的に実用に耐えない。そのため、SERIALIZABLE から分離(Isolation)の厳格性を緩めて、自身以外のトランザクションの影響を受けることを許容する分離のレベルが「トランザクション分離レベル」として ANSI によって 4 段階に定義されている。SERIALIZABLE が一番厳格なトランザクション分離レベルである。
- 意味

引用元:https://docs.pingcap.com/ja/tidb/stable/transaction-isolation-levels/
MySQL でトランザクション分離レベルの確認
mysql> SHOW VARIABLES like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.00 sec)
その他トランザクションに関する設定
mysql> SHOW VARIABLES like 'transaction%';
+------------------------------+-----------------+
| Variable_name | Value |
+------------------------------+-----------------+
| transaction_alloc_block_size | 8192 |
| transaction_allow_batching | OFF |
| transaction_isolation | REPEATABLE-READ |
| transaction_prealloc_size | 4096 |
| transaction_read_only | OFF |
+------------------------------+-----------------+
5 rows in set (0.00 sec)
古典的な(= ANSI で定義されている)トランザクション分離レベルと現代の RDBMS におけるトランザクション分離レベルは同じ言葉を使っていても意味が異なる場合が多いので注意。
厳密に各 DBMS でのトランザクション分離レベルの定義がどうこうとかを比較、網羅することに労力を割くよりかは、自身のアプリケーションでの要件に照らし合わせて使用する DBMS のトランザクション分離レベルの設定で要件が満たせるか?という観点で見ていくのが良い。
もちろん DBMS 選定時にそれぞれが何に当たるかは確認、比較する必要があるけども、全部を網羅しようとするのは膨大すぎる。
対話環境に備わっている自動コミット、暗黙的なコミットについて
自動コミットの設定を確認する。
なお、トランザクションが明示的に開始されている場合、自動コミットは適用されない。
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
1 row in set (0.00 sec)
設定を変更する:
-- グローバルな設定
SET GLOBAL autocommit = 0;
-- セッション単位
SET autocommit = 0;
READ UNCOMMITTED(リードアンコミッテッド、非コミット済み読み取り)
基本的に使う場面はないと考えて良い。
MVCC では、読み取りがブロックされることはないので READ UNCOMMITTED 自体はそもそも必要ない。Oracle、PostgreSQL、Firebird では READ UNCOMMITTED はサポートされていない。
READ COMMITTED(リードコミッテッド、コミット済み読み取り)
REPEATABLE READ(リピータブルリード、再読み取り可能読み取り)
REPEATABLE READ の実装例:MVCC, Multi-Version Concurrency Control
REPEATABLE READ(スナップショット分離)の実装の1つに MVCC がある。
各トランザクションが単調増加なトランザクション ID を持ち、DB へ書き込む際にデータに created by、deleted by が記載される。
そして後から発行されたトランザクション ID による書き込みを全て無視することで一貫したスナップショットを見ることができる。

Alice の最後の口座 2 への Read のとき、自身のトランザクション ID 12 よりあとに発行された ID の処理は無視して Read することで、トランザクション開始時点の Read と同じ結果を返すことができる(同じレコードに対して繰り返される SELECT は常に最初と同じ結果を返す)。
引用元↓
SERIALIZABLE(シリアライザブル、直列化可能)
一番厳格なトランザクション分離レベル。パフォーマンス上の制約のため、ハイトラフィックな Web アプリケーションの本番環境では基本使われないと考えて良い。
トランザクション分離レベルを考案する元となっている。
MySQL(ストレージエンジン InnoDB)におけるトランザクション分離レベル
MySQL のデフォルトのトランザクション分離レベルは REPEATABLE READ。
mysql> SHOW VARIABLES like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.00 sec)
現在稼働中のトランザクションのトランザクション分離レベルを確認する
information_schema.innodb_trx というテーブルから現在稼働中のトランザクションの情報を取得できる。もちろんトランザクション分離レベルも取得できる。
テーブルの定義の確認
mysql> desc information_schema.innodb_trx;
+----------------------------+-----------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------------------+-----------------+------+-----+---------+-------+
| trx_id | bigint unsigned | NO | | | |
| trx_state | varchar(13) | NO | | | |
| trx_started | datetime | NO | | | |
| trx_requested_lock_id | varchar(126) | YES | | | |
| trx_wait_started | datetime | YES | | | |
| trx_weight | bigint unsigned | NO | | | |
| trx_mysql_thread_id | bigint unsigned | NO | | | |
| trx_query | varchar(1024) | YES | | | |
| trx_operation_state | varchar(64) | YES | | | |
| trx_tables_in_use | bigint unsigned | NO | | | |
| trx_tables_locked | bigint unsigned | NO | | | |
| trx_lock_structs | bigint unsigned | NO | | | |
| trx_lock_memory_bytes | bigint unsigned | NO | | | |
| trx_rows_locked | bigint unsigned | NO | | | |
| trx_rows_modified | bigint unsigned | NO | | | |
| trx_concurrency_tickets | bigint unsigned | NO | | | |
| trx_isolation_level | varchar(16) | NO | | | |
| trx_unique_checks | int | NO | | | |
| trx_foreign_key_checks | int | NO | | | |
| trx_last_foreign_key_error | varchar(256) | YES | | | |
| trx_adaptive_hash_latched | int | NO | | | |
| trx_adaptive_hash_timeout | bigint unsigned | NO | | | |
| trx_is_read_only | int | NO | | | |
| trx_autocommit_non_locking | int | NO | | | |
| trx_schedule_weight | bigint unsigned | YES | | | |
+----------------------------+-----------------+------+-----+---------+-------+
25 rows in set (0.00 sec)
実際にトランザクションを動かして確認する
稼働中のトランザクションの情報は information_schema.innodb_trx テーブルから取得できる。
注意点として、読み取り専用、または非ロックのトランザクションを作成してもこのテーブルにはレコードが追加されない。そのため、このテーブルにレコードを追加するには更新系クエリ(INSERT / UPDATE / DELETE)を実行する必要がある。以下、公式ドキュメントより引用:
InnoDB 内部の一意のトランザクション ID 番号。 これらの ID は、読取り専用および非ロックのトランザクションに対しては作成されません。
MySQL :: MySQL 8.0 リファレンスマニュアル :: 26.51.29 INFORMATION_SCHEMA INNODB_TRX テーブル
以下のコードでトランザクションを作成、稼働させ、各トランザクションのトランザクション分離レベルを確認してみる。2 つのコネクション Transaction A、B から、それぞれトランザクション分離レベルを REPEATABLE READ、READ COMMITTED としてトランザクションを作成、稼働させている。
-- ------------------------------------------------------------
-- 稼働中のトランザクションのトランザクション分離レベルを確認する
-- ------------------------------------------------------------
-- Transaction A> : REPEATABLE READ
-- ---------------------------------------------------------------
SET TRANSACTION isolation level repeatable READ;
START TRANSACTION;
INSERT INTO
tx_test (id, name)
VALUES
(4, 'Oracle from Tx A');
-- Transaction B> : READ COMMITTED
-- ---------------------------------------------------------------
SET TRANSACTION isolation level READ committed;
START TRANSACTION;
INSERT INTO
tx_test (id, name)
VALUES
(5, 'JavaDB from Tx B');
-- 現在稼働中のトランザクションのトランザクション分離レベルを確認
-- trx_mysql_thread_id は CONNECTION_ID() と同一
-- +--------+---------------------+---------------+
-- | trx_id | trx_isolation_level | connection_id |
-- +--------+---------------------+---------------+
-- | 4655 | READ COMMITTED | 12 |
-- | 4654 | REPEATABLE READ | 11 |
-- +--------+---------------------+---------------+
SELECT
trx_id,
trx_isolation_level,
trx_mysql_thread_id AS connection_id
FROM
information_schema.innodb_trx;
-- Transaction A>
ROLLBACK;
-- Transaction B>
ROLLBACK;
他トランザクションからの更新の見え方
トランザクション分離レベルを変えた 2 つのトランザクションから、別のトランザクションの更新はどのように見えるのか。
-- ------------------------------------------------------------
-- トランザクション分離レベルを変えて見る
-- Transaction A> : REPEATABLE READ
-- Transaction B> : READ COMMITTED
-- Transaction C> : REPEATABLE READ(この例ではあまり関係ないけど )
-- ------------------------------------------------------------
-- Transaction A> : REPEATABLE READ
-- ---------------------------------------------------------------
-- SET TRANSACTION ISOLATION LEVEL <level>:
-- 注意:
-- - 次に開始するトランザクションの特性を変える文なのでセッション変数は上書きされない。
-- - なので SHOW VARIABLES like 'transaction_isolation'; は変わらない。
SET TRANSACTION isolation level repeatable READ;
START TRANSACTION;
-- Transaction B> : READ COMMITTED
-- ---------------------------------------------------------------
SET TRANSACTION isolation level READ committed;
START TRANSACTION;
-- Transaction C> : REPEATABLE READ
-- ---------------------------------------------------------------
SHOW VARIABLES like 'transaction_isolation';
-- MySQL のデフォルトのトランザクション分離レベルは REPEATABLE READ
-- Transaction C: レコードの更新。Tx A, B からはどう見えるか?
-- (id, name) = (1, 'MySQL') -> (5, 'PostgreSQL') に更新
START TRANSACTION;
UPDATE tx_test
SET
id = 5,
name = 'PostgreSQL'
WHERE
id = 1;
-- Transaction A> : REPEATABLE READ
-- 更新は見えない
SELECT
*
FROM
tx_test;
-- Transaction B> : READ COMMITTED
-- 更新は見えない
SELECT
*
FROM
tx_test;
-- Transaction C
COMMIT;
-- Transaction A> : REPEATABLE READ
-- 更新は見えない
SELECT
*
FROM
tx_test;
-- Transaction B> : READ COMMITTED
-- トランザクション中だが、更新が見えるようになる
-- Fuzzy Read (Non Repeatable Read) が起きている
SELECT
*
FROM
tx_test;
-- +-----+--------------+
-- | id | name |
-- +-----+--------------+
-- | 2 | MySQL |
-- | 3 | PostgreSQL |
-- | 5 | PostgreSQL |
-- | 999 | Aurora MySQL |
-- +-----+--------------+
-- Transaction A>
ROLLBACK;
-- トランザクションを抜けたので、Transaction A でも更新が見えるようになる
SELECT
*
FROM
tx_test;
-- Transaction B>
ROLLBACK;
-- Transaction C>
ROLLBACK;
ちなみに、自分自身のトランザクションによる更新は即座に見ることができる。
分離レベルによる読み取り内容の違いは、ロールバックセグメントという領域に保存される「UNDO ログ」により実現される。