代表的な面接トピック

システム設計面接:リアルタイムゲームリーダーボードの設計方法

システム設計難しい
Offer.cc 編集チーム公開日 更新日

質問

リアルタイムのゲームリーダーボードを設計してください。各シーズンには5000万人のアクティブプレイヤーが存在し、ピーク時には毎秒20万回のスコア更新と毎秒100万回の読み取りが発生します。システムはグローバルトップ100、プレイヤーの正確な順位、およびその前後のプレイヤーを表示する必要があります。更新は5秒以内に反映される必要があり、読み取りのp99は100ミリ秒未満を維持しなければなりません。スコアは0から1,000,000までの整数で、プレイヤーのシーズン最高スコアのみが保持され、同点の場合は順位を共有(タイ)します。API、データモデル、スコア検証、順序付けとシャーディング、整合性、シーズン集計、リカバリ、キャパシティ、および検証について説明してください。

プロンプトとスコープ

対戦型ゲーム向けのシーズンリーダーボードを設計します。1シーズンには5000万人のアクティブプレイヤーが存在し、毎秒最大20万回のスコア更新を受信し、毎秒最大100万回の読み取りリクエストを処理します。プレイヤーにはグローバルトップ100、自身の正確な順位、および前後10人のプレイヤーを表示する必要があります。送信されたリザルトは5秒以内に反映され、読み取りのp99は100ミリ秒未満を維持する必要があります。スコアは0..1,000,000の整数であり、各プレイヤーはシーズン中の最高スコアのみを保持します。同点の場合は順位を共有するため、1位、2位、2位の次は4位となります。同点のプレイヤーはplayer_idによって安定した表示順序を持ちますが、その順序によって順位自体が変わることはありません。

規模、レイテンシ、スコア範囲は面接における前提条件です。クライアントが信頼できるスコアを直接宣言することはできません。マッチサービスがリザルトの検証と不正対策チェックを完了した後に、リーダーボードが消費できるイベントを生成します。この課題では、順序付きインデックス、読み書きの分離、ホットキーのシャーディング、整合性、再構築可能性、およびシーズンのライフサイクルが評価されます。不正検知モデル自体は求められていません。

公開されている技術面接ガイドでは、システム設計の例として「ゲームのリーダーボードの設計」が挙げられており、ランキングロジック、読み書きのパフォーマンス、リアルタイム更新がテストされると記載されています。また、2026年に公開されたRedisリーダーボードのチュートリアルでも、スコア更新、Top-N取得、順位検索を行う基本的なSorted Setの手法が解説されています。公開情報からは特定の企業への帰属を特定できないため、companyNameはnullのままとしています。

面接官が評価しているポイント

第1のシグナルは、候補者が「順位(ランク)」を明確に定義しているかどうかです。高スコア順に並べること自体は簡単ですが、同点の場合に順位を共有するのか、先に達成した方を優先するのか、プレイヤーIDでタイブレークするのかによって、データモデルが直接変化します。この契約(仕様)がなければ、2つのサービスが同じプレイヤーに対して異なる順位を生成してしまう可能性があります。

第2のシグナルは、1つのSorted Setが解決できるのは1つの順序付きコレクションであると認識しているかどうかです。Redisでは、ZADDによる更新やZREVRANKによる順位検索をO(log N)として文書化しており、中規模のリーダーボードには有用です。しかし、Redis Clusterはキー単位でハッシュスロットを割り当てます。グローバルボードを1つのキーに保持している場合、クラスタノードを追加したからといって5000万人のメンバーが自動的に分散されるわけではありません。優れた回答では、まずシンプルなアプローチを提示し、1つのキーがメモリ、書き込み、またはリカバリの許容量を超えた場合にのみ、アプリケーションレベルのシャーディングを追加します。

第3のシグナルは、「5秒以内の正確性」を一貫した読み取りバージョンへと落とし込めるかどうかです。プレイヤーをあるスコアバケットから別のスコアバケットに移動させる際、「削除してから追加」すると一時的な消失が発生し、「追加してから削除」すると重複が発生します。また、ある瞬間のバケットカウントと別の瞬間のバケット順序を読み取ると、誤った順位が算出されます。テスト可能な設計では、1つのリクエストが完全に公開された1つのバージョンのみを読み取るようにします。

