アーキテクチャ 2026-05-13 ⏱ 約 15 分

分散トランザクションの代表的3パターン — 2PC・Saga・Outbox

マイクロサービスにおける分散トランザクション設計の代表的パターン、2PC・Saga・Outboxを比較し、それぞれの仕組み・長所短所・使い分けを整理する。

Read in: en
分散トランザクションの代表的3パターン — 2PC・Saga・Outbox

マイクロサービスを採用すると、ほぼ確実にぶつかる壁が「複数の DB や、DB とメッセージブローカーをまたぐ書き込みを、どう一貫性を保って扱うか」という問題である。この記事では、この問題に対する代表的な3つのアプローチ — 2PC・Saga・Outbox — を端的に整理する。

前提: なぜモノリスでは問題にならなかったのか

モノリスの場合、すべてのテーブルが同一 DB に存在するため、BEGIN; ... COMMIT; で囲むだけで原子性・一貫性・分離性・永続性(ACID)が DB エンジンによって保証される。アプリ側は「トランザクション境界を切るだけ」で済む。

ところが、サービスを分割して DB も分けた瞬間、この前提が崩れる。異種リソースをまたぐ COMMIT は、DB エンジンの責務の外になるからである。ここで初めて、分散トランザクションの設計が必要になる。

1. 2PC(Two-Phase Commit / 二相コミット)

「全部 COMMIT」か「全部 ROLLBACK」かを、コーディネーターが2フェーズで決めるプロトコルである。

仕組み

長所

短所

ACID の観点

ACID 2PC での担保
Atomicity(原子性) ◎ 全参加者で同時に COMMIT か ROLLBACK が決まる
Consistency(一貫性) ◎ COMMIT 完了時点で全リソースが揃っている(即時整合性)
Isolation(分離性) ◎ Prepare 中はロックで他トランザクションから隔離される
Durability(永続性) ◎ COMMIT 後は各参加者で永続化される

…と理想だけ見れば完璧だが、その代償として可用性を犠牲にしている。

適する場面

ほぼ無い。マイクロサービス間では避けるのが定石である。XA 等のトランザクションマネージャ配下で異種リソース(複数の DB、DB + MQ など)を確実に揃えたいレガシー寄りの場面で、限定的に検討する。

2. Saga パターン

長期トランザクションを、各ステップのローカルトランザクションと「補償トランザクション」の連鎖で構成するパターンである。

仕組み

実装方式

長所

短所

ACID の観点

ACID Saga での担保
Atomicity(原子性) ○ 「全ステップ成功」または「補償で業務的に無かったことになる」を最終的に保つ
Consistency(一貫性) ○ 各ステップは独立して COMMIT、全体は結果整合性
Isolation(分離性) これを諦めるのが本質。中間状態が他のサービスやユーザーから見える
Durability(永続性) ◎ 各ステップのローカル COMMIT で永続化される

ACID の "I" を諦め、"A" を補償で取り戻すのが Saga の本質である。

適する場面

複数サービスをまたぐ業務フロー全般。テナント登録・注文処理・決済フローなど。

3. Outbox パターン

「業務データの書き込み」と「外部システムへの通知」を、確実にズレなく揃えるためのパターンである。Saga のような大きなフロー設計ではなく、もっと小さな「書いた事実とイベント発行を一致させる装置」である。

解きたい問題: dual write

そもそも何が困るのかというと、素朴に書くとこうなる:

BEGIN;
INSERT INTO orders ...;   -- 業務データ
COMMIT;                    -- DB はここで確定
queue.publish(event);      -- 別システムへの操作

DB のトランザクションは「同じ DB の中の操作」しか束ねられない。queue.publish は DB の外側なので、

COMMIT と publish のどちらを先にやっても、逆向きのズレが残る。これが二重書き込み(dual write)問題である。

仕組み: 同じ DB のテーブルに「送信予約」を書く

