Dev Study
← 分散システムの土台
レッスン 12 / 13·約 9 分·第12期 分散システムの土台

2相コミットは止まる — 準備済みのまま残る約束

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理
  7. DB保存今ここ

前回、分断時は一貫性か可用性を諦めるしかないと見ました。今回は「一貫性を諦めない」を貫くと何が起きるかを、DBの中で見ます。

まず結論

2相コミットは、参加者が「はい」と答えた時点で決着まで資源を握り続けます。調整役が消えると握りっぱなしになり、同じ行に触った人は待ち続けます。

実測では完全な再起動でも解けません。むしろ解けないように作ってあります。逃げ道は「待つのをやめる」ことですが、代償は原子性そのものです。

? 読む前に予測

2相コミットの1段目を終えた参加者が、調整役の返事を待ったまま取り残されました。DBサーバを再起動すると、どうなるでしょう。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

「はい」と答えた側は、撤回できない

前回、分断時は一貫性か可用性を諦めるしかないと見ました。今回は一貫性を諦めないを貫くと何が起きるかを見ます。複数の相手にまたがる更新を全員そろって成立させる古典的な手順が、2相コミットです。

  1. 第1相 — 調整役が全員に「準備できるか」を問い、参加者は作業を終えて「はい」と答える
  2. 第2相 — 全員が「はい」なら、調整役が「決着せよ」と指示する

全員の合意を求めます。本コースで見た過半数は少数派を止めて割れを避けましたが、こちらは全員を要求するので1人でも欠けたら進めません。

重さは第1相です。「はい」と答えた参加者は決着の指示まで撤回できません。 約束を守るための資源——ロック——を握り続けるしかありません。

具体実物

「はい」と答えたまま取り残す

PostgreSQL の第1相にあたるのが PREPARE TRANSACTION です。公式 Description の逐語。

After this command, the transaction is no longer associated with the current session; instead, its state is fully stored on disk

Those commands can be issued from any session, not only the one that executed the original transaction.

セッションから切り離され、ディスクに完全に保存され、決着は別セッションからも打てる。 裏を返せば誰かが決着させに来るまで残り続けるということです。

PostgreSQL 18.6(Debian、aarch64、Docker)で PREPARE TRANSACTION の直後に調整役の psql を終了させました。2026-09-18、実行は1回です。後の再起動の記録(xid 771)は別の準備済みトランザクションで取りました。

調整役が消えたあとの pg_locks(実測)コマンド出力
locktype | rel | mode | granted | pid | transactionid
---------------+---------------+------------------+---------+-----+---------------
relation | accounts | AccessShareLock | t | |
relation | accounts | RowExclusiveLock | t | |
relation | accounts_pkey | AccessShareLock | t | |
relation | accounts_pkey | RowExclusiveLock | t | |
transactionid | | ExclusiveLock | t | | 756
(5 rows)

pid 列が空です。 通常このビューには持ち主のプロセスの pid が入ります。持ち主がいないのにロックだけが生きており、granted は t——順番待ちではなく、握ったままです。 この状態で別のクライアントが同じ行を更新すると、こうなります。

同じ行への UPDATE(timeout 15 で包んだ・実測)コマンド出力
===== [5] ANOTHER client now tries to touch the same row. IT BLOCKS. =====
--- exit code: 124 (124 = timed out = still blocked) ---

**エラーが返ったのではありません。返ってこないのです。**誰が待たせているかも見えます。wait_event_type = Lock / wait_event = transactionid で待つ相手は 756 番。その 756 番を granted=t で握る行の pid が NULL です。

✓ ここまでの確認

取り残された準備済みトランザクションのロックを pg_locks で見ると、pid 列が空でした。なぜですか。

抽象原理

待ちを解くのは時間ではない

公式はこの危険を Caution に3つ挙げています。

It is unwise to leave transactions in the prepared state for a long time. This will interfere with the ability of VACUUM to reclaim storage, and in extreme cases could cause the database to shut down to prevent transaction ID wraparound … Keep in mind also that the transaction continues to hold whatever locks it held.

VACUUM が回収できないこと、ID周回でDBが停止しうること、持っていたロックを持ち続けること。DB編『掃除は4つの仕事をしている — そしてディスクは空かない』と『32ビットが尽きる日 — 止まるのではなく、書けなくなる』が同時に効きます。1つ目は実測できました。

VACUUM VERBOSE accounts;(実測・抜粋)コマンド出力
INFO: finished vacuuming "lab.public.accounts": index scans: 0
tuples: 0 removed, 3 remain, 1 are dead but not yet removable
removable cutoff: 756, which was 15 XIDs old when operation ended

回収の下限が準備済みの 756 番で止まっています。ID周回の停止は本教材では再現していません。公式が「そうなりうる」と書くところまでです。公式はさらに踏み込みます。

If you have not set up an external transaction manager … it is best to keep the prepared-transaction feature disabled by setting max_prepared_transactions to zero.

既定値は 0、最初から無効です。手元で試すには明示的な有効化が要ります。公式が「使うな」に近いことを書く機能です。

取り残された準備済みトランザクションと、待つ側(実測の pg_locks から)
pid=NULL準備済みが xid 756 を握る× granted = t
pid 158同じ行を UPDATE× 756 を granted = f で待つ
再起動別の準備済み(xid 771)で検証× 復元され、待ちは継続
決着ROLLBACK PREPARED を打つ○ 解放、待っていた更新が通る
↓
待ちを解いたのは時間でも再起動でもなく、決着のコマンドだけだった
  • ・pid が空の行が「持ち主のいないロック」
  • ・granted は t が握っている、f が待っている