最後に、設計が運用可能である必要があります。スコアイベントには冪等性が必要であり、修正によってスコアを引き下げられる必要があり、シーズン終了時には一貫したスナップショットを凍結(フリーズ)でき、キャッシュが失われた場合は信頼できる台帳(Ledger)から再構築可能でなければなりません。「ゲームサービス → Redis」としか書かれていない構成図では、重複、ホットスポット、データ損失、不正スコアの取り消し、あるいは集計時の不一致に対処できません。

回答前に明確にすべき質問

  • スコアは累積値、最高値、最新値のどれですか? 本プロンプトではシーズン最高値を保持します。累積スコアの場合は、重複イベントによって二重加算されるのを防ぐため、イベントの一意性とアトミックなインクリメントが重要になります。管理者がスコアを修正できる場合、APIにはZINCRBYだけでなくバージョン管理された絶対値が必要です。
  • 同点はどのように順位付けされますか? 本プロンプトでは競技用ランキング(Competition ranking)を採用します。厳密に高いスコアのみをカウントし、同点のプレイヤーは順位を共有します。「先着優先」にする場合は、ソートキーに信頼できる達成時刻を含める必要があり、ZSETのデフォルトの辞書順タイブレークでは対応できません。
  • 正確性と最新性の要件はどのようなものですか? 公開されるバージョンは、最大5秒の遅延(staleness)を許容しつつ、正確である必要があります。線形化可能性(Linearizability)を伴う即時可視性を求めると、書き込みパスにクロスシャードの協調が必要になります。近似順位でよい場合は、サンプリングや分位点スケッチ(Quantile sketches)を用いたはるかにシンプルな設計が可能です。
  • どのボードが必要ですか? メインとなるのはグローバルシーズンボードです。少数の固定されたリージョンについては、別個のマテリアライズドビューを持つことができます。フレンドボードは通常、プレイヤーごとにボードを維持するのではなく、フレンドのスコアを一括取得してリクエストごとにソートします。
  • どのような読み取りがサポートされますか? Top 100、個人順位、および前後20人の周辺プレイヤーです。無制限の深いページネーションは負荷の高いスキャンを引き起こすため、ウィンドウを制限し、バージョン管理されたカーソルを使用する必要があります。期限切れのバージョンの場合は再配置が必要です。
  • スコアは誰が確定しますか? 信頼できるマッチリザルトサービスのみが書き込みを行います。クライアントからの送信はゲーム入力にすぎず、順序付きインデックスに直接挿入できるスコアではありません。
  • シーズンはいつ終了しますか? サーバーのイベント時刻と明確なカットオフを使用します。遅延して届いたリザルトを拒否するのか、レビューするのか、別枠として扱うのかはプロダクトポリシーで定める必要があり、バックグラウンドジョブが確定済みスナップショットを勝手に変更してはなりません。
  • 何が永続化されますか? 順位ビューは一時的に古くなったり再構築されたりしても構いません。しかし、信頼できるスコア台帳と最終シーズンスナップショットは、キャッシュ障害によって失われてはなりません。

30秒の回答フレームワーク

「順位は1+自分よりスコアが厳密に高いプレイヤーの数と定義し、同点のプレイヤーは同じ順位を共有します。信頼できるマッチサービスが、event_idとプレイヤーバージョンを付与して絶対値の最高スコアを書き込みます。永続化されたスコアテーブルが信頼できる唯一の情報源(Source of Truth)となり、その変更ストリームからリーダーボードをマテリアライズします。小規模であれば、1つのRedis Sorted SetでZADD、Top-N、逆順位に対応できます。5000万人規模でグローバルのホットキーが単一シャードの許容量を超える場合は、固定スコア範囲でパーティショニングします。個人順位は、それより上位の全バケットの合計カウント+プレイヤーのバケット内で自身より高いスコアのカウント+1となります。

