PostgreSQL Transaction

PostgreSQL のトランザクションの実装について調査したときのメモ書き。

ACID

Consistency がよくわからん。制約の話をしている?

Durability は技術的な限界がある。変更されたデータがどのようなデバイス に存在するのか。例えばローカルディスクなのか、ネットワーク越しの遠い場 所にあるのか、いずれにせよ、伝送のタイミングで消失してしまう可能性があ る。「何があっても残る」というのは、現実的には完全に実現することは難し い。

レコードの可視性

この仕組みにより Isolation を実現。複数のトランザクションにデータをう まく見せる。

Atomicity (原子性)

コミット済みのデータだけを処理対象とする。コミット済みかどうかは、タプ ルのヘッダを見れば分かる。

この辺を使う。

前者は前述の通り。タプルの生き死にが分かる。

後者はトランザクションの実行状態が分かる。タプルからは自身を作成したト ランザクションへのポインタがあるので、そのトランザクションがコミット済 みかどうか判断できれば、タプルがコミット済みかどうかも判断できる。

ちなみにアボートされたトランザクションの情報も残るらしい。当然、そのト ランザクションから作成されたタプルは処理対象外となる。

Consistency (一貫性)

制約の話。

制約のチェックで引っかかった場合は、その時点でトランザクションをアボー トする。

Isolation (分離性)

かの有名な MVCC (MultiVersion Concurrency Control) の話。

MVCC 「で」実現しているではなく、MVCC 「を」実現しているということらしい。 その実現方法は Snapshot によるトランザクションの世代管理ですよ、と。

Snapshot のデータ構造 (SnapshotData 構造体) において重要な項目は以下:

  1. xmin
    • トランザクションが始まる時点で、ある XID 以下のトランザクションは 終了している (Committed Or Aborted) ことが保証されている。その XID を持つ項目
  2. xmax
    • トランザクションが始まる時点で、ある XID 以上のトランザクションが 始まっていないか進行中である。これらは無視してよい。端的には、自 身の XID 以上のトランザクションは無視してよい。
  3. xip[]
    • xmin と xmax の間の進行中トランザクションの XID を持つ配列。なぜ clog を見る、ではだめなのか?

あるタプルが見えるかどうかの判定:

  1. 無効なタプル (削除 XID が入っている) なら見えない
  2. タプルの XID が自身と同じなら見える
  3. タプルの XID が xmax 以上なら見えない
  4. タプルの XID が xmin 以下で、CLOG を参照してトランザクションが Committed なら見える
  5. タプルの XID が xmin < XID < xmax で、xip 内に存在するなら見えない
  6. CLOG を参照してトランザクションが Committed なら見える

Snapshot をとるタイミングは分離レベルによって変わる。

以下の一連の文を実行する時を考える:

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 をとるのは、性能上の理由とのこと。

トランザクションの分離レベル

上に行くほど緩く、下に行くほど厳しくなる。厳しくなればなるほど、同時実 行性能は落ちる。性能、信頼性のバランスをとって決める。

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 もチェックする:

  1. 実行中の文の CI よりも前の番号は見える
  2. 実行中の文の CI と同じ番号は見えない
  3. 実行中の文の CI よりも後の番号は存在しない

これによって前掲の SELECT/INSERT は、自身が追記した行を参照することな く動作する。

Durability (永続性)

コミットした情報は WAL に随時書き出していく。定期的に shared_buffer の 内容をデータファイルに書き出す (永続化)。書き出す前にプロセスが落ちて しまい、消えてしまった shared_buffer 上のデータであっても、コミットさ れていれば WAL に残っているので、ここから復元できる。具体的には最後の チェックポイント以降の WAL からリカバリすればいい。

参考