トランザクションの ACID 特性とは?わかりやすく解説

トランザクショントランザクションとは、複数の処理を1まとめに処理することです。
ACID 特性ACID 特性とは、トランザクションが持つ次の4つの特性です。
ACID 特性説明
Atomicity (原子性)トランザクションの処理を分割しない (全て実行する/しない)
Consistency (一貫性)トランザクション前後で制約を満たす
Isolation (分離性)他のトランザクションの影響を受けない
Durability (永続性)トランザクションを COMMIT した結果が失われない

本記事では、MySQL を例に説明をします。

関連記事:データベースの基礎知識編
学習ロードマップ
スポンサーリンク

Atomicity (原子性)

Atomicity (原子生)Atomicity とは、トランザクションの実行結果が、以下のどちらかになる特性です。
・トランザクションに含まれるすべての操作を実行しない (ROLLBACK)
・トランザクションに含まれるすべての操作を実行する (COMMIT)

つまり、Atomicity は処理が途中で終わることを禁止する特性です。

すべての操作を実行しない (ROLLBACK)

すべての操作を実行する (COMMIT)

Atomicity が無い場合、途中で終了すると、①の口座からお金が消え、②の口座に届きません。

すべての操作を実行しない (ROLLBACK)

ROLLBACK とは、トランザクションを開始する前の状態に戻すことです。

ROLLBACK はなんらかの問題が発生した場合に、トランザクション開始前の状態に戻します。

動作確認

以下の検証用テーブルを利用して、Atomicity (原子性) の動作を確認してみます。

CREATE DATABASE IF NOT EXISTS test;
USE test;
CREATE TABLE account(name VARCHAR(20), balance INT);
INSERT INTO account VALUES('Taro',2000);
INSERT INTO account VALUES('Hanako',1000);
SELECT * FROM account;
+--------+---------+
| name   | balance |
+--------+---------+
| Taro   |    2000 |
| Hanako |    1000 |
+--------+---------+

Taro から Hanako に 500 円を送金するために、以下のトランザクション処理をします。

  1. Taro の銀行口座から 500 円を減らす
  2. Hanako の銀行口座に 500 円を増やす
BEGIN; #トランザクションを開始するコマンド
UPDATE account SET balance = balance - 500 WHERE name = 'Taro';
SELECT * FROM account;
+--------+---------+
| name   | balance |
+--------+---------+
| Taro   |    1500 |
| Hanako |    1000 |
+--------+---------+

ここで障害が発生し、「2. Hanako の銀行口座に 500 円を増やす」が出来ない場合を考えます。

ROLLBACK を利用して、トランザクション開始前の状態に戻します。

SELECT * FROM account;
+--------+---------+
| name   | balance |
+--------+---------+
| Taro   |    2000 |
| Hanako |    1000 |
+--------+---------+

ROLLBACK でトランザクション開始前の状態に戻るため、Atomicity を満たします。

すべての操作を実行する (COMMIT)

COMMIT とは、トランザクション処理の変更を確定することです。

動作確認

Taro から Hanako に 500 円を送金するために、以下のトランザクション処理をします。

  1. Taro の銀行口座から 500 円を減らす
  2. Hanako の銀行口座に 500 円を増やす
SELECT * FROM account;
+--------+---------+
| name   | balance |
+--------+---------+
| Taro   |    2000 |
| Hanako |    1000 |
+--------+---------+
BEGIN;
UPDATE account SET balance = balance - 500 WHERE name = 'Taro';
UPDATE account SET balance = balance + 500 WHERE name = 'Hanako';
SELECT * FROM account;
+--------+---------+
| name   | balance |
+--------+---------+
| Taro   |    1500 |
| Hanako |    1500 |
+--------+---------+

上記のトランザクションは COMMIT して確定いるため、ROLLBACK で戻すことはできません。

SELECT * FROM account;
+--------+---------+
| name   | balance |
+--------+---------+
| Taro   |    1500 |
| Hanako |    1500 |
+--------+---------+

全ての変更が確定しているため、Atomicity を満たします。

スポンサーリンク

Consistency (一貫性)

