TT Lab
はじめる
学ぶ 学習パス コース

冪等性 — 二度押しても決済は一度だけ

トランザクショナルアウトボックスで注文とイベントを守る:設計の考え方

TT Labで続きを見る

一言でいうと

注文と、発行するイベントを、同じDBトランザクションに残し、配信の重複はコンシューマーが処理します。

なぜ必要なのか

注文をDBに保存した直後にプロセスが死んでイベントを発行できないと、ほかのサービスは、 注文ができたことを永遠に知らないかもしれません。逆にイベントから先に送ると、注文の保存が 失敗しても、ほかのサービスが存在しない注文を処理することがあります。2つのシステムに書く作業を、 関数2行の順序で解決することはできません。同じDBの中で、注文とoutboxの行をアトミックに 保存したあと、別の発行器が未送信のイベントを読むパターンが、この隙間を小さくします。

どう動くのか

BEGIN → orders INSERT → 장애 지점 → outbox INSERT → COMMIT
                                       ↓
발행기: pending → publish → sent 표시
소비자: consumed 확인 + 효과 반영 → 같은 트랜잭션으로 COMMIT

注文の保存とイベントの保存の間に例外が起きたら、両方ともなくなっている必要があります。発行が失敗したら、 sentを記録してはいけません。ところが、発行は成功してsentを記録する前に死んだなら、次の実行が同じ イベントをまた送ります。したがって、これはexactly-onceの送信ではありません。少なくとも1回配信されうる 設計で、効果の重複を防ぐために、コンシューマーもイベントidの記録と合計への反映を、 1つのトランザクションにまとめます。

現場での姿

冪等キーを再利用しながら金額を変えたなら、既存の成功を返す代わりに、衝突を知らせる必要があります。 このラボでは、注文idがイベントidで、1つの注文につき作成イベントを1つだけ作ります。一般的な 修正・キャンセルのイベントまで含めるには、別のイベントidと、バージョン・順序のポリシーが必要です。 発行器は1つという前提で構成します。複数の発行器のclaim、lease、ポイズンメッセージの隔離、 outboxの整理は、このラボが実装しない運用上の課題です。

次のラボですること

DBファイルを開いて、行数とpayloadを確認し、途中の例外を注入します。最後に、 コンシューマーがすでに処理した直後に発行器が例外を受ける状況を作ったあと、再送します。 配信の記録は2回ですが、売上の合計は1回である必要があります。レポートに「重複排除」という 文言を書くだけでは、合格しません。別の接続で読んだ、実際の保存状態で判定します。

参考: SQLiteのトランザクション