6月17日,開発中の次期バージョンPostgreSQL 8.1に,2相コミットが取り込まれることが決定した。2相コミットは分散データベースを構築する上で欠かすことのできない機能であり,以前からPostgreSQLに2相コミットの実装を望む声は高かった。
2相コミットとは
2相コミット(two-phase commit)はデータベースに精通している人以外にはなじみが薄い言葉かも知れない。そこで簡単に2相コミットについて解説しておこう。ちなみに良く似た言葉に「2相ロック(two-phase lock」)というのがあるが,まったく別の概念なので混同しないこと。
分散データベースと2相コミット
2相コミットは,複数のデータベースに対する複数のトランザクションをまとめて一つのトランザクションとして扱う際に必須の機能である。この場合データベースは同じホスト内にあっても良いし,別々のホストに存在していても構わない。こうしたデータベース・システムは「分散データベース・システム」と呼ばれる。
PostgreSQLでは,コネクションを複数張ることによってアプリケーションが同時に複数のデータベースを扱うことができるので,一見2相コミットがなくても分散データベースシステムを構築することができそうである。しかし,実際には問題がある。そのことを示そう。
シナリオ: 分散データベースで障害発生
銀行の口座テーブルa,bがデータベースda, dbに作成されているものとする。今,aからbに1000円送金したいとしよう。DB内部では,
(1) DAのテーブルaの該当レコードから1000円マイナス
(2) DBのテーブルbの該当レコードに1000円プラス
という処理が行われる。ここで注意しなければならないのは,この2つの処理は不可分であり,ステップ(1),(2)が完全に終了するか,障害の際には(1)も(2)もまったく実行されなかったかのどちらかでなければならないということである。例えば,(1)のみ実行した後に障害が起きて(2)が実行されなかったとすると,1000円が空中に消えてしまったことになり,間違いなくユーザーから苦情が来るだろう。
このことを確認するために,上の処理をSQLでコーディングしてみよう。
da: CREATE TABLE a(id TEXT, amount INTEGER); -- 口座テーブルaをdaに作成
da: INSERT INTO a VALUES('foo', 10000); -- 初期デポジット10000円をfooさんの口座に振り込む
db: CREATE TABLE b(id TEXT, amount INTEGER); -- 口座テーブルbをdbに作成
db: INSERT INTO b VALUES('bar', 10000); -- 初期デポジット10000円をbarさんの口座に振り込む
ここでda: db:は,それぞれデータベースda, dbでのセッションを表すものとする。
fooさんの口座から1000円を引き落とす。
da: BEGIN; da: UPDATE a SET amount = amount - 1000 WHERE id = 'foo';
次にbarさんの口座に1000円入金する。
db: BEGIN; db: UPDATE b SET amount = amount + 1000 WHERE id = 'bar';
2つのトランザクションをコミットする。
da: COMMIT; db: COMMIT;
ここでdbのCOMMITが何らかの理由で失敗してしまったとする。この段階では既にfooさんの口座から1000円引き落とされているのにbarさんの口座には1000 円が振り込まれていないということになってしまった。つまり,分散データベースとしての一貫性を維持できなくなったのである。
もちろん,もう一度fooさんの口座に1000円を振り込んで1000円の引き落としを「なかったこと」にすればこの件に関してだけは問題が解決できるが,現実のシステムでは複雑にテーブルがからんでおり,すべてを「なかったこと」にするのは実際には容易なことではない。
2相コミットで分散データベースの一貫性を維持
こうした問題を解決するのが2相コミットである。2相コミットでは,COMMITの前に,コミットでもロールバックでもどちらの状態にも移行できる「セキュア」という状態を設ける。上の例で言えば,2つのUPDATEの後にいきなりコミットするのではなく,一旦セキュア状態にする。2つのDBに対してどちらのセキュアも成功したときのみコミットするのである。もしどちらかのセキュアが失敗したならロールバックすればよい。
それでは,開発中のPostgreSQL 8.1での2相コミットの実装を評価してみよう。