Consistency (一貫性)Consistency とは、トランザクション前後で整合性 (制約) を満たす特性です。

Consistency の定義は曖昧ですが、本記事はデータベース制約について記載します。
(過去のトランザクションの結果を保証する方法は、Durability の記事で説明します。)

なお、制約内容はアプリの内容に依存するため、基本的にアプリ側の責任となります。

Consistency is one of the four guarantees that define ACID transactions; however, significant ambiguity exists about the nature of this guarantee. It is defined variously as:

  • The guarantee that database constraints are not violated, particularly once a transaction commits.[2][3][4][5][6]
  • The guarantee that any transactions started in the future necessarily see the effects of other transactions committed in the past.[7][8]
https://en.wikipedia.org/wiki/Consistency_(database_systems)

データベース側で Consistency を保つ場合は、次のような制約を設定します。

トランザクションで制約を違反した場合、違反した SQL 文に対して ROLLBACK します。

通常、データ変更ステートメント (INSERTUPDATE など) がプライマリキー、一意キー、または外部キーの制約に違反すると、エラーが発生します。InnoDB などのトランザクションストレージエンジンを使用している場合、MySQL ではステートメントが自動的にロールバックされます。

https://dev.mysql.com/doc/refman/8.0/ja/constraint-primary-key.html

PRIMARY KEY(主キー)

PRIMARY KEY とは、列の値が一意かつ NULL を禁止する制約です。

動作確認

CREATE TABLE consistency1(account_id INT NOT NULL PRIMARY KEY, name VARCHAR(20));
BEGIN;
INSERT INTO consistency1 VALUES(1,'hoge');
INSERT INTO consistency1 VALUES(1,'tech');
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'

PRIMARY KEY が重複した場合は ROLLBACK するため、Consistency を満たしています。

SELECT * FROM consistency1;
+------------+------+
| account_id | name |
+------------+------+
|          1 | hoge |
+------------+------+

FOREIGN KEY(外部キー)

FOREIGN KEY とは、関連テーブルにある値のみ格納可能な制約です。
アカウントテーブルに存在しない会員 ID で、入出金テーブルに預金残高を登録禁止

動作確認

CREATE TABLE consistency2 (
foreign_account_id INT,
balance INT,
FOREIGN KEY (foreign_account_id) REFERENCES consistency1(account_id));
BEGIN;
INSERT INTO consistency2 VALUES(2,3000);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`test`.`consistency2`, CONSTRAINT `consistency2_ibfk_1` FOREIGN KEY (`foreign_account_id`) REFERENCES `consistency1` (`account_id`))

consistency1 に存在しない会員 ID は ROLLBACK するため、Consistency を満たします。

SELECT * FROM consistency2;
Empty set (0.00 sec)

無効なデータ

無効なデータ とは、特定のデータの格納を禁止する制約です。

今回は UNSIGNED 属性 (負の整数を禁止) で検証します。

動作確認

CREATE TABLE consistency3(balance INT UNSIGNED);
BEGIN;
INSERT INTO consistency3 VALUES(-1);
ERROR 1264 (22003): Out of range value for column 'balance' at row 1

無効データの挿入を禁止して、Consistency を満たしています。

SELECT * FROM consistency3;
Empty set (0.00 sec)

ENUM および SET

ENUM および SET とは、特定の値のみを列に格納できる制約です。

動作確認

CREATE TABLE consistency4(grade ENUM('bronze', 'silver', 'gold'));
BEGIN;
INSERT INTO consistency4 VALUES('blue');
ERROR 1265 (01000): Data truncated for column 'grade' at row 1

デフォルトの SQL モードでは、ENUM に無い値はエラーとなり、Consistency を満たします。

SELECT * FROM consistency4;
Empty set (0.00 sec)
スポンサーリンク

Isolation (分離性)

IsolationIsolation とは、他のトランザクション処理の影響 (リード現象) を受けない特性です。

リード現象

リード現象とは、読み取り時に、他のトランザクションの影響によって発生する現象です。

一般的なリード現象は次の 3 つです。

ダーティーリード