バケットをまたぐ移動を途中の状態で公開することはできません。そのため、マテリアライザは数秒ごとのエポック単位でバージョンをコミットします。すべてのバケット、プレフィックスカウント、Top 100の準備が整った後、コーディネータが現在のアクティブマニフェストをアトミックに切り替えます。レスポンスにはleaderboard_versionas_ofが含まれます。書き込みはイベントとバージョンによって冪等性が担保され、キャッシュはスコア台帳から再構築可能であり、シーズン終了時には報酬付与前に完全な1つのバージョンを凍結して照合します」

ステップごとの詳細解説

ステップ1:ストレージを選択する前にAPIと順序付けの契約を確定する。

内部の書き込みパスと公開読み取りを分離します:

text
POST /internal/v1/seasons/{season_id}/scores:apply
GET  /v1/seasons/{season_id}/leaderboard/top?limit=100
GET  /v1/seasons/{season_id}/players/{player_id}/rank
GET  /v1/seasons/{season_id}/players/{player_id}/neighbors?radius=10&version=...

書き込みにはevent_idmatch_idplayer_id、絶対値のbest_scorescore_version、および信頼できる完了時刻が含まれます。マッチリザルトのアイデンティティのみが認可されます。読み取りはleaderboard_versionas_of、スコア、順位、およびメンバーを返します。周辺プレイヤーのリクエストでは、最初のレスポンスのバージョンを維持する必要があります。そうしないと、ページング中の順位変動によってプレイヤーの重複やスキップが発生します。

設計を牽引する4つの不変条件(Invariants):各(season_id, player_id)は1つのバージョン内に1回のみ出現する。公開されるプレイヤースコアは順序付きインデックスと一致する。順位は常に1 + count(score > my_score)に等しい。マニフェストはすべて完了したシャードバージョンのみを参照する。

ステップ2:信頼できるスコアテーブルをSource of Truthにする。

マッチ集計サービスは、不変のリザルト台帳に書き込む前に、認可、マッチ状態、不正防止チェックの結果を検証します。1つのデータベーストランザクション内で、リーダーボードライターはevent_idの重複を排除し、プレイヤーの行を条件付きで更新します。古いバージョンは拒否し、同一の再試行には以前の結果を返し、new_score > best_scoreの場合にのみ通常の最高スコアを更新します。不正スコアを取り消す場合は、より高いscore_versionと修正された絶対値を書き込むため、スコアの引き下げも正しく収束します。

コミット後、アウトボックスまたは同等の永続変更ストリームが以下を発行します:

text
ScoreChanged {
  season_id, player_id, old_score, new_score,
  score_version, event_id, committed_at
}

イベントはプレイヤー単位でパーティショニングされ、マテリアライザはそのプレイヤーの現在のバージョンより新しい変更のみを適用します。重複配信によってポイントが二重加算されることはなく、遅れて届いた古いイベントが新しいスコアを上書きすることもありません。リーダーボードは破棄可能なマテリアライズドビューです。台帳と現在のスコアテーブルが再構築のソースとなります。

ステップ3:まず単一Sorted Setの設計を提示する。

メンバー数、ピーク時の書き込み、メモリ、リカバリ時間が1つのシャードに収まる場合、シーズンごとに1つのZSETを使用するのが適切な出発点です:

text
key    = leaderboard:{season_id}
member = player_id
score  = best_score

ZADDは既存メンバーの新しいスコアを設定し、位置を更新します。降順範囲指定でTop-Nを取得し、ZREVRANKで位置を取得します。Redisでは、更新をO(log N)、固定サイズ範囲をO(log N + M)、順位検索をO(log N)としています。プロンプトの整数範囲0..1,000,000は、double型が整数を正確に表現できる2^53の境界をはるかに下回っています。

