MySQLを使ってトランザクションを学ぶ
はじめに
モダンなアプリケーション開発では、O/R Mapper や Web フレームワークによってトランザクション制御が抽象化されているため、開発者がトランザクションを直接意識する機会は少なくなっています。しかし、パフォーマンスチューニングやデータの整合性を確実に確保するためには、トランザクションの仕組みを根本から理解することが不可欠です。
今回は、トランザクション分離レベルについて学習した内容をまとめます。実際の開発現場ではあまり意識することがなくても、トランザクション分離レベルに起因する問題は突然発生する可能性があり、状況に適切に対処するための実践的な知識が重要です。
一般的なWeb記事では、トランザクションの基本的な分離レベルの説明や、単純な操作方法の解説が多く見られます。しかし、それらには以下の課題があると感じています。
- 単にクエリとその実行結果を羅列しているだけ
- 視覚的な図解による説明が不足している
この記事では、MySQLに焦点を当てながら、シーケンス図とgifを用いてトランザクション分離レベルについて学習した内容を備忘録としてまとめます。
成果物
https://github.com/kntks/blog-code/tree/main/2025/04/db-transaction
使用バージョン
バージョン | |
---|---|
Mac | Sequoia 15.2 |
Docker | 27.5.1 |
Docker Compose | v2.32.4 |
MySQL | 8.4.4 |
テーブル定義
この記事では、映画館の予約システムを想定したデータベースを使用します。
テーブル名 | 説明 |
---|---|
theaters | 劇場情報を格納するテーブル |
screens | スクリーン情報を格納するテーブル |
movies | 映画情報を格納するテーブル |
screenings | 上映情報を格納するテーブル |
customers | 顧客情報を格納するテーブル |
seats | 座席情報を格納するテーブル |
reservations | 予約情報を格納するテーブル |
reservation_seats | 予約座席を格納するテーブル |
環境構築
docker compose up
コマンドで MySQL を起動します。
docker compose up -d
トランザクションの基礎
トランザクションは、コミットまたはロールバック可能なアトミックな作業単位です。 トランザクションがデータベースに複数の変更を加えると、トランザクションがコミットされたときにすべての変更が成功するか、トランザクションがロールバックされたときにすべての変更が取り消されます。
トランザクションがあることでシステム障害やハードウェア故障が発生した場合でも、トランザクションのロールバック機能によってデータベースを一貫性のある状態に戻すことができます。
分離レベル
分離レベルは、トランザクションが他のトランザクションの影響をどの程度受けるかを決定する設定です。データの整合性とパフォーマンスのバランスを取るために重要です。分離レベルには、Read Uncommitted、Read Committed、Repeatable Read、Serializable の4種類があり、それぞれ異なるレベルの保護を提供します。
READ UNCOMMITTED
他のトランザクションで変更されたデータを読み取ることができる分離レベル。他のトランザクションがコミットされる前のデータを読み取ることができるため、データの整合性が保証されません。
READ UNCOMMITTED
The isolation level that provides the least amount of protection between transactions. Queries employ a locking strategy that allows them to proceed in situations where they would normally wait for another transaction.
訳:トランザクション間で最も低い保護レベルを提供する分離レベルです。クエリは、通常なら他のトランザクションを待つような状況でも処理を進めることができるロッキング戦略を採用しています。
However, this extra performance comes at the cost of less reliable results, including data that has been changed by other transactions and not committed yet (known as dirty read).
訳:しかし、このパフォーマンス向上は信頼性の低い結果という代償を伴います。これには、他のトランザクションによって変更されたがまだコミットされていないデータ(ダーティリードとして知られる)も含まれます。
Use this isolation level with great caution, and be aware that the results might not be consistent or reproducible, depending on what other transactions are doing at the same time. Typically, transactions with this isolation level only do queries, not insert, update, or delete operations.
訳:この分離レベルは十分な注意を払って使用し、同時に他のトランザクションが何を行っているかによって、結果が一貫性を欠いたり再現不可能になる可能性があることを認識してください。
READ COMMITTED
この分離レベルは、データベースから読み取るデータが他のトランザクションによって変更されたデータを見ることはできませんが、現在のトランザクションが開始された後に他のトランザクションによってコミットされたデータを見ることができます。
READ COMMITTED
An isolation level that uses a locking strategy that relaxes some of the protection between transactions, in the interest of performance.
訳:パフォーマンスの観点から、トランザクション間の保護の一部を緩和するロック戦略を使用する分離レベル。
Transactions cannot see uncommitted data from other transactions, but they can see data that is committed by another transaction after the current transaction started.
訳:トランザクションは他のトランザクションからコミットされていないデータを見ることはできませんが、現在のトランザクションが開始した後に他のトランザクションによってコミットされたデータを見ることはできます。
Thus, a transaction never sees any bad data, but the data that it does see may depend to some extent on the timing of other transactions.
訳:したがって、トランザクションが悪いデータを見ることはありませんが、見るデータは他のトランザクションのタイミングにある程度依存する可能性があります。
When a transaction with this isolation level performs UPDATE … WHERE or DELETE … WHERE operations, other transactions might have to wait. The transaction can perform SELECT … FOR UPDATE, and LOCK IN SHARE MODE operations without making other transactions wait.
訳:この分離レベルを持つトランザクションがUPDATE … WHERE、DELETE … WHERE操作を実行すると、他のトランザクションを待たせる可能性があります。 トランザクションは、他のトランザクションを待たせることなく SELECT … FOR UPDATE および LOCK IN SHARE MODE 操作を実行することができます。
REPEATABLE READ
MySQL のデフォルトの分離レベルです。トランザクションが開始された時点のスナップショットを使用して、他のトランザクションによる変更を防ぎます。これにより、同じクエリを繰り返し実行しても、同じ結果が得られます。
REPEATABLE READ
The default isolation level for InnoDB. It prevents any rows that are queried from being changed by other transactions, thus blocking non-repeatable reads but not phantom reads.
訳:InnoDB のデフォルトの分離レベル。他のトランザクションによって変更されたクエリされた行を防ぎます。そのため、繰り返し不可能な読み取りはブロックされますが、ファントムリードはブロックされません。
It uses a moderately strict locking strategy so that all queries within a transaction see data from the same snapshot, that is, the data as it was at the time the transaction started.
訳:トランザクション内の全ての問い合わせが同じスナップショットのデータ、つまりトランザクションが開始した時点のデータを参照できるように、適度に厳密なロック戦略を使用します。
When a transaction with this isolation level performs UPDATE … WHERE, DELETE … WHERE, SELECT … FOR UPDATE, and LOCK IN SHARE MODE operations, other transactions might have to wait.
訳:この分離レベルを持つトランザクションが UPDATE … WHERE、DELETE … WHERE、SELECT … FOR UPDATE、および LOCK IN SHARE MODE 操作を実行すると、他のトランザクションを待たせる可能性があります。
SERIALIZABLE
もっともトランザクションの独立性が高い分離レベル。同時に実行されるトランザクションが互いに影響を与えないように、すべてのトランザクションが直列に実行されているかのように振る舞います。
SERIALIZABLE
The isolation level that uses the most conservative locking strategy, to prevent any other transactions from inserting or changing data that was read by this transaction, until it is finished.
訳:最も保守的なロック戦略を使用する分離レベル。このトランザクションによって読み取られたデータを、トランザクションが終了するまで他のトランザクションが挿入または変更できないようにします。
This way, the same query can be run over and over within a transaction, and be certain to retrieve the same set of results each time.
訳:このようにして、トランザクション内で同じクエリを何度も実行し、毎回同じ結果セットを取得できることが確実になります。
Any attempt to change data that was committed by another transaction since the start of the current transaction, cause the current transaction to wait.
訳:現在のトランザクションが開始されてから、他のトランザクションによってコミットされたデータを変更しようとすると、現在のトランザクションは待機します。
This is the default isolation level specified by the SQL standard. In practice, this degree of strictness is rarely needed, so the default isolation level for InnoDB is the next most strict, REPEATABLE READ.
訳:これは、SQL 標準で指定されたデフォルトの分離レベルです。実際には、この厳しさの程度はほとんど必要ないため、InnoDB のデフォルトの分離レベルは次に厳しい REPEATABLE READ です。
トランザクションで発生しうる問題
ダーティーリード (Dirty Read)
別のトランザクションが更新中であるコミットされていないデータを読み取ってしまう現象をダーティーリードと呼びます。
ダーティーリード
An operation that retrieves unreliable data, data that was updated by another transaction but not yet committed. It is only possible with the isolation level known as read uncommitted.
訳:他のトランザクションによって更新されたがまだコミットされていないデータを取得する操作。これは、未コミット読み取り(read uncommitted)として知られる分離レベルでのみ可能です。
This kind of operation does not adhere to the ACID principle of database design. It is considered very risky, because the data could be rolled back, or updated further before being committed; then, the transaction doing the dirty read would be using data that was never confirmed as accurate.
訳:このような操作は、データベース設計の ACID 原則に準拠していません。非常にリスクが高いと考えられており、そのデータがロールバックされる可能性や、コミットされる前にさらに更新される可能性があります。その結果、ダーティーリードを行うトランザクションは、正確であると確認されていないデータを使用することになります。
Its opposite is consistent read, where InnoDB ensures that a transaction does not read information updated by another transaction, even if the other transaction commits in the meantime.
訳:その反対が一貫した読み取り(consistent read)であり、InnoDB は、たとえ他のトランザクションがその間にコミットしたとしても、あるトランザクションが他のトランザクションによって更新された情報を読み取らないよう保証します。
シナリオ
ユーザーAが座席情報を取得し、ユーザーBが同じ予約情報を更新しようとした場合、ユーザーAは未コミットのデータを読み取ることになります。
実際にクエリを実行するときは、トランザクション分離レベルを READ UNCOMMITTED
に設定し、事象を確認します。
使用するクエリ
use example;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;START TRANSACTION;
SELECT customer_id, total_amount_yenFROM reservationsWHERE reservation_id = 1;
-- terminal-2のトランザクションによって-- UPDATEが実行される
SELECT customer_id, total_amount_yenFROM reservationsWHERE reservation_id = 1;
-- termian-2でROLLBACKされるCOMMIT;
use example;
START TRANSACTION;
UPDATE reservationsSET total_amount_yen = total_amount_yen - 1000WHERE reservation_id = 1;
ROLLBACK;
MySQLのデフォルトは REPEATABLE READ
なのでダーティリードが発生しないことを確認します。
右側のターミナルで更新クエリを実行し、左側のターミナルでSELECTクエリを実行しても、total_amount_yen
の値は変わらないことがわかります。
反復不能読み取り (Non-Repeatable Read)
あるトランザクションが同じ条件で複数回クエリを実行した際、他のトランザクションによるコミットが原因で、1回目と2回目以降で取得内容が変化してしまうような現象を反復不能読み取りと呼びます。(ファジーリードとも呼ばれる)
反復不能読み取り
The situation when a query retrieves data, and a later query within the same transaction retrieves what should be the same data, but the queries return different results (changed by another transaction committing in the meantime).
訳:クエリがデータを取得し、同じトランザクション内で後のクエリが本来同じであるはずのデータを取得するにもかかわらず、別のトランザクションがその間にコミットすることで、異なる結果が返される状況。
This kind of operation goes against the ACID principle of database design. Within a transaction, data should be consistent, with predictable and stable relationships.
訳:このような操作は、データベース設計の ACID 原則に反します。トランザクション内では、データは一貫性を持ち、予測可能で安定した関係であるべきです。
Among different isolation levels, non-repeatable reads are prevented by the serializable read and repeatable read levels, and allowed by the consistent read, and read uncommitted levels.
訳:異なる隔離レベルの中で、反復不能読み取り(non-repeatable read)は、シリアライズ可能読み取り(serializable read)および反復可能読み取り(repeatable read)レベルでは防がれ、一貫性読み取り(consistent read)およびコミットされていない読み取り(read uncommitted)レベルでは許可されます。
シナリオ
ユーザーが映画チケットの予約を進めている間に、キャンペーンが終了してしまうケースを考えます。たとえば、ユーザーが割引価格の座席を選択し、予約を進めている最中に、キャンペーンが終了して通常価格に戻った場合、ユーザーが最初に確認した価格と最終的に適用される価格が異なる状況が発生します。
実際にクエリを実行するときは、トランザクション分離レベルを READ COMMITTED
に設定し、事象を確認します。
使用するクエリ
以下は映画チケットの情報を取得するクエリです。
SELECT r.reservation_id, m.title, r.total_amount_yenFROM reservations rJOIN screenings s ON r.screening_id = s.screening_idJOIN movies m ON s.movie_id = m.movie_idWHERE r.reservation_id = 1;
以下は料金を更新するクエリです。
START TRANSACTION;
UPDATE reservationsSET total_amount_yen = 4000WHERE reservation_id = 1;
COMMIT;
左側のターミナルで2回目のSELECTを実行した結果、total_amount_yen = 4000
になっていることがわかります。
MySQLのデフォルトは REPEATABLE READ
なので反復不能読み取りが発生しないことを確認します。
右側のターミナルで更新クエリを実行し、左側のターミナルでSELECTクエリを実行しても、total_amount_yen = 3000
の値のままであることがわかります。
ファントムリード (Phantom Read)
同一トランザクション内で同じ検索条件を使用して複数回クエリを実行した際、別のトランザクションによる挿入や削除操作によって、検索結果の行数が変化する問題をファントムリードと呼びます。
ファントムリード
A row that appears in the result set of a query, but not in the result set of an earlier query.
訳:クエリの結果セットには表示されますが、それより前のクエリの結果セットには表示されなかった行です。
For example, if a query is run twice within a transaction, and in the meantime, another transaction commits after inserting a new row or updating a row so that it matches the WHERE clause of the query.
訳:例えば、トランザクション内でクエリを2回実行し、その間に別のトランザクションが新しい行を挿入するか、クエリのWHERE句に合致するように行を更新した後にコミットした場合です。
This occurrence is known as a phantom read. It is harder to guard against than a non-repeatable read, because locking all the rows from the first query result set does not prevent the changes that cause the phantom to appear.
訳:この現象はファントムリードとして知られています。最初のクエリ結果セットからすべての行をロックしても、ファントム現象を引き起こす変更を防げないため、反復不能読み取り(non-repeatable read)よりも防止が難しくなっています。
Among different isolation levels, phantom reads are prevented by the serializable read level, and allowed by the repeatable read, consistent read, and read uncommitted levels.
訳:さまざまな分離レベルの中で、ファントムリードはシリアライザブルレベルでは防止されますが、リピータブルリード、一貫性読み取り、およびリードアンコミッテッドレベルでは許容されます。
引用:phantom
シナリオ
ユーザーAが空席情報を取得し、ユーザーBが同じ座席を予約した場合、ユーザーAは最初の読み取りと2回目の読み取りで異なる空席情報を得る状況が発生します。
実際にクエリを実行するときは、トランザクション分離レベルを READ COMMITTED
に設定し、事象を確認します。
使用するクエリ
以下は空席情報を取得するクエリです。
SELECT s.seat_id, s.seat_row, s.seat_numberFROM seats sJOIN screens sc ON s.screen_id = sc.screen_idJOIN screenings sg ON sc.screen_id = sg.screen_idWHERE sg.screening_id = 1AND s.seat_id NOT IN ( SELECT rs.seat_id FROM reservation_seats rs JOIN reservations r ON rs.reservation_id = r.reservation_id WHERE r.screening_id = 1 AND r.status != 'cancelled');
以下は予約を作成するクエリです。
-- ターミナル2(ユーザーB)- 同時に実行START TRANSACTION;
-- 新しい予約を作成INSERT INTO reservations (customer_id, screening_id, reserved_at, status, total_amount_yen, payment_method)VALUES (3, 1, NOW(), 'confirmed', 1800, 'credit_card');
-- 予約IDを取得SET @new_reservation_id = LAST_INSERT_ID();
-- A列1番席(seat_id=101)を予約INSERT INTO reservation_seats (reservation_id, seat_id)VALUES (@new_reservation_id, 101);
COMMIT; -- このコミットがターミナル1の2回目のSELECTの前に行われると、反復不能読み取りが発生
左側のターミナルで2回目のSELECTを実行した結果、seat_id = 3
の座席が出力されないことが確認できます。
MySQLのデフォルト分離レベルは REPEATABLE READ
です。本来この分離レベルではファントムリードが発生しますが、InnoDB ストレージエンジンを使用している場合、Next-Key Lock を使用しているため、ファントムリードは発生しません。
右側のターミナルで更新クエリを実行し、左側のターミナルでSELECTクエリを実行しても、seat_id = 3
の値が取得できていることがわかります。
分離レベルと発生しうる問題の対応表
分離レベル | ダーティリード (Dirty Read) | 反復不能読み取り (Non-Repeatable Read) | ファントムリード (Phantom Read) |
---|---|---|---|
Read Uncommitted | 許容 | 発生する可能性あり | 発生する可能性あり |
Read Committed | 不可 | 許容 (同じクエリを再度実行すると同じ結果とは限らない) | 発生する可能性あり |
Repeatable Read | 不可 | 不可 (同じクエリを再度実行すると同じ結果を保証) | MySQL(InnoDB)では一部発生の可能性 PostgreSQL では不可の場合が多い |
Serializable | 不可 | 不可 | 不可 |