ダーティーリードとは、COMMIT していない別のトランザクションの結果が見える現象です

ノンリピータブルリード

ノンリピータブルリードとは、別のトランザクションの更新により、同じデータを読み取っても、異なる値を取得する現象です。

トランザクションが分離されているなら、右側は COMMIT するまで同じ値を返すべきである。
(右側は COMMIT するまで、左側のトランザクションの影響を受けないことが期待される)

ファントムリード

ファントムリードとは、別のトランザクションの追加・削除により、同じ検索条件でも、取得する行数が異なる現象です。

リード現象の対策

リード現象を防ぐために、RDBMS には「MVCC」と「ロック」の2つの機能があります。

MVCC (MultiVersion Concurrency Control) とは

MVCC とは、変更前の行 (スナップショット) を Undo ログに残す仕組みです。

MVCC は、行に以下の3つのフィールドを追加することで実現します。

フィールド説明
DB_TRX_ID行を挿入/更新(削除)したトランザクションの識別子
DB_ROLL_PTRUndo レコードへのポインタ
DB_ROW_ID行の識別子

MVCC では、過去のスナップショット (変更前の行) を読み取ることで、リード現象を防ぎます。

自分の DB_TRX_ID より新しい行は見えない (DB_ROLLPTR で更新 Undo ログを遡る)

なお、Undo ログロールバックでも利用します。

ロックとは

ロックとは、競合/不整合を防ぐために、読み書きを制限することです。

先にロックが取得されている場合は、後続はロックの解除を待つ必要があります。

ロックの範囲

ロックの範囲 (LOCK_TYPE) には、行ロックと、テーブルロックが存在します。

ロックの種類

ロックの種類 (LOCK_MODE) には、次の2種類が存在します。

ロックの種類該当する SQL説明
共有ロック (S)SELECT ... FOR SHARE読み取りのため
排他ロック (X)SELECT ... FOR UPDATE/INSERT/UPDATE/DELETE読み書きのため
ロックの取得条件

すでに誰かがロックを取得している場合、後から取得するロックには、次の制限があります。

先/後共有ロック (S)排他ロック (X)
共有ロック (S)×
排他ロック (X)××

つまり、誰かが読み取り中のレコードは書き込めませんし、誰かが書き込み中のレコードは読み書きできません。(ロックが解放されるまで待つ必要があります)

これにより、リード現象を防ぎます。

ロック読み取りと一貫性読み取り

ロックの解除待ちが発生するため、ロックはなるべくしたくないです。

そのため、ロックを取得しない一貫性読み取り (SELECT 文) が存在します。

読み取りの種類SQLMVCC 読み取り先ロック
一貫性読み取り
(Consistent Read)
SELECTスナップショットロックしない
ロック読み取り
(Locking Read)
SELECT ... FOR SHARE現在のテーブル共有ロック
SELECT ... FOR UPDATE現在のテーブル排他ロック

一貫性読み取りでは、ロックの代わりに MVCC のスナップショットでリード現象を防ぎます。

分離レベル

分離レベル分離レベルとは、リード現象をどの程度許容するか定義したものです。

トランザクションを並行で実行すると、他のトランザクションでリード現象が発生します。

一方で、パフォーマンスの観点から、トランザクションは可能な限り並行で実行したいです。

そのため、分離レベルを用いて、パフォーマンスと Isolation のバランスを決定します。

分離レベルダーティーリードノンリピータブルリードファントムリード
READ UNCOMMITTED発生する発生する発生する
READ COMMITTED発生しない発生する発生する
REPEATABLE READ発生しない発生しない△※1
SERIALIZABLE発生しない発生しない発生しない
※1 ANSI では「発生する」。
MySQL (InnoDB) の場合は、一貫性読み取りとロック読み取りの1種類だけだと「発生しない」。組み合わせた場合は「発生する」
一貫性読み取りは MVCC, ロック読み取りはギャップロック/ネクストキーロックが防ぐ

なお、分離レベルは以下のグローバル変数・セッション変数で確認可能です。

