プロンプトと適用されるコンテキスト
コンサート、スポーツイベント、または高需要の展示会向けのチケット予約システムを設計します。1つのイベントには50,000の指定席があります。発売開始から最初の5分間に200万人のユーザーがアクセスし、エッジトラフィックは毎秒50,000リクエストのピークに達します。システムは同時に座席マップを閲覧・購入操作するユーザーを最大10,000人まで入場させ、毎秒最大2,000件の仮押さえ試行と毎秒1,000件の決済承認を処理します。仮押さえは5分間持続し、購入されなかった仮押さえ座席は再び販売可能にならなければなりません。
決済プロバイダーは承認(authorization)と売上確定(capture)の分離をサポートしていると仮定します。座席マップのp99目標は500ミリ秒未満、仮押さえ作成は300ミリ秒未満、注文ステータス読み取りは200ミリ秒未満です。アクセス数、レイテンシ目標、仮押さえ期間、および決済レートは面接用の前提条件であり、特定のチケット販売会社が公開しているベンチマークではありません。正確性に関する中核ルールは、1つのイベントにおける1つの座席は、最大でも1つの有効な販売済み注文にのみ属するということです。可用性が低下した場合、サービスは入場を一時停止したり仮押さえを拒否したりすることはあっても、在庫を推測して販売を継続してはなりません。
スコープには、イベント閲覧、仮想待合室、座席マップ、アトミックな複数座席の仮押さえ、チェックアウト、発券前の注文確定、期限切れ処理、および障害復旧が含まれます。イベント作成、動的価格設定、二次流通(リセール)、返金会計、および完全なボット検知システムはスコープ外ですが、これらが在庫および決済の境界とどのように接するかを回答で定義する必要があります。2026年時点の公開システム設計資料でも、チケット予約は並行性と瞬間的アクセス集中(フラッシュクラウド)が組み合わさった問題として依然として提示されています。Ticketmasterの公開資料でも、購入者が仮想待合室に入り、制御されたレートで座席選択とチェックアウトに進む実際のフローが説明されています。
面接官が評価するポイント
第1のシグナルは、候補者がキューの公平性と在庫の正確性を分離しているかどうかです。仮想待合室はオリジンを保護し、販売への入場を制御します。待合室だけでは、入場した2人の購入者が同じ座席を要求するのを防ぐことはできません。最終的な排他制御は、信頼できる正規の(authoritative)在庫トランザクションが提供する必要があります。
第2のシグナルは、データベースの行ロックとビジネス上の仮押さえの違いの理解です。PostgreSQLの行ロックは短いトランザクション内に属し、トランザクションの終了時に解放されます。購入者が詳細を入力して支払う5分間もの間、コネクションとトランザクションを開いたままにしておくことは安全ではありません。より長期的なビジネス上の意味合いは、hold_idとexpires_atを持つ永続的な状態として保持される必要があります。
第3のシグナルは、時間と再試行を認識した状態遷移です。遅延した期限切れ処理ワーカーが、別の新しい購入者が現在保持している座席や、すでに販売された座席を誤って解放してはなりません。クライアントのタイムアウトによって2重の仮押さえや決済が作成されてはなりません。すべての状態遷移は現在の状態、所有者、バージョン、および期限をチェックし、冪等性キー(idempotency key)によって元の結果を取得できるようにします。
第4のシグナルは、正しい決済境界です。承認が成功してもローカルの確定が失敗する可能性があります。販売トランザクションがコミットされてもそのレスポンスが失われる可能性があります。売上確定がWebhookの到着より先に成功する可能性があります。優れた回答では、注文と決済操作のレコードを保持し、状態照会、Webhook、および照合(レコンシリエーション)を通じて状態を収束させます。応答がないことを単純な失敗として扱いません。
最後に、キャパシティと検証は実際のボトルネックに対処しなければなりません。CDN、キャッシング、リードレプリカによって閲覧トラフィックを吸収できます。1つのアクセス集中イベントに対する座席の書き込みは、依然として1つのトランザクション可能な信頼できる正規のシャードに配置されるべきです。1つのイベントの座席を早期に複数の書き込みシャードに分割すると、通常の3席の仮押さえが分散トランザクションになってしまいます。
回答前に確認すべき質問
- 座席は指定席ですか、それとも自由席(一般入場)ですか? 指定席には座席ごとの排他的な状態が必要です。自由席は、マイナスにならない条件付きデクリメントを伴う、チケット階層ごとの販売可能数量としてモデル化するのが適しています。
- 複数座席のリクエストはall-or-nothing(全取得または全失敗)である必要がありますか? このプロンプトでは、1〜6席のall-or-nothingの仮押さえが必要です。部分的な成功は、価格設定、解放動作、および購入者体験を変化させます。
- キューの公平性ポリシーは何ですか? FIFOは早い到着を優遇します。ランダム選出はミリ秒単位のネットワーク差異の影響を減らします。ポリシーは開示され、販売の途中で変更して順序通りの公平性を主張することのないよう、安定している必要があります。
- 決済の承認と売上確定は分離できますか? 可能な場合は、承認し、座席を確定してから売上確定します。即時売上確定の場合、代金が引き落とされたものの座席の確定に失敗した際に補償処理が必要になります。
- アカウントごとのチケット制限は何枚ですか? 仮押さえと注文確定の両方で強制します。ブラウザ側のみの制限は回避可能です。
- 複数のリージョンが同じイベントに書き込むことはありますか? 基本設計では各イベントに1つの書き込みホームを割り当て、閲覧とキューイングを他の場所で処理します。並行するマルチリージョン書き込みには、リージョン間コンセンサスまたは事前割り当てされた在庫が必要となり、レイテンシと障害時の動作の両方が変化します。
- 座席マップの鮮度はどの程度必要ですか? 数秒の古さが許容される場合は、スナップショットと差分(デルタ)を使用します。表示されるすべての
AVAILABLE状態は、仮押さえトランザクションが成功するまでは暫定的な案内にすぎません。 - 決済認証の実行中に仮押さえの有効期限が切れた場合はどうなりますか? 1回限りの制限付き
PAYMENT_PENDING猶予期間を定義します。無制限に延長したり、決済結果が不明な間に再販売したりしてはなりません。
30秒の回答フレームワーク
「私は200万人のアクセスをイベント単位の仮想待合室に保持し、在庫シャード、決済プロバイダー、およびチケット発券システムがサポートできるレートでのみ、有効期限の短い署名付き入場トークンを発行します。閲覧と座席マップにはCDN、キャッシング、およびバージョン管理された差分を使用し、仮押さえの書き込みはイベントの単一の正規データベースシャードに送ります。短いトランザクションで選択された座席をソート順にロックし、すべての座席が利用可能か期限切れの場合にのみ、データベース時刻に基づく5分間の有効期限を持つ1つのhold_idを書き込みます。チェックアウト中にデータベースのロックが開いたままになることはありません。遅延キューによって期限切れのクリーンアップを促進しますが、解放処理は仮押さえID、状態、期限を条件付きで照合して行います。決済は安定した操作IDを使用してまず承認を行います。承認後、トランザクションが仮押さえを再検証し、売上確定の前に座席、注文、およびOutboxを販売済みとしてコミットします。タイムアウトは状態不明として扱い、Webhook、照会、および照合によって収束させます。同一座席の競合、期限切れと決済の競合、応答消失、重複メッセージ、シャード障害を用いてこの設計を検証します。」
ステップごとの詳細解説
ステップ1:トラフィック量を適正な入場制御目標に変換する
300秒間で200万人のアクセスは、平均すると毎秒約6,667人になります。毎秒50,000リクエストというエッジのピークは、販売開始の瞬間やブラウザのリロードが平均よりもはるかにバースト的であることを示しています。在庫システムは毎秒2,000件の仮押さえ試行と毎秒1,000件の決済承認しか処理できない見積もりであるため、エッジでの再試行トラフィックが直接在庫サービスに到達してはなりません。
各仮押さえには最大6席を含めることができるため、毎秒2,000リクエストは、競合やロールバックが発生する前に毎秒最大12,000件の座席行に対する判定を引き起こす可能性があります。キャパシティテストには、人気セクションへの偏ったトラフィック分布が必要です。これら12,000件の判定を50,000席に均等に分散させてしまうと、競合が隠蔽されてしまいます。
仮想待合室はevent_idごとに分離されており、販売開始前から訪問者を受け入れることができます。待合室は到着コホート、入場時刻、アカウント、およびリスクシグナルを記録します。入場が許可されると、訪問者はイベントとアカウントに紐づく、有効期限の短い署名付きのadmission_tokenを受け取ります。販売エントリポイントでは、トークン、有効期限、および1つのアクティブセッションを検証し、無効なトラフィックはエッジで拒否されます。入場レートは、在庫のロック待機時間、仮押さえのp99、決済エラー、アクティブな買い物客数、および完了率に応じて調整されます。データベースの処理速度が低下した場合は、リードレプリカを増やして書き込み競合を隠蔽するのではなく、入場数を減らします。
Cloudflareの公開待合室ドキュメントでは、訪問者が最初にアクティブキューに到達したタイムスタンプからのFIFO順序と、待機中の訪問者をランダムに選択するモードが説明されています。これらは製品ポリシーであり、データベースの一貫性メカニズムではありません。FIFOであっても、時間の粒度、再接続、複数デバイス、時刻の信頼できる基準(クロックオーソリティ)、およびボットに関する明示的なルールが必要です。グローバルネットワーク全体でリクエスト単位の絶対的な順序を保証することはできません。
ステップ2:閲覧、座席ビュー、および信頼できる正規の在庫を分離する
イベントの詳細、静的な会場マップ、および価格の説明は、CDNまたはキャッシュに配置できます。座席ビューは、バージョン管理されたスナップショットとステータスの差分を組み合わせます。クライアントはまずsnapshot_versionを受け取り、次にseat_id、新しいステータス、およびより高いバージョンを含む変更を適用します。切断後やバージョンのギャップが生じた場合は、スナップショットを再読み込みします。10,000人のアクティブな買い物客が5秒ごとにポーリングすると、毎秒約2,000件の読み取りが発生します。差分配信によりマップ全体の重複読み取りが削減されますが、差分配信自体が在庫の信頼できる情報源(Single Source of Truth)になるわけではありません。
座席マップは一時的に古くなる可能性があります。購入者はAVAILABLEを見ていても、仮押さえの競争に負けることがあります。キャッシュは、データベースが利用できない間に、古いデータから販売を確定したり仮押さえを受け入れたりすることはできません。これにより、可用性の高い閲覧パスと、強一貫性を持つ在庫書き込みパスが分離されます。
すべてのイベントを1つのプライマリ書き込みシャードに割り当て、その中でリレーショナルトランザクションを使用します。異なるイベントはevent_idによって水平方向にスケーリングされ、非常に人気のあるイベントには専用のシャードまたはリソースプールが割り当てられる場合があります。50,000の座席行自体は容量の問題ではありません。同じ少数の座席を対象とする多数のリクエストによって引き起こされるロック待ち、コネクションの枯渇、および再試行の嵐こそが問題なのです。
ステップ3:API、状態、および不変条件を定義する
コアAPIはシンプルに保つことができます。
POST /events/{eventId}/holds
{ seatIds, idempotencyKey, admissionToken }
-> { holdId, status, expiresAt, seats }
POST /holds/{holdId}/checkout
{ paymentMethodToken, idempotencyKey }
-> { orderId, paymentStatus, nextAction }
GET /orders/{orderId}
-> { orderStatus, paymentStatus, seats }最小限のデータモデルは次のとおりです。
SeatInventory(event_id, seat_id, price_version, status,
hold_id, hold_expires_at, order_id, version)
Hold(hold_id, event_id, account_id, status, expires_at,
idempotency_key, request_digest, created_at)
Order(order_id, hold_id, account_id, amount_minor, currency,
status, payment_operation_id, created_at)
PaymentOperation(operation_id, order_id, provider_reference,
kind, status, idempotency_key, updated_at)
Outbox(event_id, aggregate_id, event_type, payload, published_at)重要な不変条件は次のとおりです。
one (event_id, seat_id) has at most one valid SOLD order
all seats in one hold share the event, account, and expiry
only the current hold_id may move HELD to SOLD or release it
order confirmation revalidates price version, quantity, limit, and total
one idempotency key describes one immutable request; changed parameters are rejected金額は通貨の最小単位の整数(例:セント単位)です。クライアントから送信された合計金額は信頼されず、サーバーがロックされた価格バージョンから再計算します。admission_tokenは販売への入場を許可するものであり、いかなる座席の所有権も付与しません。
ステップ4:短いトランザクションでアトミックな複数座席の仮押さえを作成する
1つのリクエストで最大6席を選択します。トランザクションはseat_idでソートし、その固定された順序でSELECT ... FOR UPDATEを使用して行をロックすることで、逆のロック順序によって引き起こされるデッドロックを削減します。すべての行がAVAILABLEまたは期限切れのHELDである場合、トランザクションはすべての行に同じhold_idとexpires_at = database_now + 5 minutesを書き込み、HoldとOutboxイベントを挿入します。1つの行でも販売済みであるか、別の有効な仮押さえに属している場合、トランザクション全体がロールバックされます。
ステータスと期限の述語を指定した条件付きUPDATEも有効な実装アプローチです。ただし、更新された行数が要求された数と等しいことをサービスが確認する必要があります。どちらのアプローチでも、判定と書き込みは1つの正規トランザクションを共有する必要があります。Redisロックを取得して非同期でデータベースに書き込む方式は、一方が成功して他方が失敗した際に2つの信頼できる情報源を作り出してしまいます。「利用可能」を読み取ってから無条件で書き込む方式には、当初の競合状態がそのまま残ります。
PostgreSQLのドキュメントには、行ロックは同じ行に対する書き込みおよび他のロッカーをブロックし、トランザクション終了時に解放されると記載されています。したがって、購入者の5分間のチェックアウトは永続的なデータ状態であり、5分間のデータベーストランザクションではありません。直ちにコミットしてコネクションを解放し、その後の各状態遷移に対して新しい短いトランザクションを開きます。
冪等性レコードを仮押さえ結果とともにコミットします。サービスが仮押さえをコミットした後に応答する前にクラッシュした場合、同じidempotencyKeyでの再試行によって元のholdIdが返されます。新しいキーは新しい競争に入ります。同じキーを異なる座席グループに再利用できないように、リクエストダイジェストを保存します。
ステップ5:ジョブが遅延しても期限切れ処理が正しく動作するようにする
hold_expires_atはデータベース時刻から生成します。読み取り処理では期限切れの仮押さえを利用可能として表示できますが、それを再確保する際は書き込みトランザクション内で期限をチェックします。仮押さえの作成時に遅延メッセージがスケジュールされ、定期的なスキャンによって補償処理が提供されます。これらはいずれも明示的なAVAILABLEへの遷移を促進しますが、どちらも期限切れの正確性の唯一の情報源ではありません。
解放操作は次のようになります。
UPDATE seat_inventory
SET status = 'AVAILABLE', hold_id = NULL, hold_expires_at = NULL
WHERE event_id = :eventId
AND hold_id = :holdId
AND status = 'HELD'
AND hold_expires_at <= database_now;遅延メッセージは重複したり、遅れたり、順序が狂ったりする可能性があります。新しいhold_idが現在その座席を所有している場合、古い述語は一致しなくなります。座席がSOLDである場合、そのステータスは一致しなくなります。seat_idのみで解放を行うと、別の購入者の有効な仮押さえを削除してしまう可能性があります。
追加の決済認証によって仮押さえの期限に近づくことがあります。承認が開始される前に、この設計では有効な仮押さえのHold.statusを、1回限りの制限付き猶予延長を伴ってアトミックにPAYMENT_PENDINGに移行させることができます。猶予期間中の占有も、アクティブな在庫およびアカウント制限に対してカウントされます。新しい期限になっても結果が不明なままの場合は照合処理に入ります。クライアントが無制限に延長して座席を買い占めることはできません。
ステップ6:状態マシンに結果不明な決済結果を組み込む
チェックアウトでは安定したpayment_operation_idを作成し、プロバイダーに対してその冪等性キーを再利用します。Stripeの公開APIドキュメントでは、同じ冪等性キーでリクエストを再試行すると、最初のリクエストの保存された結果が返されると説明されています。PaymentIntent形式のオブジェクトは、追加の認証や非同期完了が必要となる可能性のある決済のライフサイクル状態も公開します。
次のシーケンスを使用します。
- 短いトランザクションで、仮押さえ、チケット制限、価格、および期限を検証し、
Orderと承認操作を作成します。猶予期間が必要な場合は、同じトランザクション内でHold.statusをPAYMENT_PENDINGに移行します。 - トランザクションの外部で決済承認を呼び出します。タイムアウトは
UNKNOWNとなり、2つ目の操作は作成しません。 - 承認が確認された後、座席を再度ロックし、
hold_idを照合し、すべての座席をSOLDに設定し、注文をCONFIRMEDに設定し、Outboxイベントを1つのトランザクションで書き込みます。 - コミット後に売上確定(capture)を行います。売上確定の再試行には同じ操作IDを使用します。チケット発券は、コミットされた確定イベントのみを消費します。
- 承認は成功したものの仮押さえが確認できなくなった場合は、承認を無効化(void)します。販売トランザクションがコミットされ、売上確定が不明な場合は、Webhook、能動的な照会、および照合が収束するまで座席と注文を保持します。それらを解放して再販売してはなりません。売上確定が確定的に失敗し再試行できない場合は、チケット発券前の監査可能な補償トランザクションで注文をキャンセルし、その座席を解放します。
この順序付けにより、一時的な「販売済み、売上確定保留中」の状態が許容されますが、同じ座席が2人の購入者に渡ることは決してありません。ビジネス要件として即時売上確定しか行えない場合、課金成功後に販売トランザクションが失敗した際には、監査可能な無効化または返金が必要になります。そのプロバイダー機能は顧客リスクおよび運用リスクを増大させるため、明示的に述べる必要があります。
座席と注文の状態は単調に進行します。成功ページへのブラウザのリダイレクトによってチケットの発券を許可することはできません。検証済みのプロバイダー応答、Webhook、または能動的な照会のみが決済状態を進めることができます。WebhookはプロバイダーのイベントIDで重複排除し、正当な遷移のみを受け入れます。
ステップ7:障害、スケーリング、およびデグラデーションの設計
- イベントシャードが利用できない場合: そのイベントの入場と新しい仮押さえを停止し、ユーザーを待合室に留めます。閲覧画面に「予約は一時的に利用できません」と表示することはあっても、古いレプリカが販売を継続してはなりません。
- キャッシュまたは差分配信が失敗した場合: 頻度を落としたバージョン管理スナップショットにフォールバックします。仮押さえの判定は引き続きデータベースが行います。
- 期限切れメッセージが失われた場合: 条件付き再回収と補償スキャンによって座席を復旧します。期限切れのバックログと最も古い期限超過仮押さえを監視します。
- 決済プロバイダーの処理が遅延した場合: 入場とチェックアウトの並行数を減らし、状態不明の操作を保持します。自動的にプロバイダーを切り替えて2重課金のリスクを冒してはなりません。
- コミット後に応答が失われた場合: 冪等性キーを使用して、元の仮押さえ、注文、または決済操作を取得します。
- 1つのイベントが極端にアクセス集中した場合: 専用のデータベースリソースを割り当て、イベントごとにレート制限をかけ、不要不急の読み取りを削減します。1つの複数座席トランザクションをシャード間で分割してはなりません。
- リージョン障害が発生した場合: 各イベントは1つの書き込みホームと検証済みのフェイルオーバーを持ちます。新しいプライマリが引き継ぐ前に、古いプライマリが書き込みを継続できないことを証明し、アクティブ-アクティブによる過剰販売を回避します。
待合室には、トークンのコピー、リプレイ、およびバイパスに対する耐性も必要です。トークンは署名され、有効期限が短く、イベントとアカウントに紐づけられます。サービスは並行セッションとチケット数量を制限し、高リスクのトラフィックには追加の検証を実施します。これらの制御はボットの優位性を低下させます。ただし、これらは購入者が人間であることや、割り当てが完全に公平であることを独立して証明するものではありません。
ステップ8:フォールトインジェクションによる不変条件の検証
機能テストに加えて、機械的にチェック可能な特性をアサートします。
count(valid SOLD orders for one seat) <= 1
every CONFIRMED order owns exactly its recorded seats
every SOLD seat points to one CONFIRMED or payment-reconciling order
an expiry action changes only its own hold_id
replaying one idempotent request does not create new business state1つの座席に対して数千の並行クライアントを送信し、有効な仮押さえが1つだけになることを確認します。次に、all-or-nothingの動作、固定ロック順序、およびデッドロックの再試行を検証するために、クライアントに異なる順序で同じ3席のセットを要求させます。新しい仮押さえ、期限切れメッセージ、決済承認、および確定をインターリーブさせながら、期限切れ境界をまたいで時間を進めます。古いクリーンアップタスクが新しい状態を変更してはなりません。
仮押さえのコミット直後、承認応答の返却前後、販売コミット後、売上確定送信後、Outbox公開前後にプロセスを強制終了したり応答を消失させたりします。Webhookや遅延メッセージを重複させ、キャッシュ、決済プロバイダー、データベースプライマリを切断します。結果は、イベントレベルの販売数、ロック待機時間、条件付き競合率、仮押さえ期限切れ率、状態不明決済の滞留時間、キュー待機および入場レート、チケット発券の照合差異を用いて評価します。HTTP 200が返るだけではほとんど何も証明できません。
質の高い模範解答
「私はこの問題をトラフィック保護と在庫の正確性に分割します。5分間に200万人のアクセスは平均で毎秒約6,667人、エッジピークは毎秒50,000リクエストに達しますが、在庫システムは毎秒2,000件の仮押さえ試行しか受け付けません。そこで、イベント単位の仮想待合室を作成します。これはエッジでリロードトラフィックを吸収し、イベントシャードと決済パスの健全性に応じて、有効期限の短いアカウント紐づけの入場トークンを発行します。キューの順序付けは明示的な製品ポリシーであり、座席の排他制御を提供するものではありません。
イベントページと静的な会場マップにはCDNを使用します。座席マップにはバージョン管理されたスナップショットと差分を使用し、数秒の遅延を許容します。最終決定は仮押さえ処理のみが行います。各イベントには1つのリレーショナルデータベース書き込みシャードがあり、その50,000席が複数のシャードに分割されることはありません。仮押さえAPIは、1〜6個のソートされた座席IDと冪等性キーを受け取ります。短いトランザクションでそれらの行をロックし、すべての座席が利用可能または期限切れの場合にのみ、データベース時刻に基づく5分間の有効期限を持つ1つのhold_idを書き込みます。それ以外の場合はすべて失敗します。トランザクションはコミット時にロックを解放します。チェックアウトはデータベースのロックではなく、永続的な仮押さえ状態を保持します。
遅延キューとスキャナーが期限切れの仮押さえをクリーンアップしますが、すべての解放処理はhold_id、HELD、および期限を照合します。したがって、遅れて届いた古いメッセージが新しい仮押さえや販売済み座席を解放することはありません。冪等性キーとリクエストダイジェストは仮押さえとともにコミットされるため、コミット後に応答が失われても元の結果が取得されるだけです。
決済については、プロンプトの前提に従い承認と売上確定を分離します。安定した決済操作を作成し、まず承認を行います。タイムアウトは状態不明として扱われ、照会または再試行に同じ操作IDを使用します。承認後、トランザクションが仮押さえ、価格、制限、および期限を再検証し、座席をSOLDに、注文をCONFIRMEDに移行し、Outboxを書き込みます。売上確定とチケット発券がその後に続きます。承認は成功したものの座席の確定に失敗した場合、承認を無効化します。座席が確定され売上確定が不明な場合は、注文を保持し、Webhook、照会、および照合を通じて収束させます。決して解放して再販売することはありません。
スケーリングはイベント単位で行われます。通常のイベントはシャードを共有し、人気イベントには専用リソースが割り当てられ、1つのイベントは1つの書き込みホームを持ちます。データベース、決済プロバイダー、または発券システムが遅延した場合、待合室は入場数を減らし、仮押さえを一時停止できます。検証としては、数千のクライアントが1つの座席や重複する座席セットを競合する状況を作り、遅延した期限切れ、コミット後のクラッシュ、決済応答の消失、重複Webhook、キャッシュ消失、プライマリ障害を注入します。この設計は、1席あたり最大1つの有効な販売済み注文しか存在しないこと、リプレイによる新しい状態が作成されないこと、古い期限切れアクションによって新しい仮押さえが変更されないことが証明されて初めて合格となります。」
よくある間違い
- Redisでロックし、在庫を非同期で書き込む → ロックとデータベース書き込みの不一致が発生し、2つの信頼できる情報源が生まれる → 条件判定、永続的な仮押さえ、および冪等な結果を1つの正規データベーストランザクションで実行する。
- 5分間のチェックアウト中に行ロックを保持し続ける → 長時間のトランザクションはコネクションを消費し、書き込みをブロックし、離脱後の復旧が困難になる → ロックは状態遷移にのみ使用し、チェックアウトは永続的な仮押さえ状態で表現する。
- 仮想待合室が過剰販売を防ぐと思い込む → 待合室は入場を制御するだけで、入場した購入者は依然として同じ行で競合する → 在庫トランザクション内で排他制御を個別に強制する。
- 座席IDのみで期限切れを解放する → 遅延したメッセージが新しい仮押さえや販売済み状態を消去してしまう可能性がある →
hold_id、状態、および期限をセットで照合する。 - 座席マップで利用可能と表示されているからといって課金を行う → 読み取りモデルは古くなっている可能性があり、別の購入者が先に獲得する可能性がある → 決済の前に信頼できる正規の仮押さえを確保する。
- 決済タイムアウト後に解放し、新しいキーで再試行する → 最初の操作が成功していた可能性があり、再販売や2重課金を引き起こす → 状態不明を保持し、操作IDを再利用し、Webhook、照会、および照合を通じて収束させる。
- アクセス集中イベントのすべての座席をランダムにシャーディングする → 1つの複数座席の仮押さえがシャードをまたぎ、単純なアトミック性が失われる → 測定されたキャパシティによってより複雑な在庫パーティションが必要であると証明されるまでは、イベントを1つの書き込みシャードに保持する。
- 厳密なグローバルFIFOを保証すると約束する → ネットワークレイテンシ、再接続、デバイスの切り替え、およびボットによって観測可能な順序が変化する → キューの粒度、同一性、再接続、およびリスクポリシーを、検証可能な公平性契約として定義する。
- 平均スループットのみを負荷テストする → 販売開始時の需要は少数の座席に集中し、一様分布のテストではロック待ちや再試行の嵐が隠蔽されてしまう → 人気座席、重複するグループ、瞬間的なバースト、および依存関係の遅延をテストする。
フォローアップの質問と回答
フォローアップ1:チケット階層の数量のみが過剰販売されてはならない自由席(一般入場)の場合、何が変わりますか?
座席ごとの状態をInventoryBucket(event_id, tier_id, available, held, sold, version)に置き換えます。仮押さえトランザクションは条件付きでavailable >= quantityを要求し、1回デクリメントして仮押さえを作成します。期限切れと確定は、引き続きhold_idによって冪等にカウントを移動させます。競合の激しい階層では、書き込みを分散させるために複数のバケットを使用できますが、クォータ制御プレーンがバケット容量を割り当て、それらの合計を実際の在庫内に維持する必要があります。アトミックな座席選択を排除することでストレージは簡素化されますが、期限切れや決済結果不明の競合は依然として残ります。
フォローアップ2:承認に平均90秒かかり、追加の認証が5分を超えることがあります。長時間の仮押さえをどのように阻止しますか?
決済が開始される前に残り時間を確認し、残りが少なすぎる場合は新しい認証フローを拒否します。開始されたら、仮押さえを1回限りの制限付きPAYMENT_PENDING猶予状態に移行し、その在庫に対してアカウントごとおよびイベント全体での制限を設けます。猶予期限が切れたら、新しい処理の開始を停止し、プロバイダーに照会します。承認がないことを確認した後にのみ解放し、確認された承認は完了させるか無効化します。クライアントに何度も延長させるのではなく、コンバージョンと在庫回転率のデータを使用して5分間の仮押さえと猶予期間を調整します。
フォローアップ3:販売中にプライマリ書き込みリージョンが消失しました。別のリージョンが直ちに引き継ぐべきですか?
タイムアウトのみに基づいて2つ目のライターを開始してはなりません。そのイベントの入場と新しい仮押さえを一時停止します。コンセンサスリース、データベースフェイルオーバー、または古いプライマリの隔離(フェンシング)を使用してライターが1つだけであることを証明し、その後、新しいプライマリが確認済みのログ位置から処理を再開できるようにします。中断の間も、キューイングと読み取り専用の縮退運転は継続できますが、一意性を証明できない仮押さえは行えません。入場数を再び増やす前に、注文、座席、および決済操作を照合します。
フォローアップ4:不正対策を正確性の依存関係にすることなく、公平性を向上させボットを制限するにはどうすればよいですか?
認証済みアカウント、購入制限、レート制限、デバイスおよび行動シグナル、高リスクセッションに対する追加のチャレンジを使用します。キュートークンは署名され、有効期限が短く、イベントとアカウントに紐づけられます。重複セッションはポリシーによって統合または拒否されます。販売では、大まかな到着時刻に基づくFIFOを使用するか、事前待機ユーザーにランダムに順位を割り当てることができます。リスク検知がボットを見逃したかどうかに関係なく、データベースは同一の仮押さえ、制限、および注文の不変条件を強制します。リスク制御の見逃しは割り当ての公平性には影響しますが、過剰販売を引き起こしてはなりません。
フォローアップ5:既存の分散ロックサービスですべての座席を保護しないのはなぜですか?
信頼できる正規の在庫がすでに条件付き書き込みと短いトランザクションを備えたリレーショナルデータベースにある場合、独立したロックサービスを導入すると、在庫と一致させなければならない第2の状態が生じ、さらにリースの有効期限やフェンシングの境界問題が発生します。行ロックまたは条件付き更新は、排他制御と永続的な書き込みを1つのトランザクション内に配置するため、よりシンプルです。分散ロックを検討するのは、在庫が互いにトランザクションを実行できない複数のデータストアにまたがっている場合や、クリティカルセクションが外部リソースを保護している場合のみです。その場合でも、永続ストアには遅れてきたロック保持者を拒否するためのバージョンまたはフェンシングトークンが依然として必要です。