仕組みから学ぶ Web
発展読了目安 16 分#整合性#外部サービス#補償#突き合わせ#決済

2 つのシステムに書く — 片方だけ成功した状態を、先に設計しておく

決済は成功した。自社の DB には「未払い」と残った。利用者はお金を払ったのに使えなかった。自社の DB と外部のサービスは、1 つのトランザクションにならない。だから、途中で止まったときにどちらへ倒れるかを、先に決めておく。

この記事の進み方

What — 1 つのトランザクションにならない相手

自社の DB の中なら、2 つの書き込みを1 つのトランザクションにできます。両方成功するか、両方なかったことになるかのどちらかです(トランザクションと分離レベル)。

外部のサービスは、そうはいきません。決済、メール送信、在庫の外部システム、会計ソフト。相手の DB と自分の DB を、同じトランザクションで包む方法はありません。どちらかを先に書き、もう片方を後に書くしかない。その間で止まれば、片方だけが残ります。

Compareどの順番で書くか

先に課金し、成功したら自社の DB を「有効」にする。

① 課金する(外部)   ✓
 ── ここで止まる ──
② 契約を有効にする    ✗

残る状態:
払った。でも使えない
止まったときの状態
払ったのに使えない
自社が失うもの
信用
気づき方
問い合わせ
  • –冒頭の事故はこれ。自社の DB には課金した記録が無いので、自社の側からは気づけない。
  • –利用者がもう一度押すと、2 回目の課金が走る。

どの順番でも、途中で止まる可能性は残る。違うのは、止まったときにどちらへ倒れるか。

片方だけ成功した状態を、名前のある状態にする

3 つ目のやり方の要点は、途中の状態に名前を付けて、自社の DB に残すことです。

状態意味次に起きること
課金中外部に頼んだ。結果はまだ分からない結果が来たら確定。来なければ照会して確定
有効課金が成功したと確かめた—
失敗課金が失敗したと確かめた利用者にやり直しを案内

「外部が先」「自社が先」の失敗は、途中の状態がどこにも記録されないことでした。記録があれば、止まっても自社の側から見つけて、続きをやれます。

そのために、外部に頼むときは冪等キーを渡します(冪等性)。止まったあとに同じキーでもう一度頼んでも二重にならず、そのキーで結果を照会できます。

確認 — ここまで読めたか

自社の DB の更新と、外部の決済サービスへの課金を、1 つのトランザクションにまとめられない理由はどれですか?

まず選ぶ(解答例は a〜d の記号で説明します)

取り消しと、突き合わせ

途中で止まったものを確定させる方法は、2 つあります。

取り消し(補償): 片方が成功し、もう片方が失敗したと確定したら、成功したほうを取り消す操作を別に行います。課金したのに契約を作れないなら、返金する。ロールバックと違い、取り消しも 1 つの新しい操作なので、これ自体も失敗しうるし、記録が要ります。

突き合わせ: 定期的に、自社の記録と外部の記録を並べて、ずれを探します。「決済サービスでは成功、自社では課金中のまま 10 分」「自社では有効、決済サービスには記録が無い」。どんな設計でも漏れは起きうるので、最後の網として置きます。

Why — なぜ「順番を工夫する」では直らないのか

どの順番にも、止まる場所がある

冒頭のチームは、順番を逆にすれば直ると考えました。しかし、2 つの書き込みの間は必ず存在します。サーバーの入れ替え、タイムアウト、プロセスの強制終了、ネットワークの切断。どれも、どの行の間でも起きます。

順番で選べるのは、止まったときにどちらへ倒れるかだけです。

  • 払ったのに使えない(利用者が損をする)
  • 払わずに使える(自社が損をする)

どちらを許すかは事業の判断で、多くの場合は利用者に損をさせない側に倒します。ただし、どちらに倒しても、倒れたことに気づけなければ意味がありません。

外部の応答が来ないのは「失敗」ではない

冒頭のアプリは、課金の応答を受け取る前に止まりました。仮に止まらなくても、応答がタイムアウトしたら同じことが起きます。リトライとタイムアウトで見たとおり、タイムアウトは「失敗した」ではなく「分からない」です。

分からないのに「失敗しました」と表示すると、利用者はもう一度押し、2 回目の課金が走ります。分からない状態を「課金中」として持ち、確かめてから答えるのが正しい扱いです。

外部のほうが「正」であることが多い

お金が動く記録、送ったメールの記録。多くの場合、外部のサービスの記録のほうが事実です。自社の DB は、それを写したものにすぎません。

だから、ずれたときは外部に合わせて自社を直すのが基本です。外部に照会できる手段(冪等キー、取引 ID、Webhook の再送)を、設計の最初から確保しておきます。

確認 — ここまで読めたか

決済サービスへの課金の呼び出しがタイムアウトしました。自社の DB の契約は、どの状態にするのが正しいですか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

冒頭の購読の手続きを、どう作り直しますか?

与えられた条件
  • 決済サービスは冪等キーを受け付け、キーで取引を照会できる。結果は Webhook でも届く
  • 今は「課金する → 成功したら有効にする」の 2 行で、途中の状態を記録していない
  • デプロイは 1 日に数回あり、そのたびに処理中のリクエストが途切れうる
  • 37 人の未対応と、12 人の二重課金がすでに発生している
この軸で考える
  • · 途中で止まったとき、自社の DB に何が残るか
  • · 応答が分からないとき、利用者に何を見せるか
  • · どんな設計でも漏れたときに、どう見つけるか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「課金に失敗したら DB を戻す処理を書いたので、もう大丈夫」と言う同僚に、それでは足りない理由を説明してください

与えられた条件
  • 相手は、例外が起きたときに DB を元に戻す処理をきちんと書いている
  • 相手のテストでは、決済サービスがエラーを返す場合をすべて確かめている
  • 何を足せばよいかまで示したい

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。

この知識を使う実践ケース