select @@global.tx_isolation, @@session.tx_isolation;
+-----------------------+------------------------+
| @@global.tx_isolation | @@session.tx_isolation |
+-----------------------+------------------------+
| REPEATABLE-READ       | REPEATABLE-READ        |
+-----------------------+------------------------+

SERIALIZABLE

SERIALIZABLE とは、直列に実行 (各トランザクションを順番に順番) した場合と、同じ結果を保証する分離レベルです。

SERIALIZABLE は、全てのリード現象を防ぎます。

MySQL (InnoDB) の SERIALIZABLE では、すべての SELECT 文で共有ロックを取得 (SELECT ... FOR SHARE に変換) することで SERIALIZABLE を保証します。

SERIALIZABLE
このレベルは REPEATABLE READ と似ていますが、autocommit が無効になっている場合、InnoDB はすべてのプレーン SELECT ステートメントを SELECT ... FOR SHARE に暗黙的に変換します。 autocommit が有効な場合、SELECT は独自のトランザクションです。

https://dev.mysql.com/doc/refman/8.0/ja/innodb-transaction-isolation-levels.html

REPEATABLE READ

REPEATABLE READ とは、同じトランザクション内で SELECT を実行すると、常に同じ結果となることが保証されている分離レベルです。

REPEATABLE READ は、ANSI の定義上ファントムリードが発生します。

しかし、MySQL (InnoDB) の場合、「一貫性読み取り」と「ロック読み取り」が混在しない場合は、ファントムリードが発生しません。(混在する場合は発生します)

読み取りの種類SQLファントムリードを防ぐ方法
一貫性読み取り
(Consistent Read)
SELECT最初に SELECT を実行した時の
MVCC のスナップショット
ロック読み取り
(Locking Read)
SELECT ... FOR SHARE共有ロック
(ギャップロック/ネクストキーロック)
SELECT ... FOR UPDATE排他ロック
(ギャップロック/ネクストキーロック)
一意検索 (WHERE id = 1) ではレコードロック、範囲検索 (WHERE id > 5) ではギャップロック/ネクストキーロックを使う

READ COMMITTED

READ UNCOMMITTED とは、他のトランザクションが COMMIT した結果を読み取る分離レベルです。

READ COMMITTED では、ファントムリードとノンリピータブルリードが発生します。

MySQL の READ COMMITTED では、以下の理由でファントムリードが発生
・一貫性読み取りでは、常に最新のスナップショットを取得するため
ロック読み取りでは、ギャップロックを行わないため

ファントムリードの動作
CREATE TABLE isolation(id INT);
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
INSERT INTO isolation VALUES(1);
SELECT * FROM isolation;
+------+
| id   |
+------+
|    1 |
+------+
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM isolation;
Empty set (0.00 sec)
SELECT * FROM isolation;
+------+
| id   |
+------+
|    1 |
+------+
ノンリピータブルリードの動作
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
UPDATE isolation SET id=2 WHERE id=1;
SELECT * FROM isolation;
+------+
| id   |
+------+
|    2 |
+------+
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM isolation;
+------+
| id   |
+------+
|    1 |
+------+
SELECT * FROM isolation;
+------+
| id   |
+------+
|    2 |
+------+

READ UNCOMMITTED

READ UNCOMMITTED とは、トランザクションが全く分離していない分離レベルです。

READ UNCOMMITTED では、ファントムリードとノンリピータブルリードに加えて、ダーティリードが発生します。

ダーティリードの動作
CREATE TABLE dirty(id INT);
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
INSERT INTO dirty VALUES(1);
SELECT * FROM dirty;
+------+
| id   |
+------+
|    1 |
+------+
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT * FROM dirty;
+------+
| id   |
+------+
|    1 |
+------+

Durability (永続性)

DurabilityDurability とは、トランザクションを完了 (COMMIT) した結果が失われない特性です。

トランザクション処理を COMMIT した結果が失われる状況は、主に次の2種類が考えられます。

障害の種類説明対策
システム障害OS や電源の障害等で、メモリ上のデータが消失ロールフォワード
- WAL (Redo ログ)
- チェックポイント
ロールバック
- ロールバックセグメント
ダブルライトバッファ
メディア障害ストレージの破損等で、メディア上のデータが消失バックアップ
Point-in-Time リカバリ
レプリケーション

