玩家贏一場加 1 分,即時顯示前 10 名與自己的名次。難點不在吞吐量,而在對數百萬持續變動的分數即時排名——關聯式資料庫的直接排序在這裡會崩潰。
📋 Requirements
- 功能:顯示前 10 名玩家、顯示特定使用者的排名;加分項是回傳目標使用者上下各 4 名玩家。
- 計分:贏一場 +1 分;每月開新錦標賽、開新排行榜;分數需即時反映,批次歷史結果不可接受。
- 規模:500 萬 DAU、2,500 萬 MAU,每人每天平均 10 場。
- 估算:平均 50 使用者/秒,尖峰抓 5 倍 → 250。得分 QPS 尖峰 ≈ 2,500;擷取前 10 名 QPS ≈ 50。
🧠 Core Ideas
TIP
核心是「用對數時間就能排序的資料結構取代關聯式資料庫的全表排序」。Redis sorted set 讓每個元素在插入/更新時自動定位到正確順序,新增與查找皆為 O(log n)。
分數必須在伺服器端設定:客戶端直接設分數易遭中間人攻擊,玩家可透過代理竄改。因此客戶端先呼叫遊戲服務確認勝利有效,再由遊戲服務呼叫排行榜服務更新分數。得分 API(POST /v1/scores)應為內部 API,只能由遊戲伺服器呼叫。
⚖️ Reference Design
為何關聯式資料庫直接排序不可行
- 每月一張表存
user_id與score;加分用UPDATE ... SET score=score+1。 - 找排名需
ORDER BY score DESC全表排序,或用巢狀COUNT(*)算比自己高分的人數。 - 資料到數百萬列時,排名操作要數十秒,達不到即時要求;且資料持續變動,連快取都不可行。加索引+
LIMIT只解決前 10 名,仍無法有效率地找出榜中特定使用者的排名。
Redis sorted set 解法
- Redis 是記憶體內的鍵值儲存;sorted set 內部由 hash table(user→score)加 skip list(score→user)實作,故排序操作是 O(log n)。
- 加分:
ZINCRBY leaderboard_feb_2021 1 'mary1934'(不存在則從 0 起算)。每月建新 sorted set,舊的移到歷史儲存。 - 前 10 名:
ZREVRANGE leaderboard_feb_2021 0 9 WITHSCORES(降序取最高分)。 - 特定使用者排名:
ZREVRANK。上下各 4 名:對排名 361 的玩家執行ZREVRANGE ... 357 365。
儲存、備份與擴展
- 每筆 24 字元 ID + 2 bytes 分數 ≈ 26 bytes;2,500 萬 MAU 最壞情況約 650 MB,尖峰 2,500 QPS,單一 Redis 伺服器綽綽有餘。
- 持久性顧慮:Redis 從磁碟重啟大型實例很慢,通常配唯讀副本,主實例失效時提升副本。
- 另需 MySQL 的 user 與 point 兩張表(後者含時間戳):供其他功能使用,也可在 Redis 故障時逐筆
ZINCRBY重建排行榜。 - 擴展到 5 億 DAU(65 GB、25 萬 QPS)需分片:偏好固定分區(依分數範圍切,可直接定位前 10 名與排名);雜湊分區(Redis cluster,
CRC16(key)%16384)需 scatter-gather 且無法直接算特定排名。
🔑 Takeaways
- 這題的取捨核心是 SQL vs NoSQL:關聯式資料庫不適合高讀取、持續變動的即時排名;Redis sorted set 的 O(log n) 是關鍵。
- 記住四個操作:
ZADD/ZINCRBY更新、ZREVRANGE取前段與相對區間、ZREVRANK取名次。 - 分數必須伺服器端設定;得分 API 為內部 API。是否加訊息佇列(Kafka)取決於分數是否被多個消費者(分析、推播)共用。
- 擴展偏好固定分區;NoSQL(DynamoDB + global secondary index + write sharding)為替代方案,但同樣難直接算特定排名,可改回傳百分位。
- 打破平手:用 Redis hash 記使用者得分的時間戳,較早達到者排較高。
No notes yet — jot your takeaways or Q&A here.