まとめ
| 観点 | 内容 |
|---|---|
| 課題 | 一部の予約が解放されずに残り、在庫が「満席のように見える」状態が発生。販売機会を失い、担当者の手動解放も発生していた。 |
| 対応 | Redis の TTL(有効期限)とバックグラウンドジョブを用いて、期限切れ予約を自動解放する仕組みを導入。 - 同一予約の重複処理を防ぐため、一意キーで制御。 - Redis はトリガー、実際の在庫更新は DB が担当する構成に。 |
| 可視化 | 滞留中の予約数と解放までの経過時間をメトリクス化し、モニタリングで挙動を常時把握。 |
| 成果 | 未解放予約はほぼゼロに改善。在庫が速く正確に更新されるようになり、販売機会損失を防止。 |
| 効果 | 予約APIのレスポンスが安定し、ロック待ちも減少。手動での解放作業がほぼ不要になった。 |
背景と課題
予約システムにおいて、「決済未完了のまま在庫を占有し続ける予約」、いわゆる「ゴースト予約」は課題となっていた。
ユーザーがカートに商品や座席を入れたまま離脱したり、決済途中でエラーが発生した場合でも、在庫は「仮押さえ」状態のまま残り、販売可能な枠が減少。
特にセールやイベント販売ピーク時にはこの影響が顕著で、
- 実際には空きがあるのに「満席」と表示される
- 顧客が再訪しても予約できず離脱する
- 運営側が手動で滞留予約を解放する必要があり、オペレーション負荷が高い
という状況が生じていた。
これにより、販売機会損失・在庫回転率の悪化・顧客満足度低下という三重の損失が発生していた。
一方で、過剰な自動解放を行うと、実際に決済中のユーザーの予約を誤って消してしまうリスクもあり、「解放タイミングの設計」と「安全な監視・検知」が課題だった。
調査・測定フェーズ
このフェーズでは、次の2点を重点的に調べた。
- ゴースト予約がどの程度発生し、在庫や販売機会にどんな影響を与えているか。
- 自動解放の基準となるTTL(予約保持時間)をどのくらいに設定するのが適切か。
まず、現行システムの予約データを分析したところ、決済が完了していない予約が長時間残り続け、在庫が実際より少なく見えてしまうケースが多いことが分かった。
特に目立ったのは次の2つのパターンだった。
- 決済途中で離脱し、そのまま戻ってこないケース
- 通信エラーなどで決済APIが失敗し、リトライされないまま残るケース
これらはアプリケーションログと決済ログを突き合わせて確認した。
また、滞留した予約を自動で検出する仕組みがなく、担当者が管理画面から手作業で削除していたため、運用負荷が大きかった。
あわせて、TTLの初期設定を検討するために、仮予約から決済完了までの所要時間や離脱後の復帰率を本番データで分析。
「多くのユーザーがどのくらいの時間で戻ってくるか」を可視化し、TTLの初期値を決めるための判断材料とした。
これにより、
- ゴースト予約の実態を把握し、どのくらい解放の仕組みが必要かを明確化。
- TTLを適切に設定するための基礎データを得る。
という2つの目的を達成した。
設計・導入フェーズ
目的:ゴースト予約を自動で検知・解放する仕組みを設計し、販売機会損失を最小化する。
- アーキテクチャ設計
- 予約API、DB、Redis、ジョブワーカー、監視基盤を分離構成
- TTL管理とイベントドリブンな自動解放を組み合わせた構成
| コンポーネント | 役割 | 補足 |
|---|---|---|
| 予約API(App層) | ユーザーからの予約操作を受け付ける。予約確保・決済開始・キャンセルなどの同期処理を担当。 | 即時応答重視。外部から直接呼ばれる。 |
| DB(Persistent層) | 予約ヘッダ/明細/在庫を正規化して保持。状態遷移を一貫性のあるトランザクションで保証。 | MySQLなど。整合性の中心。 |
| Redis(キャッシュ/TTL層) | 一時的な予約保持(TTL付きキー)や在庫カウンタ、ジョブキュー制御を担う。 | 「期限切れ検知」や「軽量ロック」に利用。 |
| ジョブワーカー(Async層) | TTL切れ・決済失敗・期限解放などの非同期タスクを処理。 | 定期ジョブ+遅延キューでスケジュール。 |
| 監視基盤(Observability層) | 滞留数・処理時間・誤解放率を可視化。 | アラート・分析・運用改善の軸。 |
パターン①:正常に予約・決済が完了するフロー
ユーザーが予約 → 決済完了時に在庫確定 → TTLキーを削除。
パターン②:決済未完了(ゴースト予約)→ 自動解放ジョブで回収
決済されない予約はTTL期限切れでイベント発火 → ジョブが在庫を戻す。
-
データ管理
- 予約ごとに有効期限(
hold_expires_at)を持ち、キーで重複処理を防止。 - 状態は
HOLD→PAYING→CONFIRMED/EXPIREDの流れで管理。
- 予約ごとに有効期限(
-
監視・アラート設計
- 滞留数、解放ジョブレイテンシ、ロック待ち時間、誤解放率を計測
検証
目的
自動解放ジョブが正しく動き、安定して在庫を戻せることを確認する。
誤解放を起こさず、遅延やロック競合が発生してもシステム全体が止まらないことを重視した。
検証内容
- 正しさ:予約、キャンセル、決済未完了など、さまざまな状態遷移で正しく在庫が更新されるか。
- 整合性:二重予約や売り越しが発生しないか。
- 安定性:ピーク時の処理遅延やロック競合が許容範囲内に収まるか。
- 可用性:Redisやジョブが一時的に停止しても、最終的にDB基準で正しく回復するか。
- 監視性:滞留数や遅延がダッシュボードで確認でき、異常時に通知されるか。
確認方法
- 正常系と異常系(離脱・決済遅延・キャンセル・障害注入)のシナリオを実行し、予約状態と在庫数を突き合わせて検証。
- 負荷テストツールを用いて、実際のトラフィック量に近い負荷を再現。高負荷時でも自動解放が遅れず、整合性が保たれるかを確認した。
- Redis TTL経由とDBスキャン経由の両経路で在庫が戻ることを確認。
- 冪等キーによる重複防止を実測で確認し、同一イベントの多重処理が起きないことを確認した。
結果
- 誤解放や売り越しは発生せず、在庫が自動で正しく戻ることを確認。
- ジョブ遅延やロック競合は発生してもシステム全体への影響はなく、監視ダッシュボード上でも正常な範囲で推移した。
結果・成果
定量的な結果
- ゴースト予約:自動解放により、長時間残る予約はほぼ解消。
- 在庫の整合性:画面表示と実際の在庫の差がほとんど発生しなくなった。
- レスポンス安定性:ピーク時でも予約APIの応答が安定し、処理遅延が発生しにくくなった。
- 運用工数:手動での在庫解放作業が不要になった。
定性的な効果
- 販売機会の回復:在庫の「見かけ満席」が解消し、販売ロスが減少。
- 可視化の強化:滞留数や解放のタイミングがダッシュボードで常時確認可能に。
参考
TTLの管理方法比較
今回の予約保持はRedisを活用しましたが、導入できない、もしくは永続性を優先する場合は以下の2方式が代表的です。
パターンA:DB主導のTTL管理(定期ジョブスキャン)
-
構成
- 予約テーブルに hold_expires_at を持たせる。
- 定期的にバッチ/ジョブワーカーが WHERE NOW() > hold_expires_at を検索して期限切れを解放。
-
メリット
- Redis不要、永続データのみで一貫性を担保できる。
- 再起動・障害復旧後も状態が消えない。
-
デメリット
- 遅延(バッチ間隔依存:最短でも数十秒単位)
- 大量データをスキャンするため、ピーク時はDB負荷が上昇。
- 「期限切れ直後に即座に販売再開」が難しい。
-
適用例
- チケットやホテルではなく、低頻度のBtoB予約など即時性より信頼性が優先のケース。
パターンB:ジョブキュー主導の遅延実行
-
構成
- 仮押さえ時に「解放ジョブ」をキューに登録。
- 例:enqueue(ReleaseJob, delay: 15.minutes, reservation_id: 1234)
- メッセージキュー(SQS, RabbitMQ, Sidekiqなど)が15分後にジョブを発火。
-
メリット
- Redisに依存せず、ジョブシステムの再実行保証・永続性を活用できる。
- 遅延実行は精度が高く、DBスキャンも不要。
-
デメリット
- メッセージキューの遅延精度は秒単位であり、Redisより若干遅い。
- キューが大量発生する場合は監視・再試行管理が煩雑。
-
適用例
- SQS + Lambda, Sidekiq + PostgreSQL, Celery + RabbitMQなどのジョブ駆動型アーキテクチャ。
比較
| 項目 | Redis TTL方式 | DBスキャン方式 | 遅延ジョブ方式 |
|---|---|---|---|
| 即時性 | ◎(ほぼリアルタイム) | △(バッチ間隔依存) | ○(秒〜十秒単位) |
| 永続性 | △(揮発性) | ◎ | ◎ |
| 構成のシンプルさ | ○ | ○ | △(ジョブシステム依存) |
| 負荷特性 | 低(メモリ) | 高(I/O集中) | 中 |
| 障害復旧 | 再生成必要 | 永続維持 | 再実行保証あり |
| 向いているケース | 高頻度・即時性重視 | 信頼性・履歴重視 | イベント駆動・分散環境 |
決済Webhookの障害対応
決済事業者からの Webhook は「遅延」「重複」「順序入れ替わり」「再送」が起こりうる。それぞれのパターンでどのように整合性をとっているかをシーケンス図でまとめています。
重複配信
TTL 期限切れ直後に決済成功が遅れて到着
グレース内で復権して確定
TTL 期限切れ後に成功が大きく遅延
復権しない方針 返金または再予約導線
逆順イベント
先に失敗 後から成功 最終状態は時刻優先で確定
ユーザーが支払い前にキャンセル
後から成功が来ても冪等で無害化