プレイブック
決済システムにおける送信箱/受信箱のパターン (Outbox Inbox Pattern In Payment Systems)
データベースへの書き込みとイベントのパブリッシュが同じトランザクション内にない場合、そのうちの 1 つが失われるか、繰り返されます。送信トレイのブロードキャスト、コンシューマでの受信トレイの重複排除。
分散型決済エンジン
一部 7 の 22
取得と完了の間のギャップを埋める一連の分散型支払いアーキテクチャ。
1 つのトランザクションではなく 2 つの書き込み
サービスが「発注」を決定すると、通常、データベースに行を書き込み、メッセージ キュー/ブローカーにイベントを発行するという 2 つのことを実行します。これら 2 つの書き込みは異なるシステムに送信されるため、単一のトランザクションでアトミックに実行することはできません。それらの間のスペースは「デュアルライトホール」と呼ばれます。```text DbContext.SaveChanges() → veritabanına yazıldı Broker.Publish(event) → burası başarısız olursa event kaybolur (veya sıra tersine çevrilirse, event gönderilir ama DB commit rollback olur)
## 最初に説明した概念```text
📦 Dual-Write Problemi
Aynı iş kararının iki farklı sisteme (DB ve broker) atomik olmayan iki yazma ile yansıtılmaya çalışılması.
📦 Outbox
İş kararıyla aynı veritabanı transaction'ında yazılan, sonradan ayrı bir süreç tarafından yayınlanan olay tablosu.
📦 Lease
Bir publisher sürecinin, outbox'taki bir satırı işlerken diğer publisher süreçlerinin aynı satırı almasını önleyen geçici kilit.
📦 Inbox
Tüketici tarafında, gelen bir olayın işlenmeden önce kaydedildiği ve tekrarları filtreleyen tablo.
📦 Effectively-once
At-least-once teslimat ile idempotent tüketicinin birleşiminden elde edilen, pratikte “tam olarak bir kez” gibi davranan sonuç.
```Outbox は送信側の問題 (書き込み + ブロードキャストのアトミック性) を解決します。 Inboxは受信側の問題(再配達)を解決します。これらを組み合わせることで、信頼性の高いエンドツーエンドのイベント フローが確立されます。
## デュアルライトホールを開く方法
ハンドラーがデータベースへの書き込み直後にブローカーにイベントを発行すると、2 つの別々のステップの間にシステムがクラッシュする可能性があります。 DB の書き込みは成功したが、ブローカーの呼び出しが失敗した場合、ビジネス上の決定は維持されますが、イベントは公開されません。ダウンストリーム サービスにはこの決定が通知されません。```text
1. DB: Order.Status = Captured ✓ (commit edildi)
2. Broker.Publish(OrderCaptured) ✗ (network hatası, process crash)
Sonuç: Order tablosunda Captured var, ama hiçbir servise event gitmedi
```逆の場合も可能です。イベントは発行されますが、DB トランザクションはロールバックされます。この場合、ダウンストリーム サービスは、決して発生しなかったイベントに反応します。
## 送信ボックス: 書き込みとブロードキャストを同じトランザクションに移動します
送信ボックス パターンは、イベントをブローカーに直接ブロードキャストするのではなく、ビジネス上の意思決定と同じデータベース トランザクション内の送信ボックス テーブルにイベントを書き込みます。この行は、ビジネス上の決定によって、両方ともコミットするか、どちらもコミットしないかのいずれかによって、アトミックに永続化されるようになりました。```text
Tek transaction:
UPDATE orders SET status = 'Captured' WHERE id = 42;
INSERT INTO outbox (event_type, payload, dispatched_at) VALUES ('OrderCaptured', ..., NULL);
COMMIT
```別のパブリッシャー プロセスは、送信ボックス内の `dispatched_at IS NULL` 行を定期的にスキャンし、それらをブローカーにパブリッシュし、成功した場合は `dispatched_at` フィールドに値を設定します。このパブリッシャーが複数のインスタンスを操作する場合、リース (短期ロック) メカニズムが使用され、2 つのインスタンスが同時に同じ行を受信するのを防ぎます。```text
Publisher A: satır #7'yi lease'ler (30 sn) → broker'a yayınlar → dispatched_at = now()
Publisher B: satır #7 lease'li, atlar; başka satır arar
```この手順により、イベントが少なくとも 1 回ブロードキャストされるようになりますが、パブリッシャーがクラッシュした場合は 2 回ブロードキャストされる可能性があります。だからこそ、消費者側の防御が必要になるのです。
## 受信トレイ: 消費者側での重複排除
Outbox は少なくとも 1 回のブロードキャストを保証するため、消費者は同じイベントを複数回受信できます。イベントを直接処理する代わりに、受信箱パターンはまず、一意のイベント ID を使用してイベントを受信箱テーブルに書き込みます。この書き込みがすでに存在する場合 (一意制約違反)、イベントは再度処理されません。```text
Tüketici event alır: event_id=evt_001
INSERT INTO inbox (event_id, status) VALUES ('evt_001', 'received')
→ başarılı: iş bir job'a kuyruklanır
→ unique constraint hatası: zaten görülmüş, sessizce atlanır
```## 送信トレイ + 受信トレイ = 実質的に 1 回
送信トレイは、ブロードキャストが (少なくとも 1 回は) 失われないようにします。 Inbox は、コンシューマ側 (冪等コンシューマ) で同じブロードキャストが 2 回処理されないようにします。これら 2 つが組み合わされると、システムは実質的に「1 回だけ」動作します。これを「事実上 1 回」と呼びます。分散システムでは真の 1 回限りの保証はほぼ不可能ですが、この組み合わせは実際にはそれに適切に近似します。```text
Outbox (gönderen) → at-least-once yayın
Inbox (alan) → idempotent tüketim
─────────────────────────────────────────
Toplam davranış → effectively-once
このエピソードで最も混乱を招く対戦```text
❌ DB'ye yazıp hemen ardından broker'a publish etmek güvenlidir ✓ Bu iki adım atomik değildir; aralarında dual-write hole vardır
❌ Outbox tek başına tekrar teslimatı önler ✓ Outbox at-least-once garanti eder; tekrar teslimata karşı tüketici tarafında inbox gerekir
❌ Lease, kalıcı bir kilittir ✓ Lease geçici ve süreli bir kilittir; publisher crash olursa süre dolunca serbest kalır
❌ Effectively-once, exactly-once ile aynı şeydir ✓ Exactly-once dağıtık sistemlerde pratik değildir; effectively-once, at-least-once + idempotency'nin sonucudur
## 送信トレイ/受信トレイ設定のチェックリスト
1. ビジネス上の決定を DB に書き込み、イベントを公開することは同じトランザクションで行われますか、それとも 2 つの別々のステップで行われますか?
2. Outbox パブリッシャーが複数のインスタンスを使用している場合、2 つのインスタンスが同じ回線を受信しないようにするリース メカニズムはありますか?
3. Publisher がクラッシュした場合、リース期限が切れた後に回線を再試行できますか? それとも永久に「リース」されたままになりますか?
4. コンシューマ側では、受信トレイ テーブル内のイベント ID に一意の制約はありますか?
5. 送信トレイ内の `dispatched_at IS NULL` を含む行数を追跡するメトリクス/アラーム (公開遅延インジケーター) はありますか?
これら 5 つの質問のうち 2 つに自信を持って答えることができない場合、お使いのシステムは依然として二重書き込みホールに対して脆弱である可能性があります。
## このセクションで覚えておくべきこと
1. データベースへの書き込みとイベントの発行は別のシステムに移動し、アトミックではありません。このスペースは二重書き込みホールです。
2. 送信ボックスは、ビジネス上の決定と同じトランザクションにイベントを書き込むことで、送信者のアトミック性を保証します。別の発行者プロセスによって発行されます。
3. リースにより、複数のパブリッシャー インスタンスが同じ送信トレイ行を同時に処理することができなくなります。これは一時的なロックであり、永続的なロックではありません。
4. Inbox は、コンシューマ側で同じイベントが 2 回処理されることを防ぎます。 Outbox の少なくとも 1 回の保証を冪等にします。
> 送信トレイなしで公開されたイベントは、スローされるとすぐに消える場合があります。 Inbox なしで受信したイベントは 2 回処理される可能性があるため、損失が 2 倍になります。
FAQ
よくある質問
二重書き込み問題とは何ですか?
2 つの非アトミック書き込みを使用して、同じビジネス上の決定を 2 つの異なるシステム (DB とブローカー) に反映しようとしています。
送信ボックスとは何ですか?
イベント テーブルはビジネス上の意思決定と同じデータベース トランザクションに書き込まれ、その後別のプロセスによって公開されます。
「DB に書き込んですぐにブローカーに公開しても安全」は本当ですか?
これら 2 つのステップはアトミックではありません。それらの間には二重書き込みホールがあります
このセクションでは何を修正しますか?
このセクションでは、このギャップを埋める送信ボックスと受信ボックスのパターンについて説明します。データベースへの書き込みとイベントの発行は別のシステムに送信され、アトミックではありません。このスペースは二重書き込みホールです。サービスが「発注」を決定すると、通常、データベースに行を書き込み、メッセージ キュー/ブローカーにイベントを発行するという 2 つのことを実行します。これら 2 つの書き込みは異なるシステムに送信されるため、単一のトランザクションでアトミックに実行することはできません。それらの間のスペースは「デュアルライトホール」と呼ばれます。
学んだエンジニアリング原則
- データベースの書き込みとイベントの発行が同じトランザクション内にない場合、二重書き込みホールは開いたままになります。送信トレイは、送信者側のこのギャップを埋めます。
- Inbox は、Outbox の at-least-once 保証をコンシューマ側で冪等にする必須の補完機能です。
- 実質的に 1 回は、厳密に 1 回の代わりにはなりません。これは、少なくとも 1 回の配信とべき等な消費によって生成される実際的な結果です。
続きを読む
続きを読む
シリーズの次のシリーズ
支払い証明書と支払いステータス: 混同すべきではない理由
PSPがそう言っているのが証拠です。状態はあなたが決めるものです。これら 2 つを同じレジストリに保存しておくと、回復中にどちらを信頼すればよいかがわかります。
シリーズの次のシリーズ
決済システムにおける Webhook の信頼性
Webhook が繰り返されたり、消えたり、順序が乱れたり、遅れて到着したりします。署名を検証し、迅速な ACK を返し、同期的な重労働を実行しないでください。
同じシリーズ
API (アプリケーション プログラミング インターフェイス) リクエストを超えた冪等性 (反復可能で安全なトランザクション)
冪等性は単一のヘッダーではありません。これは、API キーからステップ ポインターまで、5 つの異なるレイヤーで個別に設定する必要がある防御スタックです。