VOLUME 03 / CHAPTER 03
予約はあるのに、通知する仕事がない
二つの保存先を更新する境界から、Outbox、補償、CQRSを理解する。いつ整合するかだけでなく、それまで業務に何を見せるかを考える。
出典照合日: · 独自解説
架空事例:青葉機材が、予約記録、通知、配送手配を別の処理で扱う。業務の成立条件、補償方法、データの管理主体は比較用の設定であり、パターン名から自動的に決まるものではない。
どちらを先にしても、隙間ができる
予約をDBへ保存した後、通知用のキューへメッセージを送る。通常は、これで営業へメールが届く。ところが、予約のコミット直後にプロセスが停止すると、キューへ送る処理まで進まない。予約は存在するが、通知する仕事はどこにも残っていない。
では、先にキューへ送ればよいだろうか。送信後に予約の保存が失敗すると、存在しない予約を営業へ通知する可能性がある。
順番を逆にするだけでは、二つの独立した保存先を同時に確定することにはならない。
「同じ関数に書く」「try文の中へ入れる」「失敗したら再試行する」といった実装の形だけでも、この隙間は消えない。どの操作が同じトランザクションで確定し、どこから別の仕組みの結果になるかを確認する必要がある。原教材の原子性を、関数や画面ではなく実際の確定境界へ当てはめる場面である。1
送る前に、送るべき事実を残す
Outboxの案では、予約記録と、その予約を通知するための記録を、同じDBのトランザクションで保存する。別の処理が、確定済みの送信待ち記録を取り出してキューへ送る。予約の保存が取り消されたなら、対応する送信待ち記録も残らない。2
これで一体にしたのは、予約の更新と「通知する仕事がある」という記録である。メールの配達まで同じ瞬間に完了させたわけではない。プロセスが停止しても、後で処理する根拠を残したのである。
青葉機材の例では、予約番号、イベント識別子、通知すべき内容またはそれを再現できる情報を保存する。現在の予約状態だけを後で読み直す設計なら、送信までの間に取消しや期間変更が行われたとき、何を通知すべきかを考える必要がある。「確定した」という当時の事実を送るのか、「現在は取消し」という最新状態を送るのかは、別の契約になる。
送信済みのOutboxをいつ消すかも、保持したい履歴と再処理の条件から決める。Outboxは、名前があるから永久保存の業務履歴や監査記録になるわけではない。保存目的を混同しない。
送信できたことを、送信側が知らない場合
キューはメッセージを受け付けた。しかし、その応答が失われた。Outboxの送信処理は、成功したか分からず、同じメッセージを再送する。
重複が起こり得る。
先ほどの仕組みは、予約だけが保存されて送信する根拠が失われる問題を解いた。配送のすべてを一回にする保証を得たわけではない。AWSのOutboxの解説も、重複メッセージを考慮し、受信側を冪等にする必要を示している。2
受信側で処理済みのイベント識別子を保存する案を考える。このとき、処理済み印だけを先に確定し、その後の業務更新が失敗すると、再送された仕事を「既に終わった」と捨ててしまう。逆に、業務更新だけを先に確定すると、印を残す前の停止で重複実行が起こる。
受信側の業務更新と重複処理の記録を同じトランザクションへ含められる範囲なら、その範囲を一体として扱うことを検討する。これはOutboxと同じく、何を同時に確定できるかという境界の問題である。さらに外部メールや配送会社のAPIがあるなら、そこにまた別の境界が現れる。
「exactly once」という語を見たときは、何を一回と数えているかを問う。キューへの追加、ある処理系での読取り、DBの更新、外部への副作用は同じ対象ではない。ある区間の保証を、その外側まで広げて解釈しない。
複数のDBを、一つの確定へまとめる案
予約と配送枠の確保を、複数のトランザクション資源にまたがって一体に確定したい場合、二相コミットのような調整方式が候補になる。準備できるかを各参加者へ確認し、その結果に基づいて全体の確定または取消しを決める考え方である。
しかし、どんな外部APIもこの調整に参加できるわけではない。単なるHTTPの予約受付APIへ、DBと同じ準備・確定の操作を要求しても、その契約がなければ成立しない。
PostgreSQL 18のPREPARE TRANSACTIONは、外部トランザクション管理機構が複数の資源を調整するための機能として説明されている。準備状態のまま長く残すと、ロックなどの資源を保持し続けるため、後で必ず確定または取消しへ進める管理が必要になる。3
全体を一つに確定する利点と、参加者や調整機構へ依存することの代償を比較する。強い原子性が必要な範囲を、最初から無制限に広げない。通知まで予約と同じ分散トランザクションへ入れる必要があるかは、業務上の成立条件から判断する。
既に確定した仕事を、業務として補う
別の案では、各サービスがローカルな処理を確定し、次の段階へ進む。機材を確保し、配送を手配する。配送枠を確保できなかった場合は、機材の確保を解放する。Sagaは、このように複数のローカルトランザクションと、必要な補償を組み合わせるパターンである。4
補償は、DBのロールバックと同じではない。機材の確保は既に一度成立しており、その履歴を保ちながら解除する。顧客へメールを送っていたなら、送信した事実は消せない。必要なら取消しの連絡という別の業務を行う。
本事例では、配送確保前の機材確保を「仮確保」として、顧客への最終確約と区別する案を置く。これは第1巻の確認中とは異なり、機材の枠を一時的に押さえる新しい状態である。別の状態を追加するなら、以前の用語へ黙って混ぜず、期限、解放、確定の条件を定め直す必要がある。
補償自体も失敗することがある。機材の解放APIが止まっていれば、解放したいという意図と、実際に解放済みであることは分かれる。どの段階で止まっているか、何を再実行できるか、いつ担当者が判断すべきかを記録する。Sagaという名前を採用しただけでは、この回復は完成しない。4
調整役を置くか、事象でつなぐか
Sagaの進行を一つの調整役が管理し、次の操作を指示する構成では、全体の進捗や失敗時の分岐を一か所で追いやすい。一方、その調整役の状態保存、回復、依存関係を管理する必要がある。
各サービスが事象を受けて次の仕事を始める構成では、中央の指示役を持たずに連携できる。しかし、段階が増えると、どの事象から何が始まり、全体がどこまで進んだかを把握する負担が増える。MicrosoftのSagaの解説は、これらをorchestrationとchoreographyとして対比している。4
どちらにも、処理中の状態が外から見える問題が残る。ローカルな確定を積み重ねる方式に、単一DBのSerializableと同じ全体隔離が自動的に付くわけではない。仮確保中の機材を別の予約で使ってよいかなど、同時に走る業務の関係を定める。
青葉機材の初回の範囲では、配送と機材確保の結び付きが複雑すぎるなら、営業が確認して最終確約する手順を残す案も比較できる。分散パターンを実装することより、守るべき業務を実現できることが目的である。
読みやすい形を、正しく更新する形から分ける
営業は、予約、顧客、機種、料金、点検状態を一画面で読みたい。一方、更新側では、それぞれの責任と制約を分けて管理したい。両者を同じデータの形だけで満たそうとすると、照会も更新も複雑になる場合がある。
CQRSは、更新のためのモデルと読取りのためのモデルを分ける考え方である。別々のDBを必ず使うわけではなく、同じ保存先の上で異なるモデルと処理を持つ構成もある。メッセージやEvent Sourcingも必須条件ではない。5
参照用の一覧を別に保存し、更新から非同期に反映するなら、両者の間に遅延が生じる。確定直後の予約を一覧で見付けられず、営業がもう一度登録するかもしれない。本事例では、確定結果をそのまま表示し、一覧が追い付くまで処理中の意味が分かるようにする案を考える。古い参照モデルを、更新の最終判断に無条件で使わない。
Event Sourcingは、状態を変えた事象の列を記録の正本とし、そこから状態を導く方式である。通常のDBを更新しながら通知を送るだけの構成とは異なる。過去の状態を再構成する利点がある一方、事象の意味や版の変更、再生、同時更新を扱う責任が生じる。6
データを分けるほど、何が正本で、どの参照がどの時点を表しているかが重要になる。速い一覧、確実な更新、履歴の再構成は関連するが、同じ一つの機能ではない。
整合するまでの時間も、設計の対象になる
「最終的に整合する」とだけ説明しても、いつ営業が顧客へ回答できるかは分からない。通知が十分遅れたのか、処理が停止して永久に進まないのかを区別する必要がある。
反映待ちの最も古い時刻、未完了件数、失敗した段階、同じ仕事の再実行回数など、採用した方式に合う観測を置く。目標時間を超えた場合、誰がどの記録を基に判断するかも決める。データが一致したことを確認する照合では、単に行数だけでなく、予約番号、状態、数量など、守る約束に対応する値を見る。
予約と通知の間にあった隙間を見つけると、単純な送信処理にも多くの問いが現れた。重要なのはパターンを重ねることではない。何を一体で確定し、何を後で進め、途中の状態を誰がどう扱うかを説明できることである。
知識を答案へ戻す
午前Ⅱ。 OutboxはDB更新と送信する根拠を一体に残す。Sagaの補償は、確定済みの事実を業務として補う。CQRSは読取りと更新のモデル分離であり、Event Sourcingは事象の記録を正本とする方式である。名前が近いからと同じ保証にしない。
午後Ⅰ。 どの保存が成功した後に、どの処理が失敗するかを時系列で追う。再送で失われるもの、重複するもの、相手に見える途中状態を、問題文にある操作から特定する。
午後Ⅱ。 非同期化の理由と、整合するまでの業務上の扱いを両方述べる。データを分けた事実だけでなく、正本、遅延、補償、照合の責任を示す。
立ち止まって考える:Outboxを使えば、通知は必ず一回だけ届くか
それだけでは保証できない。送信できたのに応答が失われ、同じ記録を再送する場合がある。Outboxが一体にするのはDB更新と送信待ちの記録である。配送や受信側の業務更新、外部副作用については、各境界の重複や失敗を別に扱う。
出典と照合範囲
[1] コンプリートテキスト第7・17・18章、IPA「SAシラバス Ver.5.2」4-9、8-4、10-2、10-3:原子性、再試行、参照モデル、連携の基礎に接続した。青葉機材の状態や補償は独自の設計例である。公式シラバス
[2] AWS Prescriptive Guidance, Transactional outbox pattern:DBと通知の二重書込み、同一トランザクション内のOutbox、重複への対応を参照した。特定キューの保証を外部副作用まで一般化していない。公式解説
[3] PostgreSQL 18, PREPARE TRANSACTION:外部管理機構による二相コミットへの参加と、準備状態で保持する資源を参照した。公式文書
[4] Microsoft, Saga distributed transactions pattern:ローカルな確定と補償、二つの進行方式、補償失敗と全体隔離の制約を参照した。公式解説
[5] Microsoft, CQRS pattern:読取り・更新モデルの分離、同一DBの構成、メッセージやEvent Sourcingとの関係を照合した。公式解説
[6] Microsoft, Event Sourcing pattern:事象の列を正本とすること、状態の再構成と導入上の複雑さを参照した。公式解説