具体実物

待つのをやめる道と代償

標準も同じです。X/Open XA 仕様(1991)の xa_commit() 戻り値 [XA_RETRY]。

All resources held on behalf of *xid remain in a prepared state until commitment is possible.

**資源は準備済みのまま保持され続ける。**待ち続けるのが嫌なら §2.3.3 に逃げ道があります。ヒューリスティック決着です。

an RM that has prepared to commit a transaction branch may decide to commit or roll back its work independently of the TM. It could then unlock shared resources. This may leave them in an inconsistent state.

参加者が調整役を待たずに決めてしまう。共有資源は解放されます。しかし "This may leave them in an inconsistent state"。決着の報告には completed it with mixed results (committed some work and rolled back other work) という種類まであります。「止まらない」ようにする道は、原子性との引き換えです。

✓ ここまでの確認

X/Open XA の「ヒューリスティック決着」は、調整役を待たず参加者が自分で決めてしまう仕組みです。仕様はその代償をどう書いていますか。

具体実物

最初から原子性を諦める — Saga

手放すのが避けられないなら、最初から手放す設計にする道があります。1987年の Saga 論文です。動機は Abstract の第1文。

Long lived transactions (LLTs) hold on to database resources for relatively long periods of time, significantly delaying the termination of shorter and more common transactions.

長く資源を握るせいで短い処理が待たされる。 前半と同じ問題意識で、論文は逃げ方も先回りして否定します。

In general there is no solution that eliminates the problems of LLTs. … No matter how the mechanism operates, a transaction that needs to access the objects that were accessed by a LLT cannot commit until the LLT commits.

「ロックが悪いのだから別の仕組みにすれば解決する」という読みを、原論文自身が否定しています。

Saga は長いトランザクションを、途中でコミットする小さな列に分け、失敗したら補償トランザクションで打ち消します。捨てるものは原子性の語で書かれています。

(b) At the outer level full atomicity is not provided. That is, sagas may view the partial results of other sagas.

補償を走らせるときも、

no effort is made to notify or abort transactions that might have seen the results of Tj

途中結果を見た側に通知も中断もしません。 今の言葉なら分離性を手放したことになりますが、それは Brewer 2012 の語です。

The correctness of this approach depends not on serializability or isolation, but rather on the net effect of the transaction sequence on state and outputs.

もう1つ誤解されやすいのが補償の意味です。

The compensating transaction undoes, from a semantic point of view, any of the actions performed by Ti, but does not necessarily return the database to the state that existed when the execution of Ti began.

座席予約なら取り消せますが、「予約前の空席数を書き戻す」ことはできません。間に他の予約が入っているからです。補償は意味的な取り消しであって、巻き戻しではない。 本コースで見たとおり、補償を安全に再送できるのは操作が冪等である場合に限られます。

⇄ 2つを見比べる
具体例 A

2相コミット(実測)

全員が合意するまで決着しません。合意できないまま調整役が消えると資源を握ったまま残ります。実測では再起動しても残り、決着のコマンドまで解けませんでした。

取り残されたとき何が起きたか表
観測項目 結果
準備済みの残存 再起動後も残る(recovering)
ロック pid が空のまま granted = t
同じ行の UPDATE 終了コード124(応答せず)
別の行の UPDATE 終了コード0(通る)
解消した手段 ROLLBACK PREPARED のみ
具体例 B

Saga(原論文の記述のみ。実機では試していない)

小さなトランザクションが次々とコミットします。失敗したら補償で打ち消しますが、その間の中途半端な状態は他から見え、見た側への通知も中断も行われません。

原論文が明言していること表
論点 原論文の記述
外側の原子性 full atomicity is not provided
途中結果 sagas may view the partial results
補償時の通知 no effort is made to notify or abort
補償の意味 does not necessarily return the database
to the state that existed

片方は待ち、もう片方は待ちません。それでも「どちらかが上位互換」と言えないのはなぜでしょう。

それぞれが「何を守るために何を差し出したか」を並べてみてください。

この章のまとめ

  • 第1相で「はい」と答えた参加者は撤回できず、決着までロックを握り続ける。公式 Caution が the transaction continues to hold whatever locks it held と明記。
  • 実測では持ち主のいない(pid が空)ロックが granted=t で残り、同じ行への UPDATE はエラーではなく無応答(終了コード124)になった。
  • 完全な再起動でも解けない。 ログには recovering prepared transaction。解いたのは ROLLBACK PREPARED だけ。
  • 待ちをやめる道(XA のヒューリスティック決着)の代償は This may leave them in an inconsistent state。原子性との引き換え。
  • Saga は最初から原子性を手放す設計。full atomicity is not provided は宣言であって欠陥ではない。交換であって上位互換ではない。
  • 補償は意味的な取り消しであって、実行前の状態に戻すことではない。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

「準備済みトランザクションが残るとDB全体が止まる」の評価はどれですか。

用語集(4語)
2相コミット
全員に「準備できるか」を問い(第1相)、そのうえで決着を指示する(第2相)方式。「はい」と答えた参加者は撤回できない。
準備済みトランザクション
PREPARE TRANSACTION を終えた状態。セッションから切り離されディスクに保存され、決着のコマンドが来るまで残る。pg_prepared_xacts ビューで一覧できる。
ヒューリスティック決着
X/Open XA §2.3.3 が規定する、参加者が調整役を待たずに独自に決着させる動作。待ちは解けるが、不整合や mixed results を残しうる。
Saga
長時間トランザクションを、途中でコミットする小さなトランザクションの列に分け、失敗時は補償で打ち消す方式。原論文は外側で完全な原子性を提供しないと明言。