予約や在庫を扱うシステムで、いちばん怖い不具合があります。同じ枠に2件入ってしまうことです。
画面では1件しか受け付けていないように見えるのに、データベースを開くと2件入っている。返金と謝罪が発生して、原因を追っても再現しない。この種の不具合は、コードの書き方ではなく設計で決まっています。
私は有料通話サービスを個人で開発していて、予約と決済を実装しました。その過程で踏んだところを書きます。
「空きを確認してから入れる」は失敗する
いちばん自然に見える書き方が、これです。
1. その枠が空いているか確認する
2. 空いていたら予約を入れる
これで動いてしまうので、テストでも気づきません。問題は、1と2のあいだです。
利用者が2人同時に申し込むと、両方が「空いている」を見てから、両方が予約を入れます。あいだが0.1秒でも起きます。アクセスが増えるほど確率が上がるので、サービスが成長してから初めて出るのがたちの悪いところです。
直し方は2つある
ひとつは、データベースに「同じ枠は1件まで」を宣言してしまう方法です。
枠を一意キーにしておくと、2件目を入れようとした時点でデータベース側が拒否します。アプリ側で頑張らなくてよくなるのが利点で、実装を書き換えても効き続けます。
もうひとつは、予約を入れる処理を1件ずつ順番に通す方法です。
私のサービスでは、予約の確定処理をこの形にしています。同時に来ても、片方が終わるまでもう片方を待たせます。空き枠の計算が複雑で、一意キーだけでは表現しきれない場合はこちらになります。
どちらを選ぶかは、枠の決まり方によります。時間帯が固定なら前者で足ります。「前後30分は詰められない」のような条件が入ると後者が要ります。
決済がからむと、もう1段むずかしくなる
予約と決済の両方があるとき、次が起こりえます。
- 予約は入ったが決済が失敗した
- 決済は通ったが予約が入っていない
- 決済サービスからの通知が2回届いて、2回請求してしまった
3つ目は見落とされがちです。決済サービスは、通知が届いたか確信が持てないときもう一度送ります。受け取る側が同じ通知を2回処理しない作りになっていないと、二重計上します。
対策は、通知に付いている一意のIDを保存して、すでに処理したIDなら何もしないことです。私はこの形にしています。
残高をカラムで持たない
もうひとつ、後から効いてくる設計があります。残高を1つの数値として持たないことです。
「残高」というカラムを作って足し引きすると、計算が1回ずれただけで、いつどこでずれたのかを追えなくなります。
代わりに、入金・利用・返金の明細を1行ずつ積み上げて、残高は合計として出します。行数は増えますが、あとから1件ずつ検算できます。ずれたときに原因の行を特定できるかどうかは、運用に入ってから大きな差になります。
作る前に決めておくとよいこと
外注する場合も自分で作る場合も、着手前にこの3つを決めておくと後戻りが減ります。
1. 同じ枠に2件入らないことを、どこで保証するか(データベースか、アプリか)
2. 決済の通知が2回来たときに、何が起きるか
3. 金額がずれたとき、どうやって原因の行にたどり着くか
いずれも「動くかどうか」ではなく「間違ったときに気づけるかどうか」の話です。予約と決済のシステムでいちばん怖いのは、止まることではなく、間違ったまま動き続けることだと考えています。
DB設計のレビューをサービスとして出しています。すでに動いているシステムでも、上の3点がどうなっているかを見て、直し方を具体的にお伝えできます。設計段階のご相談も歓迎です。
▼ サービスページはこちら
WebアプリのDB設計をレビューします
※この記事は生成AIを併用して書き、内容は自分で確認しています。