分散トランザクション — 2相コミットの約束と限界、そして Saga

「銀行AのDBと銀行BのDB、両方に書けたときだけ送金成立にしたい」。1台のDBなら当たり前の原子性が、マシンをまたいだ瞬間に難問になります。古典解 2相コミット(2PC)のしくみと致命的な弱点、そして現実解 Saga の考え方を、故障を起こしながらアニメで比べます。

1. 2相コミット — 「準備」と「実行」を分ける

参加者が複数いるとき、いきなり「コミットせよ」と命じるのは危険です。片方だけ成功して片方が失敗したら、送金額が消えたり増えたりします。2PC はコーディネータ(調停役)を立て、まずフェーズ1(prepare)で全員に「コミットできる状態を作って約束せよ」と聞き、全員が YES と答えたときだけフェーズ2(commit)で実行させます。1人でも NO なら全員 abort。

ステップ実行で、メッセージと各DBの状態(ロック・undo/redoログ・残高)の変化を追ってみてください。

2PC の正常系 — 銀行A→Bへ10万円送金をステップ実行
オレンジ=prepare、水色=YES投票、緑=commit、白=ACK。参加者は YES と答えた瞬間から行をロックし続け、コーディネータの決定が届くまで解放できません。コーディネータが「決定=COMMIT」を自分のログに書いた瞬間(ステップ4)が不可逆点です。
POINT — 2PC の本質は「引き返す権利の放棄」 参加者が YES と投票することは「以後、自分の判断で abort する権利を捨てる」という約束。だから YES の前に undo/redo ログをディスクに書き、自分が故障しても再起動後にどちらへも倒せる状態を作っておく。全体の運命を握るのはコーディネータのログに書かれた決定ただ1つ。
2PC のコスト: メッセージ 3N 本 + 強制ディスク書込 2N + 1 回 N = 参加者数(ACK まで数えると 4N 本)。しかも prepare 〜 commit の間、関係する行はロックされ続ける

2. 2PC の弱点 — ブロッキング

問題はコーディネータが最悪のタイミングで落ちたときです。参加者が YES と投票した後にコーディネータが沈黙すると、参加者は commit も abort も自分では選べません(他の誰かが既に commit を受け取っているかもしれないから)。ロックを握ったまま、コーディネータの復旧をひたすら待つしかない — これをブロッキングと呼びます。故障のタイミングをボタンで切り替えて、危険な瞬間がどこかを確かめてください。

故障タイミング実験 — どこで落ちると詰むか
①なら参加者はタイムアウトで安全に自主 abort できます。危険なのは②:ロック保持時間がどこまでも伸び、同じ行を触る他のトランザクションが後ろに積み上がっていきます。③では A だけ確定済みという非対称な状態になり、B は A に問い合わせれば助かる(協調的終了)ものの、A も落ちていれば結局待つだけです。
注意 — 「タイムアウトで諦めれば?」は通用しない YES 投票後の参加者が勝手に abort すると、他の参加者は commit を受け取っているかもしれず、原子性そのものが壊れる。3相コミット(3PC)はこの穴を塞ごうとした改良だが、ネットワーク遅延に上限がない現実の非同期ネットワーク+分断の下では安全にできない。ブロッキングは 2PC の実装バグではなく、原理的な代償。

3. Saga — ロックせず、失敗したら「補償」で打ち消す

マイクロサービスの世界では、注文フローが「在庫サービス → 決済サービス → 配送サービス」と複数のDBをまたぎます。ここで 2PC を使うとサービス同士がロックで縛り合うため、代わりに Saga がよく使われます。全体をローカルトランザクションの連鎖に分割し、各ステップは終わった端から即コミット。途中で失敗したら、成功済みのステップを補償トランザクション(返金・予約取消など)で逆順に打ち消していきます。

Saga の実行 — 失敗地点を変えて補償の走り方を見る
緑=ローカル commit 済み、赤=失敗、オレンジ=補償(逆順)。ロックを持ち越さないので他の処理を待たせません。ただし T1 が commit してから補償が終わるまでの間、「在庫は減ったのに注文は存在しない」という中間状態が外から見えます — Saga は原子性を「結果的に」に緩めた設計です。
T1 → T2 → … → Tk が失敗 ⇒ Ck−1 → … → C2 → C1 Ci は Ti の補償トランザクション(意味的な打ち消し)。逆順に実行して「なかったこと」に近づける
注意 — 補償は「巻き戻し」ではない 「Saga を使えば 2PC と同じ原子性が手に入る」は誤解。補償は undo ではなく新しい打ち消し操作で、返金しても請求メールは送信済みかもしれない。分離性もないので中間状態は他者から見える。だから補償できない操作(通知送信・出荷など)はできるだけ連鎖の最後に置くのが設計の定石。

4. 使い分け — 強い原子性か、ロックしない自由か

2PC と Saga は競合ではなく、守るものが違う道具です。境界をまたぐ時間が短く、参加者が同じ管理下にあり、強い整合性が絶対条件なら 2PC。サービスが独立していて処理が長く、可用性を優先するなら Saga。バーを切り替えて得意分野を見比べてください。

2PC vs Saga — 性質の比較と使い分け指針
青=2PC、緑=Saga。2PC は「自動で全か無か」を保証する代わりにロックとブロッキングを抱え、Saga はロックなしで走る代わりに補償ロジックの設計・実装を人間が背負います。どちらを選んでも「タダの原子性」は存在しません。
一歩先へ — 現代のシステムはどう解いているか 最大の急所が「コーディネータが単一障害点」なら、そこを合意アルゴリズムで冗長化すればよい — Google Spanner は Paxos グループの上で 2PC を走らせ、コーディネータ故障をグループ内フェイルオーバーで吸収する。アプリ層では、Saga の実務形としてTCC(Try-Confirm-Cancel)や、DB書込とメッセージ送信を原子的に見せるOutbox パターンが定番。「どこまでを1つのDBに収め、どこから補償で運ぶか」が現代の設計論点。

5. まとめ