PostgreSQL のトランザクションの実装について調査したときのメモ書き。
ACID
- Atomicity (原子性)
- それ以上分解できない単位の操作
- ALL or NOTHING
- Consistency (一貫性、整合性)
- 予め定められたルールに則った状態である
- 正の値しかとらない、など
- よくわからん
- Isolation (分離性、独立性)
- 実行中のトランザクションが他のトランザクションに影響しない
- 実行中のトランザクションの状態を外から参照、変更することはできない
- Durability (永続性)
- 一度コミットされたトランザクションは、何があっても残る
- 障害が発生しても、コミットされたトランザクションの結果は残る
Consistency がよくわからん。制約の話をしている?
Durability は技術的な限界がある。変更されたデータがどのようなデバイス に存在するのか。例えばローカルディスクなのか、ネットワーク越しの遠い場 所にあるのか、いずれにせよ、伝送のタイミングで消失してしまう可能性があ る。「何があっても残る」というのは、現実的には完全に実現することは難し い。
レコードの可視性
- PostgreSQL のテーブルの中には、複数バージョンのタプルが存在する
- 最初は一行、更新されるたびに行が追加されていく
- 各タプルは、自身を「作成したトランザクション」と「削除したトランザ クション」の情報を持つ
この仕組みにより Isolation を実現。複数のトランザクションにデータをう まく見せる。
Atomicity (原子性)
コミット済みのデータだけを処理対象とする。コミット済みかどうかは、タプ ルのヘッダを見れば分かる。
- タプルごとの可視性情報
- コミットログ (clog) によるトランザクションの状態情報
この辺を使う。
前者は前述の通り。タプルの生き死にが分かる。
後者はトランザクションの実行状態が分かる。タプルからは自身を作成したト ランザクションへのポインタがあるので、そのトランザクションがコミット済 みかどうか判断できれば、タプルがコミット済みかどうかも判断できる。
ちなみにアボートされたトランザクションの情報も残るらしい。当然、そのト ランザクションから作成されたタプルは処理対象外となる。
Consistency (一貫性)
制約の話。
- ステートメント実行時に各種制約がチェックされる
- ものによってはコミット時まで遅延される
- SET CONSTRAINTS {ALL | name} {DEFERRED | IMMEDIATE} なるものがあるらしい
- https://www.postgresql.jp/document/9.4/html/sql-set-constraints.html
制約のチェックで引っかかった場合は、その時点でトランザクションをアボー トする。
Isolation (分離性)
かの有名な MVCC (MultiVersion Concurrency Control) の話。
- Snapshot によるトランザクションの世代管理
- XID と CommandId によるものらしい
- Snapshot とは:
- トランザクションごとに生成
- 可視性の情報を持つ (何が見える、何が見えない)
- タプルヘッダの t_xmin, t_xmax あたりが関係してそう
- Visibility はトランザクションの分離レベルによっても変わる
- Read Committed, Repeatable Read, Serializable
- https://www.postgresql.jp/document/9.4/html/transaction-iso.html
MVCC 「で」実現しているではなく、MVCC 「を」実現しているということらしい。 その実現方法は Snapshot によるトランザクションの世代管理ですよ、と。
Snapshot のデータ構造 (SnapshotData 構造体) において重要な項目は以下:
- xmin
- トランザクションが始まる時点で、ある XID 以下のトランザクションは 終了している (Committed Or Aborted) ことが保証されている。その XID を持つ項目
- xmax
- トランザクションが始まる時点で、ある XID 以上のトランザクションが 始まっていないか進行中である。これらは無視してよい。端的には、自 身の XID 以上のトランザクションは無視してよい。
- xip[]
- xmin と xmax の間の進行中トランザクションの XID を持つ配列。なぜ clog を見る、ではだめなのか?
あるタプルが見えるかどうかの判定:
- 無効なタプル (削除 XID が入っている) なら見えない
- タプルの XID が自身と同じなら見える
- タプルの XID が xmax 以上なら見えない
- タプルの XID が xmin 以下で、CLOG を参照してトランザクションが Committed なら見える
- タプルの XID が xmin < XID < xmax で、xip 内に存在するなら見えない
- CLOG を参照してトランザクションが Committed なら見える
Snapshot をとるタイミングは分離レベルによって変わる。
- Read Committed
- 文を実行するたび
- Repeatable Read
- 最初の文を実行する時
以下の一連の文を実行する時を考える:
1 BEGIN;
2 INSERT INTO t1 values (1);
3 SELECT * FROM t1;
4 INSERT INTO t1 values (2);
5 COMMIT;
Read Committed では 2, 3, 4 のタイミングで Snapshot をとる。Repetable Read では 2 のタイミングでのみ Snapshot をとる。BEGIN ではなく最初の文 を実行するタイミングで Snapshot をとるのは、性能上の理由とのこと。
トランザクションの分離レベル
- Read Uncommitted
- 他のトランザクションがコミットしていない内容が見える (Dirty Read)
- Read Committed
- 他のトランザクションがコミットしていない内容が見えない
- 他のトランザクションがコミットした変更が途中から見える (Unrepeatable Read)
- Repeatable Read
- 他のトランザクションがコミットしていない内容が見えない
- 他のトランザクションがコミットした変更が途中から見えない
- 他のトランザクションがコミットした追加・削除が見える (Phantom Read)
- ただし PostgreSQL では発生しない、とのこと
- Serializable
- 他のトランザクションがコミットしていない内容が見えない
- 他のトランザクションがコミットした変更が途中から見えない
- 他のトランザクションがコミットした追加・削除が見えない
上に行くほど緩く、下に行くほど厳しくなる。厳しくなればなるほど、同時実 行性能は落ちる。性能、信頼性のバランスをとって決める。
PostgreSQL としては、Read Uncommitted は Read Committed と同義。SQL 標 準として、分離レベルをより厳しくする (ex: Read Uncommitted で Dirty Read できないようにする) ことは許可されている。
複数のコマンドを含むトランザクション
分離レベルによらず、コミットしていない内容であっても、同一のトランザク ション内からは参照できる。では、
INSERT INTO t1 SELECT * FROM t1
これはどう動くのか。当然、INSERT 開始時点での t1 全件が、t1 に挿入され るという風に動作する。これを実現するには Snapshot とは別の対処が必要。 PostgreSQL ではトランザクション内の文にコマンドID (CI) と呼ばれる連番 を振ることで対処している。
文は参照しているテーブルの行の XID が自身と同じ場合、CI もチェックする:
- 実行中の文の CI よりも前の番号は見える
- 実行中の文の CI と同じ番号は見えない
- 実行中の文の CI よりも後の番号は存在しない
これによって前掲の SELECT/INSERT は、自身が追記した行を参照することな く動作する。
Durability (永続性)
- チェックポイントでデータファイルを更新
- コミットで WAL (トランザクションログ) へ同期書き込み
- チェックポイント
- shared_buffer 上のデータをディスクに一括して反映
- WAL 同期書き込み
- バッファリングとかしないよ、という意味かな
- O_WRONLY|O_SYNC たぶんこんな感じ
コミットした情報は WAL に随時書き出していく。定期的に shared_buffer の 内容をデータファイルに書き出す (永続化)。書き出す前にプロセスが落ちて しまい、消えてしまった shared_buffer 上のデータであっても、コミットさ れていれば WAL に残っているので、ここから復元できる。具体的には最後の チェックポイント以降の WAL からリカバリすればいい。
参考
- トランザクション管理
- 本コンテンツは、2014年1月30~31日に筑波大学で開講された「情報システム特別講義D」における講義「Inside PostgreSQL Kernel」の内容を再構成、加筆・修正したものです。
- PostgreSQL のトランザクション & MVCC & スナップショットの仕組み