なお、Durability は一般的に「システム障害」を対象とすることが多いようです。
(※メディア障害への対策は、バックアップレプリケーション記事をご覧ください)

Durability can be achieved by flushing the transaction's log records to non-volatile storage before acknowledging commitment.

https://en.wikipedia.org/wiki/Durability_(database_systems)

In addition, this recovery must be permanent, meaning all transactions must be reconstructed, even if the database server crashes due to OS failure or power loss.

https://www.techopedia.com/definition/27416/durability-databases

システム障害への対策

システム障害への対策で基本的な考え方は、ストレージにデータを保存することです。
(電源が落ちるとメモリ上のデータは消えますが、ストレージ上のデータは残るため)

しかし、パフォーマンスの観点から、高速に読み書き可能なメモリにデータを置きたいです。

このデータの保存とパフォーマンスをうまくやりとりするのが RDBMS の役割です。

メモリとストレージが持つデータ

RDBMS におけるメモリストレージで保持するデータは、以下のとおりです。

用語説明
バッファプール ※1テーブルとインデックスなどを保持するメモリ上のキャッシュ
データファイルテーブルとインデックスなどを保持するストレージ上のファイル
ログバッファ ※2トランザクションの API コールを記録したメモリ上のバッファ
Redo ログ ※3トランザクションの API コールを記録したストレージ上のファイル
フラッシュメモリからストレージに書き込むこと
ページメモリを一定サイズに分割した単位
メモリストレージ間で、一度に転送できるデータ量
・複数の行データを格納可能
LRU アルゴリズムで使用頻度の低いページは削除
ダーティページ更新したが、まだデータファイルに書き込まれていないページ
※1 PostgreSQL では共有バッファ、Oracle ではデータベースバッファキャッシュと呼ぶ。MySQL の実態は ibd ファイル
※2 PostgreSQL ではWAL バッファ、Oracle ではログバッファと呼ぶ
※3 PostgreSQL WAL ログ、Oracle ではトランザクションログと呼ぶ。MySQL では ib_logfileN が実態。

MySQL (innodb) の対応するアーキテクチャ

https://dev.mysql.com/doc/refman/8.0/ja/innodb-architecture.html

※テーブルスペース = InnoDB テーブルやインデックスを保持するデータファイル

上記を踏まえて、システム障害から復旧するクラッシュリカバリについて解説します。

クラッシュリカバリ

クラッシュリカバリは、システム障害から復旧する方法で、主に次の要素で構成されます。

また、クラッシュリカバリは、ダブルライトバッファで書き込み途中のクラッシュも検知します。

WAL (Write Ahead Log)

WAL とは、テーブルの変更などを、データファイルより先にログに書き込むことです。

事前にロギングを行う仕組みは全てWALと呼ばれる。MySQLで言えばInnoDBにおいてRedoログとして利用されるログもWALであるし、MySQL Clusterのディスクテーブル利用時のUndoログだってWALの一種である。

https://nippondanji.blogspot.com/2009/03/wal.html
バッファプールは行データなどテーブルの実際のデータを持ち、ログバッファはトランザクション操作を持ちます

なお、Redo ログへの書き込みが完了した時点で、COMMIT が成功した通知をクライアントに返します。(データファイルには、まだ書き込みをしてません。バイナリログも COMMIT で更新)

WAL によるシステム障害への対策

WAL でデータファイルより先にログへ書き込むことにより、システム障害を次のように防ぎます。

システム障害のタイミング対応
Redo ログに書き込み中Redo の内容を捨てる
データファイルにまだ書き込んでないので問題なし
データファイル書き込み中Redo ログから COMMIT した内容をデータファイルに復元
(ロールフォワードと言います)
WAL によるパフォーマンスの向上

WAL は、システム障害への対策だけではなく、COMMIT 時のパフォーマンスが良くなります。
(最後、データファイルにフラッシュする際のコストは別)