同点メンバーは、メンバー値のバイナリ辞書順で並べられます。これは、同点が順位を共有し、IDが表示順序を制御するという契約にのみ適合します。任意のスコアとミリ秒タイムスタンプを1つの浮動小数点値に詰め込んで、複合的な順序が維持されると安易に想定してはいけません。また、同点メンバーに異なる位置が割り当てられるため、ZREVRANK + 1はビジネス上の共有順位にはなりません。ビジネス上の計算式は1 + count(score > my_score)です。1つのZSET内では、ZCOUNT key (my_score +infがそのカウントを計算し、(で下限値を除外します。表示位置とビジネス順位は明確に区別してください。

ステップ4:大規模環境でアプリケーションレベルのシャーディングが必要な理由を証明する。

Redis Clusterはキーをハッシュスロットにマッピングし、安定したスロットは1つのプライマリによって処理されます。leaderboard:{season}というキーは単一のキーであるため、そのメンバー、書き込み、リカバリの負荷は1つのスロットのプライマリに集中したままになります。hash(player_id)でシャーディングすると書き込みは分散されますが、すべてのプレイヤーシャードにまたがってマージまたはカウントを行う必要があるため、グローバル順位の取得が高コストになります。

本プロンプトはスコア範囲が有限であるため、固定スコア範囲のバケットが有効です。バケットごとに10,000ポイントの範囲を持たせると、101個のバケットになります:

text
bucket_id = floor(score / 10,000)
rank(player) = 1
             + count(all buckets with a higher bucket_id)
             + count(score > player_score inside the player's bucket)

各バケット内はソートされた状態が保たれ、バケットキーは異なるスロットに配置できます。バージョンごとに、バケットカウントと高スコアから低スコアへの累積和(プレフィックスサム)を保存します。個人順位を取得するには、プレイヤーの検索、1つのプレフィックス値、およびバケット内で厳密に高いスコアのカウントが必要になります。Top 100は空でない最上位のバケットのみをスキャンし、別途マテリアライズされます。固定範囲ではハイスコアのバケットがホットになる可能性があります。実測に基づいてその範囲をさらに分割しますが、リーダーとライターが同じレイアウトを使用できるように、境界情報をバージョン管理されたマニフェストに含めます。

ステップ5:バージョン管理された公開を利用してバケット間のアトミック性を解決する。

プレイヤーが39,000から51,000に移動すると、システムはバケット3から削除し、バケット5に追加し、2つのカウントを変更します。Redisのスロットをまたがる削除、追加、カウンタ変更は、通常のアトミック操作ではありません。中間状態が公開されると、重複、一時的な消失、またはグローバル順位の1つのズレ(off-by-one)が発生します。

そのため、マテリアライザは短いエポック単位で論理スナップショットをコミットします。各ランクシャードは現在の公開バージョンから開始し、冪等なバッチを適用して、次の順序付きインデックスとカウントを準備します。すべてのシャードが完了を報告すると、コーディネータは総メンバー数、変更カウント、シャードのチェックサムを検証し、current_manifestvからv+1へアトミックに切り替えます。読み取り側はまずマニフェストを取得し、すべてのサブクエリでそのバージョンを引き継ぎます。未完了のバージョンは不可視であり、失敗したシャードは再試行でき、処理中の読み取りが完了するまで以前のバージョンが維持されます。

論理スナップショットは、MVCC、Copy-on-Writeページ、またはベース+差分によって古いデータを再利用できるため、外部向けの不変バージョンを維持しながら、数秒ごとに5000万人の全メンバーをフルコピーするのを避けることができます。エポック期間、適用遅延、公開遅延の合計は5秒未満に収める必要があります。もし収まらない場合は、不完全なバージョンを公開するのではなく、遅延(Staleness)を報告し、リアルタイムサービスの提供を一時停止すべきです。

ステップ6:グローバル、リージョン、フレンドの各ボードを分離する。

グローバルボードにはバケット化されたインデックスを使用します。少数の固定リージョンであれば、同じスコアイベントから独立した(season, region)ビューを維持できます。クライアントがリクエスト中にリージョンを切り替えることができないよう、リージョン情報はサーバーが管理するプレイヤープロファイルバージョンから取得します。グローバルトップNのみをフィルタリングすると、グローバルページから外れた強力なリージョンプレイヤーを見落とすことになります。リージョンインデックスを使用するか、結果を近似値として明記してください。

フレンドボードは通常小規模です。ソーシャルグラフからバージョン付きのフレンドIDを取得し、同一のリーダーボードバージョンでフレンドのスコアを一括読み取りし、アプリケーションサービス側でscore DESC, player_id ASC順にソートして同点順位を計算します。プレイヤーごとにフレンド用のZSETを維持すると、極端な書き込み増幅が発生します。1回のスコア変更がすべてのフレンドのボードにファンアウトされ、フレンド関係の変更時にもバックフィルが必要になるためです。

ステップ7:トラフィックを見積もり、実測値からシャードのサイジングを行う。

エンベロープを含む永続スコアイベント1件を128バイトとすると、ピーク時の論理書き込みの下限は次のようになります:

text
200,000 events/s × 128 bytes = 25.6 MB/s
25.6 MB/s × 86,400 s = 2.21184 TB/day
three-replica log lower bound = 6.63552 TB/day

この非圧縮の下限値には、インデックス、プロトコルオーバーヘッド、バッチ処理、再試行、レプリカリカバリは含まれていません。最高スコアを更新するか修正するイベントのみがランキングを変更しますが、すべての信頼できるイベントは台帳側での重複排除と監査が必要です。毎秒100万回の読み取りすべてをランクシャードに到達させることはできません。Top 100はバージョン単位でキャッシュします。個人リザルトには短いTTLを使用できますが、キーにはプレイヤーとバージョンを含めます。シャードはその上で周辺ウィンドウを一括読み取りします。

記事に書かれている固定のシャード数をそのまま適用してはいけません。本番環境を模した5000万行のデータを使用して、メンバーのメモリ使用量、ZADD、厳密な上位カウント、範囲読み取り、スナップショット構築のベンチマークを実施します。p50/p95/p99、CPU、メモリ、レプリケーション遅延、リカバリ時間を記録し、ピーク書き込みと障害耐性のヘッドルームからバケット分割数とノード数を導き出します。1つのZSETがすべての許容量内に収まるのであれば、独自のバケットコーディネータを構築するよりも信頼性が高くなります。

ステップ8:シーズンを終了し、ビューの再構築可能性を維持する。

シーズンがCLOSINGに入った後、カットオフ前にすでに受け入れられたリザルトはストリームを通じて処理され続けますが、新規の対象外リザルトは拒否されます。入力ハイウォーターマーク(処理上限位置)を記録します。すべてのマテリアライザがその位置に到達したら、最終バージョンの候補を作成します。集計処理では、総プレイヤー数、バケットカウントの合計、Top 100、ランダムサンプリングされた順位、重複プレイヤー数、および各シャードのチェックサムを照合します。検証に成功したら、マニフェストにFINALのマークを付けます。報酬サービスはこの不変バージョンのみを読み取ります。その後の異議申し立ては、確定済みボードを密かに書き換えるのではなく、監査ログ付きの修正フローによって処理されます。

ランキングキャッシュまたはクラスタ全体が失われた場合、score_versionによって現在のスコアテーブルまたは不変台帳を新しいネームスペースにリプレイします。完全な候補を構築して照合した後、アトミックにマニフェストを切り替えます。再構築中、古いバージョンは読み取り専用のまま維持されます。存在しない場合は、不完全なボードを完全なものとして提示するのではなく、一時的に利用不可またはデータが古い(Stale)状態であることを明示的に返します。

障害マトリクスには、イベントの重複や順序の不整合、バケットをまたぐスコア上昇や引き下げ修正、エポック途中のシャードクラッシュ、マニフェスト切り替え前後のコーディネータクラッシュ、キャッシュノードの喪失、Top-100境界での大量の同点発生、毎秒100万回の読み取りQPS、カットオフ前後のリザルト遅延、シャドウ比較を伴う完全再構築などが含まれます。4つの不変条件を継続的にアサートし、イベントから公開までのp99、バージョン経過時間、バケットの偏り、拒否されたイベント、再構築の進行状況、チェックサムエラーを監視します。

模範解答の例

「まず順位を定義します。ここでは同点のプレイヤーは順位を共有するため、プレイヤーの順位は1+自分よりスコアが厳密に高い プレイヤーの数となります。Top-100の表示位置とビジネス上の順位は別物です。クライアントが信頼できるスコアを直接書き込むことはできません。マッチ検証後、リーダーボードライターはイベントIDを重複排除し、プレイヤースコアバージョンに基づいて絶対値の最高スコアを条件付きで設定します。現在のスコアテーブルがSource of Truthとなり、そのコミットされた変更ストリームがランクビューを駆動します。

ボードが1つのRedisシャードに収まる場合は、シーズンごとに1つのSorted Setから始めます。スコア設定、Top-Nの読み取り、スコアによるカウントはシンプルに行えます。しかし、5000万人のグローバルボードは単一のホットキーとなり、Redis Clusterはキー内のメンバーではなくキー単位でシャーディングします。スコア範囲は0..1,000,000に限定されているため、101個の固定スコア範囲バケットにスケールアウトします。個人順位は、上位バケットのカウントプレフィックス+ローカルバケット内の厳密な上位プレイヤー数+1です。Top 100は空でない最上位バケットから取得します。

バケットをまたぐ移動は2つのキーとカウントを変更します。インプレースで更新すると中間状態が公開されてしまうため、最大でも数秒のエポック単位で次のインデックスを構築します。すべてのシャードが冪等な変更を適用し、メンバーシップとチェックサムの照合に合格した後にのみ、コーディネータが現在のアクティブマニフェストをアトミックに切り替えます。すべてのレスポンスにはバージョンと時点(as-of)が含まれ、周辺読み取りはそのバージョンを保持します。公開されるバージョンは、最大5秒の遅延を許容することで正確性を担保します。

1イベントあたり128バイトの場合、ピーク時の書き込みは論理トラフィックで25.6 MB/s、1日あたり約2.21 TBとなり、3レプリカのログ下限は約6.64 TBになります。インデックスノードとバケット数は、本番同等のデータを用いたベンチマークに基づいて決定します。Top 100はバージョン単位でキャッシュします。フレンドボードはフレンドのスコアを一括取得してローカルでソートして構築し、スコア変更ごとのファンアウトを防ぎます。

シーズン終了時は、入力ハイウォーターマークを記録し、すべてのシャードの追いつきを待ってから、最終バージョンの候補を凍結します。報酬サービスがFINALマニフェストを読み取る前に、メンバーシップ、バケット合計、Top 100、サンプリング順位、シャードチェックサムを照合します。失われたキャッシュは、信頼できるスコアテーブルまたは台帳から再構築します。障害テストでは、重複/順序の不整合イベント、バケット間修正、シャードおよびコーディネータのクラッシュ、大量の同点、カットオフ時の競合、完全再構築をカバーします」

よくある間違い

  • クライアントから直接スコアを受け取る → 順序付けストレージはそれが有効かどうかを判断できない → 信頼できるマッチサービスによって確認・監査されたリザルトのみを消費する。
  • 同点順位にZREVRANK + 1を使用する → ZSETは同じスコアに異なる辞書順の位置を割り当てるため、順位共有の契約に違反する → 1 + count(score > my_score)を計算する。
  • 浮動小数点スコアに安易にタイムスタンプを詰め込む → double型には精度の限界があり、複合エンコーディングによってキーの優先度が逆転する可能性がある → まず同点ルールを定義し、実績のある整数エンコーディングまたは複合順序インデックスを使用する。
  • Redis Clusterノードを増やせばグローバルボードのキーが分割されると思い込む → Clusterはキーのハッシュスロットを割り当てるため、単一のキーは1つのスロットプライマリに残る → 単一キーの限界を測定し、マージ可能なビジネスディメンションでシャーディングする。
  • プレイヤーをハッシュシャーディングし、すべての順位クエリをファンアウトする → クエリコストがシャード数に比例して増大し、100万読取QPSで破綻する → 有限のスコア範囲に対するカウントを使用するか、許容される場合は近似順位を返す。
  • バケット間で「削除してから追加」または「追加してから削除」を行う → 読み取り側に一時的な消失、重複、または誤ったカウントが見えてしまう → 完全なバージョンを構築し、マニフェストを通じてアトミックに公開する。
  • すべてのリザルトに対してZINCRBYを使用する → 重複イベントによって二重加算され、不正スコアの取り消し時に安全に引き下げることができない → イベントとプレイヤーバージョンに基づいて絶対スコアを冪等に設定する。
  • カットオフ時に稼働中のキャッシュから直接報酬を発行する → カットオフ前に受け入れられたイベントが処理中である可能性があり、キャッシュは永続的な真実ではない → ハイウォーターマークを記録し、追いつき処理を行い、照合してFINALバージョンを凍結する。
  • Top 100の見た目が正しいことだけを検証する → バケットカウントのエラーやプレイヤーの重複によって、ロングテールのすべての順位がずれる可能性がある → メンバーシップの保存則、一意性、サンプリングされた順位の計算式、シャードチェックサム、完全再構築を検証する。

フォローアップの質問と回答

フォローアップ1:プレイヤーが書き込み直後に結果を読み取る必要がある場合でも、5秒のスナップショットを維持できますか?

書き込みを行ったプレイヤー自身のRead-Your-Writes体験と、グローバルに公開される順位を分離します。書き込みレスポンスでは、コミットされた新しいスコアと保留中(pending)のバージョンを返すことができます。直後の画面更新では、スコアが確定しグローバル順位は更新中である旨を表示するか、グローバル順位は最新の完全なマニフェストを参照したまま新しい個人スコアを表示します。プロダクト要件として同一の同期レスポンス内に新しいスコアと正確なグローバル順位の両方が必要な場合は、更新処理をグローバルに協調された順序付けパスに組み込む必要があります。書き込みレイテンシと障害結合度が高くなるため、元のスループット目標を再評価する必要があります。

フォローアップ2:最高スコアのバケットに1000万人のプレイヤーが集中した場合はどうしますか?

バケットのレイアウトはバージョン管理されたメタデータです。まず書き込みQPS、メンバー数、CPU、p99を通じてホットスポットを実証し、そのスコア範囲をより狭いサブバケットに分割します。共有順位は厳密に高いスコアの数に依存するため、特定のスコアを任意にプレイヤーシャーディングしてローカル順位を単純合算することはできず、その合計カウントを集約したまま維持する必要があります。新しいレイアウトを並行して構築し、同一の入力ハイウォーターマークでメンバーシップとサンプリング順位を比較・検証した上で、新しいレイアウトを参照するマニフェストを公開します。

フォローアップ3:同点スコアに先に到達したプレイヤーが勝利する場合は何が変わりますか?

順位はスコアのみのカウントではなくなります。ソートキーは(score DESC, achieved_at ASC, player_id ASC)となり、achieved_atは信頼できる集計処理から提供されます。通常のZSETはメンバーの辞書順でのみタイブレークします。固定長の整数スコアと反転させたメンバーエンコーディングは、精度と順序が証明されていれば機能しますが、脆弱です。大規模環境では、複合キーと順序統計(Order statistics)をサポートする順序付きシャードを採用し、同一ミリ秒、再試行、修正された達成時刻に対するテストを実施します。

フォローアップ4:報酬発行後に不正防止システムがチャンピオンを取り消した場合はどうなりますか?

報酬を回収するかどうかを技術システム単体で決定することはできません。より高いscore_versionを持つ下方修正を受け入れ、元のイベント、証拠、承認者、時刻を保持し、元のFINALスナップショットを書き換えることなく、新しい修正マニフェストを生成します。報酬サービスは運用ポリシーを適用して報酬の凍結、回収、または繰り上げを行い、そのアクションをリーダーボードのバージョンに関連付けます。これにより、最初の授与と後の修正の両方に対する説明責任が維持されます。

フォローアップ5:台帳からの再構築がオンラインのリーダーボードと一致することをどのように証明しますか?

分離されたネームスペースで、同一の入力ハイウォーターマークまでイベントバージョンをリプレイします。プレイヤー数、スコアバケットごとのカウント、スコアの合計とハッシュ値、Top 100、1 + count(higher score)を使用した層化サンプリングされた多数のプレイヤー、および各シャードの決定論的チェックサムを比較します。両方の読み取りパスをシャドウイングし、スコア、順位、または周辺ウィンドウの差異をカウントします。すべての閾値をクリアした後にのみマニフェストを切り替えます。再構築ジョブが正常終了したこと自体は、その結果が正しいことの証明にはなりません。

公開情報ソース

関連する質問

関連面接ツール

システム設計の回答には「回答する」を使用

まず要件を明確にし、スケール、アーキテクチャ、コンポーネント選定、トレードオフの順に進めます。

ツールを見る