LASSIC Media らしくメディア
トランザクショナルOutboxとは|二重書き込み対策
「注文はデータベースに保存できたのに、在庫システムへの通知メッセージだけ送れていなかった」——マイクロサービス化や非同期処理を進めた開発会社から、こうした不整合の相談を受けることがあります。原因の多くは、データベースの更新とメッセージの送信という、本来ひとまとまりであるべき2つの処理が、別々に実行されていることにあります。
この問題への定番の打ち手が、トランザクショナルOutbox(Outboxパターン)です。名前だけ聞くと難しそうですが、考え方自体はシンプルです。本記事では、発注担当者やプロジェクトマネージャーに向けて、Outboxパターンが解決する課題・仕組み・Sagaや2相コミットとの違い・落とし穴を整理します。
目次
Outboxパターンとは——二重書き込み問題を解決する仕組み
アプリケーションが「業務データをデータベースに保存する」処理と「他のサービスへメッセージを送る」処理を、それぞれ別々に実行しているとします。前者が成功し後者が失敗すると、データベース上は更新済みなのにメッセージが届かない、という食い違いが生まれます。これがいわゆる二重書き込み問題です*1。データベースとメッセージブローカーはそれぞれ別の仕組みであるため、両方をまとめて1つの操作として扱うのは簡単ではありません。
Outboxパターンは、この問題を「送信すべきイベントの記録」を業務データと同じデータベース・同じトランザクションに含めることで解決します。業務データの更新と、送信予定のイベントをOutboxという専用テーブルへ挿入する処理を、1つのトランザクションでまとめて確定させます*1。データベースのトランザクションはもともと原子性(all or nothing)を持つため、この2つは常にセットで成功するか、セットで失敗します。あとは、確定済みのOutboxテーブルの内容を、別の仕組みでメッセージブローカーへ届けるだけです。
この記事のポイント
- Outboxパターンは、データ更新とイベント発行を同じトランザクションで確定させ、二重書き込み問題を解決する仕組みです。
- 確定後のOutboxテーブルは、ポーリングまたはCDC(変更データキャプチャ)でブローカーへ配信します*2。
- 配信は少なくとも1回(at-least-once)が基本のため、受け手側の処理を重複に強くしておく設計が欠かせません。
何を解決できるか——データ更新とイベント発行の整合
Outboxパターンが向くのは、次のような場面です。
| 場面 | 内容 |
|---|---|
| マイクロサービス間の連携 | あるサービスの更新を他サービスへイベントとして伝えたいが、通知漏れを避けたい。 |
| 在庫・決済など整合性が重い処理 | 注文確定と在庫引き当て通知など、片方だけ実行される状態を避けたい業務。 |
| 分散トランザクションを避けたい構成 | 複数システムをまたぐ厳密な同期処理を組まずに、整合性を確保したい。 |
| 監査・再送が必要なイベント | 送信履歴が残り、失敗時に再送しやすい構成にしたい。 |
共通しているのは、「データベースの更新」と「他システムへの通知」がセットで信頼できる状態を求めている点です。逆に、そもそも他システムへの通知が不要な処理には出番がありません。
仕組み——Outboxテーブルとリレー(ポーリング/CDC)
Outboxパターンの構成要素は大きく2つです。ひとつは業務データと同じデータベースに置く「Outboxテーブル」。送信したいイベントの内容(宛先・種別・本文など)を1行として記録します。もうひとつは、このテーブルを監視して未送信の行をメッセージブローカーへ届ける「リレー」です。全体の流れは次の図のとおりです。
リレーの実装方法には大きく2つの流派があります。ひとつはアプリケーションが定期的にOutboxテーブルをポーリングし、未送信の行を見つけて配信するやり方。実装は比較的シンプルですが、ポーリングの間隔が配信の遅延に直結します。もうひとつは、CDC(Change Data Capture)の仕組みでデータベースの更新ログを監視し、Outboxテーブルへの挿入を検知して即座に配信するやり方です*2。Debezium*2のようなツールがこの役割を担うことが多く、遅延を抑えやすい一方、CDCの基盤を用意・運用する手間が増えます。
Outboxテーブルの構造自体はシンプルで、宛先やイベント種別、送信する本文(JSONなど)に加えて、配信済みかどうかを示すフラグや送信時刻を持たせるのが一般的です。ここに一意なイベントIDを持たせておくと、受け手側で同じIDのイベントを二度処理しないよう判定できるため、後述する重複配信への備えとも直結します。リレーがどちらの方式であっても、Outboxテーブルへ行を書き込む処理そのものは業務データの更新と同じトランザクションに収めることが前提で、この点がずれると仕組み自体の意味が失われます。
Sagaや2相コミットとの違い
「データの整合性を保つ仕組み」としては、Sagaパターンや2相コミット(XAトランザクション)もよく名前が挙がります。Outboxパターンとは役割が異なるため、混同しないよう整理しておきます。
| 仕組み | 役割・特徴 |
|---|---|
| Outboxパターン | 1つのデータベース更新とイベント発行の対応を保ちやすくする、配信の信頼性を担う低レイヤーの仕組み。 |
| Sagaパターン | 複数サービスをまたぐ一連の業務処理を、補償処理を用意しながら順に進める、業務フロー全体の制御方式*3。 |
| 2相コミット(XA) | 複数のリソースを1つのトランザクションとして同期的にコミットする方式。ロックの保持時間が長く、可用性への影響やブローカー側の対応状況が課題になりやすい。 |
実務では、Sagaで業務フローの各ステップを制御し、そのステップ間の通知をOutboxパターンで取りこぼしなく配信する、という組み合わせ方がよく採られます*3。Outboxは2相コミットの代わりに使われることが多いものの、Sagaとは競合するものではなく、むしろ相性のよい組み合わせです。
よくある落とし穴——テーブル肥大化・重複配信・順序
Outboxパターンは仕組みとしてはシンプルですが、運用に乗せる段階でいくつか見落としがちな点があります。
| 落とし穴 | 内容 | 備えの方向性 |
|---|---|---|
| Outboxテーブルの肥大化 | 配信済みの行を消さないまま運用を続けると、テーブルが際限なく増えていく。 | 配信済みの行を定期的に削除またはアーカイブする仕組みをあらかじめ組み込む。 |
| 重複配信への無防備 | at-least-onceが前提のため、同じイベントが2回届くことがある。 | 受け手側でイベントIDを使った重複排除など、繰り返し処理されても結果が変わらない設計にする。 |
| 配信順序の見落とし | 対象や設定によっては、送信した順番と届く順番が一致しない場合がある。 | 順序が業務上重要なら、対象のIDを使った振り分けなど、順序を保つ設定を検討する。 |
| CDC基盤の運用負荷 | 低遅延を狙ってCDCを選ぶと、その分の監視・運用対象が増える。 | まずポーリングで始め、遅延が本当に課題になった段階でCDCへ切り替える選択肢も検討する。 |
導入・運用の勘所と外注時の確認点
Outboxパターンを検討する前に、まず「データベースの更新とメッセージ送信が、実際に食い違って困っているか」を確認するところから始めると判断がしやすくなります*1。すでにメッセージキューを使っている構成であれば、Outboxパターンは「送信の入り口」を1段固めるための追加の仕組みという位置づけになります。
外部へ委託する場合は、次の点をすり合わせておくとよいでしょう。リレーの実装方式をポーリングとCDCのどちらにするか、配信済み行の削除・アーカイブ方針をどう決めるか、受け手側の重複排除をどこで実装するか、そして順序保証がどこまで必要な業務かどうか——このあたりを最初に整理しておくと、導入後の食い違いを防ぎやすくなります。
また、既存システムに手を入れる形での導入になるケースが多いため、どの範囲の処理からOutboxパターンを適用するかも早めに決めておきたいところです。すべての通知を一度に置き換えるのではなく、まずは不整合が実際に業務上の支障になっている処理から段階的に適用し、リレーの運用が安定してから対象を広げていく進め方だと、切り替えに伴うリスクを抑えやすくなります。
まとめ:Outboxパターンで押さえる3つの判断軸
Outboxパターンは、データベースの更新とメッセージの発行の対応を保ちやすくする、地味ながら実用的な打ち手です。導入を考える際は、3つの軸で整理すると判断しやすくなります。第一に、そもそも二重書き込みの不整合が課題になっているかどうか。通知が不要な処理には出番がありません。第二に、リレーの実装方式。まずはポーリングで始め、遅延が課題になった段階でCDCを検討するという段階的な進め方も選べます。第三に、受け手側の重複排除とテーブルの肥大化対策。この2つを最初から設計に入れておくことが、運用を長続きさせる鍵になります。データ連携の不整合や設計に迷いがあれば、現状整理の段階から外部の知見を借りるのもひとつの方法です。
よくある質問
Outboxパターンを使えば、メッセージの重複はなくなりますか。
いいえ。Outboxパターンが保証するのは「送るべきイベントを取りこぼさないこと」で、配信方式はat-least-once(少なくとも1回)が基本です。同じイベントが2回届く可能性は残るため、受け手側で重複しても結果が変わらない処理にしておく必要があります。
2相コミット(XA)を使えば、Outboxパターンは不要になりますか。
2相コミットは複数のリソースを同期的にコミットする方式で、ロックの保持時間が長くなりやすく、対応していないメッセージブローカーも少なくありません。Outboxパターンは、こうした制約を避けつつ整合性を確保する目的でよく選ばれます。
Sagaパターンとはどう組み合わせますか。
Sagaは複数サービスをまたぐ業務フロー全体の制御方式で、Outboxパターンはそのステップ間の通知を取りこぼしなく配信するための仕組みです。役割が異なるため、Sagaで業務フローを制御し、その通知にOutboxパターンを使う組み合わせがよく採られます。
ポーリングとCDC、どちらを選べばよいですか。
まずはポーリングで始め、実装や運用の負担を抑えるのが現実的です。配信の遅延が業務上の課題になってきた段階で、CDCへの切り替えを検討するという段階的な進め方も選べます。
外注する場合、何を確認すればよいですか。
リレーの実装方式(ポーリングかCDCか)、配信済み行の削除・アーカイブ方針、受け手側の重複排除の実装場所、順序保証がどこまで必要か——この4点を最初に確認しておくと、認識のずれを防ぎやすくなります。
著者:テレリモ総研編集部 鈴木 亮佑
システム開発・データ連携基盤のご相談はLASSICへ
元請(プライムベンダー)として、データ不整合の原因調査から設計・実装・運用まで、貴社の課題に合わせてご提案します。まずはお気軽にご相談ください。
ご不明な点はお問い合わせフォームからもご連絡いただけます。
- *1 出典:Chris Richardson「Pattern: Transactional outbox」(microservices.io)(https://microservices.io/patterns/data/transactional-outbox.html)
- *2 出典:Debezium「Outbox Event Router」(Debezium Documentation)(https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html)
- *3 出典:AWS「Manage distributed transactions with the saga and outbox pattern」(AWS Prescriptive Guidance)(https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/manage-distributed-transactions-with-the-saga-and-outbox-pattern.html)