Outbox は、publish を直接やる代わりに 同じ DB 内の outbox テーブルに INSERT することでこれを回避する。

BEGIN;
INSERT INTO orders ...;    -- 業務データ
INSERT INTO outbox ...;    -- 「あとで publish してね」というメモ
COMMIT;                     -- 同じ DB なので両方原子的に確定
  1. アプリは業務データと同じトランザクションで outbox テーブルにメッセージを INSERT する
  2. COMMIT により「業務データ」と「送信予約」が1回の COMMIT で原子的に確定する
  3. 別プロセス(リレー)が outbox を polling や CDC で読み、外部送信する
  4. 送信成功したら outbox のレコードに送信済みフラグを立てるか、行を削除する

ポイントは、Outbox の置き場所は「業務データと一緒に1回の COMMIT で確定できる場所」でなければならないということ。実務上はほぼ「業務 DB と同じ DB のテーブル」になる(Kafka transaction や CDC で同等の境界を作る亜種はあるが、本質は同じ)。

長所

短所

ACID の観点

ACID Outbox での担保
Atomicity(原子性) これが本丸。業務データと「送信予約」が1回の COMMIT で同時に確定 or 同時になかったことになる
Consistency(一貫性) ○ 「業務データを書いた」⇔「いつか必ずイベントが飛ぶ」の対応を最終的に保つ(= 結果整合性)
Isolation(分離性) △ DB 内の INSERT 自体は分離されるが、外部システムから見ると中間状態(DB は書かれたがイベント未配信)が見える
Durability(永続性) ◎ DB に書けば消えない。リレーが何度死んでも outbox レコードが残るので最終的に publish される

Outbox が新規に持ち込む保証は実質 Atomicity だけ。それを足場に Consistency と Durability が結果としてついてくる、と捉えるのが正確である。

Outbox はどの「ゆるさ」に位置するか

Outbox は強い一貫性のための仕組みではなく、結果整合性寄りの設計である。整理するとこうなる:

要求 採るべき手段
ロストしてもいい(ログ・メトリクス等) COMMIT 後にそのまま publish
ロストは困るが、重複・遅延・順序入れ替わりは許容できる Outbox
強い即時一貫性が欲しい 2PC(だが採用は慎重に)

Outbox は強い一貫性を狙う仕組みではなく、dual write 問題を回避することに目的を絞ったシンプルなパターンである、と捉えるのが正確である。

適する場面

業務データを書いた事実を、何らかの外部システム(Pub/Sub・Webhook・別サービスの API)にロストなく通知したい場面のすべて。Saga と組み合わせるのが定番である。

三者の関係

3つは競合する技術ではなく、抽象度の違うレイヤーである。

Saga(業務フロー全体)
  └ Outbox(イベント発行の信頼性)
     └ DB Transaction(ローカルの原子性)

選定の指針

要件 推奨パターン
サービスをまたぐ業務フロー全体の整合性 Saga
DB COMMIT とイベント発行の原子化 Outbox
強い一貫性が絶対要件・可用性は妥協できる 2PC(ただし採用は慎重に)
単一 DB 内で済む 普通のトランザクションで十分

実務的には、Saga と Outbox を中心に据え、2PC は明確な理由がない限り採用しない、というのが現代のマイクロサービス設計で広く採られている方針である。

まとめ

「分散システムでは ACID の Isolation を諦め、Atomicity を補償で取り戻す」 — これが Saga の本質であり、Outbox はその各ステップを確実に動かすための土台になる。2PC を選ぶ前に、まずこの組み合わせで設計できないかを検討するところから始めるのが良い。

Tags: マイクロサービス 分散トランザクション 2phase commit Sagaパターン Outboxパターン
Share: 𝕏 Post Facebook Hatena
✏️ View source / Discuss on GitHub
☕ サポート

このブログを応援していただける方は、以下からサポートをお願いします。いただいたサポートはブログ運営・技術研鑽に活用します。


関連記事