Skip to content

MySQLを使ってトランザクションを学ぶ

はじめに

モダンなアプリケーション開発では、O/R Mapper や Web フレームワークによってトランザクション制御が抽象化されているため、開発者がトランザクションを直接意識する機会は少なくなっています。しかし、パフォーマンスチューニングやデータの整合性を確実に確保するためには、トランザクションの仕組みを根本から理解することが不可欠です。

今回は、トランザクション分離レベルについて学習した内容をまとめます。実際の開発現場ではあまり意識することがなくても、トランザクション分離レベルに起因する問題は突然発生する可能性があり、状況に適切に対処するための実践的な知識が重要です。

一般的なWeb記事では、トランザクションの基本的な分離レベルの説明や、単純な操作方法の解説が多く見られます。しかし、それらには以下の課題があると感じています。

  • 単にクエリとその実行結果を羅列しているだけ
  • 視覚的な図解による説明が不足している

この記事では、MySQLに焦点を当てながら、シーケンス図とgifを用いてトランザクション分離レベルについて学習した内容を備忘録としてまとめます。

成果物

https://github.com/kntks/blog-code/tree/main/2025/04/db-transaction

使用バージョン

バージョン
MacSequoia 15.2
Docker27.5.1
Docker Composev2.32.4
MySQL8.4.4

テーブル定義

この記事では、映画館の予約システムを想定したデータベースを使用します。

テーブル名説明
theaters劇場情報を格納するテーブル
screensスクリーン情報を格納するテーブル
movies映画情報を格納するテーブル
screenings上映情報を格納するテーブル
customers顧客情報を格納するテーブル
seats座席情報を格納するテーブル
reservations予約情報を格納するテーブル
reservation_seats予約座席を格納するテーブル

環境構築

docker compose up コマンドで MySQL を起動します。

Terminal window
docker compose up -d

トランザクションの基礎

トランザクションは、コミットまたはロールバック可能なアトミックな作業単位です。 トランザクションがデータベースに複数の変更を加えると、トランザクションがコミットされたときにすべての変更が成功するか、トランザクションがロールバックされたときにすべての変更が取り消されます。

トランザクションがあることでシステム障害やハードウェア故障が発生した場合でも、トランザクションのロールバック機能によってデータベースを一貫性のある状態に戻すことができます。

参考:transaction - MySQL

分離レベル

分離レベルは、トランザクションが他のトランザクションの影響をどの程度受けるかを決定する設定です。データの整合性とパフォーマンスのバランスを取るために重要です。分離レベルには、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 UNCOMMITTED - MySQL

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 操作を実行することができます。

READ COMMITTED - MySQL

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 操作を実行すると、他のトランザクションを待たせる可能性があります。

引用:REPEATABLE READ - MySQL

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 です。

引用: SERIALIZABLE - MySQL

トランザクションで発生しうる問題

ダーティーリード (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 は、たとえ他のトランザクションがその間にコミットしたとしても、あるトランザクションが他のトランザクションによって更新された情報を読み取らないよう保証します。

引用:dirty read - MySQL

シナリオ

ユーザーAが座席情報を取得し、ユーザーBが同じ予約情報を更新しようとした場合、ユーザーAは未コミットのデータを読み取ることになります。

実際にクエリを実行するときは、トランザクション分離レベルを READ UNCOMMITTED に設定し、事象を確認します。

使用するクエリ
ターミナル1
use example;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT customer_id, total_amount_yen
FROM reservations
WHERE reservation_id = 1;
-- terminal-2のトランザクションによって
-- UPDATEが実行される
SELECT customer_id, total_amount_yen
FROM reservations
WHERE reservation_id = 1;
-- termian-2でROLLBACKされる
COMMIT;
ターミナル2
use example;
START TRANSACTION;
UPDATE reservations
SET total_amount_yen = total_amount_yen - 1000
WHERE reservation_id = 1;
ROLLBACK;

ダーティーリード

MySQLのデフォルトは REPEATABLE READ なのでダーティリードが発生しないことを確認します。
右側のターミナルで更新クエリを実行し、左側のターミナルでSELECTクエリを実行しても、total_amount_yen の値は変わらないことがわかります。

ダーティーリード2

反復不能読み取り (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)レベルでは許可されます。

引用:non-repeatable read - MySQL

シナリオ

ユーザーが映画チケットの予約を進めている間に、キャンペーンが終了してしまうケースを考えます。たとえば、ユーザーが割引価格の座席を選択し、予約を進めている最中に、キャンペーンが終了して通常価格に戻った場合、ユーザーが最初に確認した価格と最終的に適用される価格が異なる状況が発生します。

実際にクエリを実行するときは、トランザクション分離レベルを READ COMMITTED に設定し、事象を確認します。

使用するクエリ

以下は映画チケットの情報を取得するクエリです。

SELECT r.reservation_id, m.title, r.total_amount_yen
FROM reservations r
JOIN screenings s ON r.screening_id = s.screening_id
JOIN movies m ON s.movie_id = m.movie_id
WHERE r.reservation_id = 1;

以下は料金を更新するクエリです。

START TRANSACTION;
UPDATE reservations
SET total_amount_yen = 4000
WHERE reservation_id = 1;
COMMIT;

左側のターミナルで2回目のSELECTを実行した結果、total_amount_yen = 4000 になっていることがわかります。

反復不能読み取り

MySQLのデフォルトは REPEATABLE READ なので反復不能読み取りが発生しないことを確認します。
右側のターミナルで更新クエリを実行し、左側のターミナルでSELECTクエリを実行しても、total_amount_yen = 3000 の値のままであることがわかります。

反復不能読み取り2

ファントムリード (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_number
FROM seats s
JOIN screens sc ON s.screen_id = sc.screen_id
JOIN screenings sg ON sc.screen_id = sg.screen_id
WHERE sg.screening_id = 1
AND 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 の座席が出力されないことが確認できます。

phantom read

MySQLのデフォルト分離レベルは REPEATABLE READ です。本来この分離レベルではファントムリードが発生しますが、InnoDB ストレージエンジンを使用している場合、Next-Key Lock を使用しているため、ファントムリードは発生しません。

右側のターミナルで更新クエリを実行し、左側のターミナルでSELECTクエリを実行しても、seat_id = 3 の値が取得できていることがわかります。

phantom read2

分離レベルと発生しうる問題の対応表

分離レベルダーティリード (Dirty Read)反復不能読み取り (Non-Repeatable Read)ファントムリード (Phantom Read)
Read Uncommitted許容発生する可能性あり発生する可能性あり
Read Committed不可許容 (同じクエリを再度実行すると同じ結果とは限らない)発生する可能性あり
Repeatable Read不可不可 (同じクエリを再度実行すると同じ結果を保証)MySQL(InnoDB)では一部発生の可能性
PostgreSQL では不可の場合が多い
Serializable不可不可不可

参考