2 つのシステムに書く — 片方だけ成功した状態を、先に設計しておく
決済は成功した。自社の DB には「未払い」と残った。利用者はお金を払ったのに使えなかった。自社の DB と外部のサービスは、1 つのトランザクションにならない。だから、途中で止まったときにどちらへ倒れるかを、先に決めておく。
この記事の進み方
What — 1 つのトランザクションにならない相手
自社の DB の中なら、2 つの書き込みを1 つのトランザクションにできます。両方成功するか、両方なかったことになるかのどちらかです(トランザクションと分離レベル)。
外部のサービスは、そうはいきません。決済、メール送信、在庫の外部システム、会計ソフト。相手の DB と自分の DB を、同じトランザクションで包む方法はありません。どちらかを先に書き、もう片方を後に書くしかない。その間で止まれば、片方だけが残ります。
先に課金し、成功したら自社の DB を「有効」にする。
① 課金する(外部) ✓ ── ここで止まる ── ② 契約を有効にする ✗ 残る状態: 払った。でも使えない
- 止まったときの状態
- 払ったのに使えない
- 自社が失うもの
- 信用
- 気づき方
- 問い合わせ
- –冒頭の事故はこれ。自社の DB には課金した記録が無いので、自社の側からは気づけない。
- –利用者がもう一度押すと、2 回目の課金が走る。
どの順番でも、途中で止まる可能性は残る。違うのは、止まったときにどちらへ倒れるか。
片方だけ成功した状態を、名前のある状態にする
3 つ目のやり方の要点は、途中の状態に名前を付けて、自社の DB に残すことです。
| 状態 | 意味 | 次に起きること |
|---|---|---|
| 課金中 | 外部に頼んだ。結果はまだ分からない | 結果が来たら確定。来なければ照会して確定 |
| 有効 | 課金が成功したと確かめた | — |
| 失敗 | 課金が失敗したと確かめた | 利用者にやり直しを案内 |
「外部が先」「自社が先」の失敗は、途中の状態がどこにも記録されないことでした。記録があれば、止まっても自社の側から見つけて、続きをやれます。
そのために、外部に頼むときは冪等キーを渡します(冪等性)。止まったあとに同じキーでもう一度頼んでも二重にならず、そのキーで結果を照会できます。
取り消しと、突き合わせ
途中で止まったものを確定させる方法は、2 つあります。
取り消し(補償): 片方が成功し、もう片方が失敗したと確定したら、成功したほうを取り消す操作を別に行います。課金したのに契約を作れないなら、返金する。ロールバックと違い、取り消しも 1 つの新しい操作なので、これ自体も失敗しうるし、記録が要ります。
突き合わせ: 定期的に、自社の記録と外部の記録を並べて、ずれを探します。「決済サービスでは成功、自社では課金中のまま 10 分」「自社では有効、決済サービスには記録が無い」。どんな設計でも漏れは起きうるので、最後の網として置きます。
Why — なぜ「順番を工夫する」では直らないのか
どの順番にも、止まる場所がある
冒頭のチームは、順番を逆にすれば直ると考えました。しかし、2 つの書き込みの間は必ず存在します。サーバーの入れ替え、タイムアウト、プロセスの強制終了、ネットワークの切断。どれも、どの行の間でも起きます。
順番で選べるのは、止まったときにどちらへ倒れるかだけです。
- 払ったのに使えない(利用者が損をする)
- 払わずに使える(自社が損をする)
どちらを許すかは事業の判断で、多くの場合は利用者に損をさせない側に倒します。ただし、どちらに倒しても、倒れたことに気づけなければ意味がありません。
外部の応答が来ないのは「失敗」ではない
冒頭のアプリは、課金の応答を受け取る前に止まりました。仮に止まらなくても、応答がタイムアウトしたら同じことが起きます。リトライとタイムアウトで見たとおり、タイムアウトは「失敗した」ではなく「分からない」です。
分からないのに「失敗しました」と表示すると、利用者はもう一度押し、2 回目の課金が走ります。分からない状態を「課金中」として持ち、確かめてから答えるのが正しい扱いです。
外部のほうが「正」であることが多い
お金が動く記録、送ったメールの記録。多くの場合、外部のサービスの記録のほうが事実です。自社の DB は、それを写したものにすぎません。
だから、ずれたときは外部に合わせて自社を直すのが基本です。外部に照会できる手段(冪等キー、取引 ID、Webhook の再送)を、設計の最初から確保しておきます。
決済サービスへの課金の呼び出しがタイムアウトしました。自社の DB の契約は、どの状態にするのが正しいですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
冒頭の購読の手続きを、どう作り直しますか?
- 決済サービスは冪等キーを受け付け、キーで取引を照会できる。結果は Webhook でも届く
- 今は「課金する → 成功したら有効にする」の 2 行で、途中の状態を記録していない
- デプロイは 1 日に数回あり、そのたびに処理中のリクエストが途切れうる
- 37 人の未対応と、12 人の二重課金がすでに発生している
- · 途中で止まったとき、自社の DB に何が残るか
- · 応答が分からないとき、利用者に何を見せるか
- · どんな設計でも漏れたときに、どう見つけるか
「課金に失敗したら DB を戻す処理を書いたので、もう大丈夫」と言う同僚に、それでは足りない理由を説明してください
- 相手は、例外が起きたときに DB を元に戻す処理をきちんと書いている
- 相手のテストでは、決済サービスがエラーを返す場合をすべて確かめている
- 何を足せばよいかまで示したい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。