このページの目次
分散トランザクション
複数のサービスにまたがる1回の書き込み操作——グローバルなACIDは存在しない。2PCは強い整合性を追求するが、コーディネーターが障害発生すると参加者がすべてブロックされる。SagaやTCCは補償トランザクションで可用性と引き換えに、アプリケーション層でロールバックロジックを自前で実装する必要がある。outbox patternと冪等性により、実装上の「少なくとも1回」の配信が保証される。
単一ノードのデータベースではトランザクションはACID準拠であり、BEGIN/COMMIT/ROLLBACKで制御できる。しかし分散システムでは複数のサービスが関与するため、グローバルなACIDトランザクションは存在しない。
2PC (Two-Phase Commit)
最も基本的な分散トランザクションプロトコル——ブロッキング型であり、コーディネーターの障害に対する耐性を持たない。
問題点: coordinatorがPREPARE後、COMMIT前にクラッシュした場合 → participantは不確実な状態(uncertain state)に陥る——他のparticipantがYESかどうか分からないため単独でコミットできず、他がすでにコミットしている可能性もあるため単独でアボートもできない。参加者がブロックされ、coordinatorの回復を待つ——これが「ブロッキングプロトコル」である。
実務では: XAトランザクション(2PCの実装)はデータベースのシャーディング間トランザクションで使われるが、coordinatorのクラッシュへの対応には管理者によるヒューリスティックなコミット/アボートが必要になる。
Saga
長時間のトランザクションを複数のローカルトランザクションステップに分割し、各ステップには独自の補償操作(compensating action)を持たせる。あるステップで失敗した場合、完了したステップを逆順で補償実行する。
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