このページの目次

分散トランザクション

複数のサービスにまたがる1回の書き込み操作——グローバルなACIDは存在しない。2PCは強い整合性を追求するが、コーディネーターが障害発生すると参加者がすべてブロックされる。SagaやTCCは補償トランザクションで可用性と引き換えに、アプリケーション層でロールバックロジックを自前で実装する必要がある。outbox patternと冪等性により、実装上の「少なくとも1回」の配信が保証される。

単一ノードのデータベースではトランザクションはACID準拠であり、BEGIN/COMMIT/ROLLBACKで制御できる。しかし分散システムでは複数のサービスが関与するため、グローバルなACIDトランザクションは存在しない。

2PC (Two-Phase Commit)

最も基本的な分散トランザクションプロトコル——ブロッキング型であり、コーディネーターの障害に対する耐性を持たない。

2PC 2フェーズコミット: PREPARE投票 → COMMIT/ABORT決定 ① PREPAREフェーズ · コーディネーターが投票を開始 コーディネーター → 各参加者 PREPAREリクエストを送信 参加者 操作を実行し、WALに書き込むが、コミットはしない YES(コミット可能)またはNOで応答 すべての応答を収集し、すべてがYESかどうかを判断 ② 決定フェーズ · コーディネーターが結果をブロードキャスト すべてYES → COMMIT コーディネーターがCOMMITをブロードキャストし、参加者がコミット いずれかがNO → ABORT コーディネーターがABORTをブロードキャストし、参加者がロールバック 問題点: coordinatorがPREPARE後、COMMIT前にクラッシュした場合、participantは不確実な状態に陥る—— 単独でコミットすることも、単独でアボートすることもできず、コーディネーターの回復を待ってブロック状態になる。これが2PCのブロッキングプロトコルである。

問題点: coordinatorがPREPARE後、COMMIT前にクラッシュした場合 → participantは不確実な状態(uncertain state)に陥る——他のparticipantがYESかどうか分からないため単独でコミットできず、他がすでにコミットしている可能性もあるため単独でアボートもできない。⁠参加者がブロックされ、coordinatorの回復を待つ——これが「ブロッキングプロトコル」である。

実務では: XAトランザクション(2PCの実装)はデータベースのシャーディング間トランザクションで使われるが、coordinatorのクラッシュへの対応には管理者によるヒューリスティックなコミット/アボートが必要になる。

Saga

長時間のトランザクションを複数のローカルトランザクションステップに分割し、各ステップには独自の補償操作(compensating action)を持たせる。あるステップで失敗した場合、完了したステップを逆順で補償実行する。

Saga: book_trip()をローカルトランザクションステップに分割し、失敗時は逆順で補償を実行 Step 1 book_flight() Step 2 book_hotel() Step 3 book_car() Step 4 process_payment() 失敗 補償: cancel_flight() 補償: cancel_hotel() 補償: cancel_car() process_payment() 失敗 → 完了したステップを逆順で補償実行: process_payment() 失敗 ① cancel_hotel() ② cancel_flight() Sagaには分離性がない——他のトランザクションが中間状態を見ることができるが、ブロックしない。いつでもシステムは進行中(progressing)である。 2つのオーケストレーションモード: Choreography(分散型、イベント駆動) vs Orchestration(中央コーディネーター、ロジックが集中して管理しやすい)。

Sagaには分離性がない(他のトランザクションが中間状態を見ることができる)が、ブロックしない——いつでもシステムは進行中(progressing)である。

オーケストレーションモード

  • Choreography: 各サービスが前のステップのイベントを消費し、自身の完了/失敗イベントを公開する——中央のオーケストレーターは存在しない
  • Orchestration: 中央のSagaコーディネーターが各サービスに「ステップを実行」または「補償を実行」を指示する——ロジックが集中しており、管理が容易

TCC (Try-Confirm-Cancel)

Sagaのバリエーションで、より「厳格な」リソース予約に適している。

Try:     リソースを予約 (在庫確認、残高凍結) — リソースは**消費されていない**、単に「使用待ち」マーク
Confirm: リソースを実際に消費 (在庫減算、入金処理)
Cancel:  予約を解放 (在庫解放、残高解除)

Sagaとの違い: Sagaのステップはデータを直接変更する(book_flightは実際にチケットを予約する)、補償はundoである。TCCのTryは予約のみを行い、Confirmが実際に変更を行う——⁠いつでもCancelでき、後でundoを実行する必要がない⁠。

冪等性

分散システムにおいて最も重要な非機能的要件: f(x) = f(f(x))——再試行操作が結果に影響を与えない。

実装方法:
  1. unique idempotency key: クライアントがUUIDを生成し、サーバーが保存 ((key) → (処理済み/未処理))
     → 重複するkey → キャッシュされた結果を返す、再実行しない
  2. database constraints: INSERT INTO orders VALUES (order_id=123, ...)
     → 再試行 → UNIQUE制約違反 → 無視または既存レコードを読み取る
  3. token-based: サーバーがクライアントにトークンを発行し、クライアントがリクエストにトークンを付与
     → 重複するtoken → 拒否

冪等でない例:
  UPDATE users SET balance = balance + 100  -- 再試行 = 2回加算される!
  修正: UPDATE users SET balance = ? WHERE version = ? (楽観的ロック)

Outbox Pattern

トランザクションとしてデータベースに書き込み + メッセージキューにメッセージを送信——両者を原子性で保証するには?DBに書き込んでからメッセージを送信するのは不可(DB書き込みは成功したがメッセージ送信が失敗 → 不整合)。

[ローカルトランザクション]:
  INSERT INTO orders (...);
  INSERT INTO outbox (event_type, payload, created_at) VALUES ('order_created', '...', NOW());
  COMMIT;  ← 両方が同じローカルトランザクション内にある!

[Outbox Poller]:
  SELECT * FROM outbox ORDER BY id → Kafka/MQに送信 → 成功 → outboxから削除

保証: orderの書き込み + eventの生成は原子性(同じトランザクション内)。Outbox pollerがat-least-once配信を行う。

参考

  • 論文⁠: "Sagas" (Garcia-Molina & Salem, 1987)
  • マイクロサービストランザクション⁠: microservices.io/patterns/data/saga.html
  • 冪等性⁠: stripe.com/docs/idempotency (本番環境レベルの実装)

Keywords: 2PC, XA, Saga, TCC, compensating action, idempotency, outbox pattern, distributed transaction