種類Redo ログデータファイル
アクセスシーケンシャルアクセス
(トランザクション操作を追記するだけ)
ランダムアクセスが多い
(実データを更新するため)
書き込み箇所少ない (ログファイルのみ)多い (変更した全てのデータファイル)
速度早い遅い

チェックポイント処理

チェックポイント処理とは、バッファプールからデータファイルにダーティページを書き込む (フラッシュ) することです。

これにより、メモリからストレージページが書き出され、永続化します。

チェックポイントを実施するタイミングはこちら
COMMIT は Redo ログに書き込まれるだけで、チェックポイント処理 (データファイルへのフラッシュ) は別で行います

この時、Redo ログに LSN (ログシーケンス番号) を書き込みます。

LSN (ログシーケンス番号)
LSN とは、バッファプールをどこまでデータファイルに書き込んだか示す番号です。

上記の場合、LSN=1 までの変更がデータファイルに書き込み完了していることがわかります。

ここでシステム障害が発生した場合、Redo ログの LSN:2 をデータファイルに適用します。

動作確認
delimiter //
CREATE PROCEDURE insert_loop(max int)
BEGIN
 DECLARE id INT DEFAULT 0;
 REPEAT
  SET id = id + 1;
  INSERT INTO durability VALUES(id);
  IF(MOD(id, 10000) = 0) THEN COMMIT;
  END IF;
 UNTIL id >= max
 END REPEAT;
END//
delimiter ;
SET AUTOCOMMIT = 0;
CREATE TABLE durability(id int);
CALL insert_loop(500000);
SHOW ENGINE INNODB STATUS\G
---
LOG
---
Log sequence number          153604788
Log buffer assigned up to    153604788
Log buffer completed up to   153604788
Log written up to            153604028
Log flushed up to            153604028
Added dirty pages up to      153604788
Pages flushed up to          144025476
Last checkpoint at           144025476

以下の内容が確認できます。

  • LSN (Log sequence number) の値
  • ログバッファは最新情報
  • Redo ログの書き込みは、ログバッファより少し遅れている (COMMIT まで書き込まない)
  • バッファプールにあるダーティページは最新の LSN
  • Redo ログに先、データファイルに後に書き込んでいる (WAL)
  • チェックポイントレコードは、データファイルにフラッシュした最新の LSN

なお、ダーティページの数は以下のコマンドで確認できます。

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| Innodb_buffer_pool_pages_dirty | 452   |
+--------------------------------+-------+

ロールバックセグメント

ロールバックセグメントとは、データを元に戻すための Undo ログを記録する領域です。
undo ログの中身はここ

ダーティページは、チェックポイントで定期的にデータファイルへフラッシュされます。
※チェックポイントを実施するタイミングはこちら

COMMIT を行う前にシステム障害が発生すると、データファイルから COMMIT 前のデータをロールバックする (更新前のデータに戻す) 必要があります。

ロールバックを実現するために、ロールバックセグメントに更新前のデータを持ちます。
(なお、更新前のデータは MVCC でも利用します)

ダブルライトバッファ

ダブルライトバッファとは、データファイルに書き込む前に、小さなファイルに書き込みます

※ダブルライトバッファはストレージにある小さなファイル (2MB 程度) です。

ダブルライトバッファに書き込む理由はデータファイルのデータを保護するためです。

データファイルの書き込み途中でシステム障害が発生すると、Redo ログでも復旧できません。
(壊れたデータに、Redo ログのトランザクション操作をしてもデータが復元できない)

そこで、ダブルライトバッファを使って、以下のようにデータファイルを保護します。

システム障害のタイミング対処
ダブルライトバッファの書き込み中ダブルライトバッファを捨てるだけ
(データファイルには、まだ書き込んでないので問題なし)
データファイルへの書き込み中ダブルライトバッファからデータファイルを復元

関連記事

学習ロードマップ
関連記事:データベースの基礎知識編
関連記事:データベース設計

参考記事

MySQL :: MySQL 8.0 リファレンスマニュアル
MySQL/Postgres におけるトランザクション分離レベルと発生するアノマリーを整理する