Case Study: Design a Real-time Gaming Leaderboard
“Leaderboard giống như bảng xếp hạng giải đấu thể thao: hàng triệu vận động viên, điểm thay đổi liên tục, ai cũng muốn biết thứ hạng của mình — ngay lập tức, không phải đợi 5 phút.”
Tags: system-design leaderboard redis sorted-set real-time gaming alex-xu-vol2 case-study Student: Hieu Source: Alex Xu — System Design Interview Volume 2, Chapter 10 Prerequisite: Tuan-01-Scale-From-Zero-To-Millions · Tuan-02-Back-of-the-envelope · Tuan-06-Cache-Strategy Lien quan: Tuan-09-Rate-Limiter · Tuan-10-Consistent-Hashing · Tuan-08-Message-Queue · Tuan-07-Database-Sharding-Replication
1. Context & Why — Tại sao Gaming Leaderboard quan trọng?
1.1 Analogy: Bảng xếp hạng giải đấu thể thao
Bạn tưởng tượng mình đang tổ chức một giải đấu thể thao quốc tế — giống như Olympic hoặc World Cup. Có hàng triệu vận động viên tham gia, điểm số thay đổi liên tục sau mỗi trận đấu, và khán giả trên toàn thế giới đều muốn biết:
- Top 10 ai? — Ai đang dẫn đầu bảng xếp hạng?
- Tôi đang ở đâu? — Thứ hạng của tôi là bao nhiêu trong số hàng triệu người?
- Ai ở quanh tôi? — Những người có thứ hạng gần tôi là ai?
- Cập nhật ngay! — Khi tôi ghi bàn, bảng xếp hạng phải cập nhật liên tục, không phải đợi đến cuối ngày
Đây chính xác là bài toán của gaming leaderboard. Trong game, mỗi khi người chơi kết thúc 1 trận, điểm số thay đổi và bảng xếp hạng phải cập nhật real-time. Với hàng triệu người chơi đồng thời, đây không phải bài toán đơn giản.
1.2 Gaming Leaderboard trong thực tế
Các hệ thống leaderboard nổi tiếng: Fortnite, League of Legends, PUBG, Clash Royale, Among Us, Candy Crush. Mỗi game có hàng chục triệu người chơi, và leaderboard là feature không thể thiếu.
| Thông số | Giá trị điển hình |
|---|---|
| DAU (Daily Active Users) | 5-50 triệu |
| Score updates/ngày | 25-250 triệu |
| Leaderboard views/ngày | 50-500 triệu |
| Latency yêu cầu | < 100ms cho mỗi thao tác |
| Freshness | Real-time hoặc near-real-time (< 5 giây) |
1.3 Tại sao đây là bài toán khó?
| Thách thức | Giải thích |
|---|---|
| Scale | 25 triệu score updates/ngày, 5 triệu DAU — không thể dùng brute-force |
| Real-time ranking | Khi 1 người thay đổi điểm, thứ hạng của hàng triệu người khác cũng thay đổi |
| Top K query | ”Top 10 ai?” phải trả lời trong < 100ms |
| User rank query | ”Tôi xếp thứ bao nhiêu trong 25 triệu người?” — câu hỏi này khó hơn nhiều |
| Tie-breaking | 2 người cùng điểm — ai xếp trước? |
| Monthly reset | Mỗi tháng reset leaderboard — nhưng phải lưu lịch sử |
| Multiple leaderboards | Per-game, per-region, per-friend-group — hàng nghìn leaderboard đồng thời |
1.4 Tại sao Backend Dev cần hiểu Leaderboard?
| Lý do | Giải thích |
|---|---|
| Mọi sản phẩm đều cần ranking | Không chỉ game — e-commerce (top sellers), social media (trending), education (student ranking), fitness (step count) |
| Redis Sorted Set là pattern cơ bản | Hiểu Sorted Set = hiểu được hàng chục bài toán ranking khác |
| Interview favorite | Leaderboard là bài interview phổ biến vì nó kiểm tra khả năng chọn đúng data structure |
| Trade-off điển hình | Real-time vs near-real-time, memory vs speed, single node vs sharded — mọi thứ đều có trade-off |
Aha Moment: Leaderboard nhìn có vẻ đơn giản — chỉ là sắp xếp điểm số. Nhưng khi scale lên hàng triệu người, “sắp xếp” trở thành bài toán O(n log n) mỗi lần update, và “tìm thứ hạng” trở thành O(n) — hoàn toàn không chấp nhận được. Đây là lý do ta cần data structure đặc biệt.
1.5 Leaderboard vs Sort — Sự khác biệt cơ bản
| Khía cạnh | Sort thông thường | Leaderboard system |
|---|---|---|
| Tần suất update | Một lần (batch) | Liên tục (real-time) |
| Tần suất query | Một lần | Hàng triệu lần/ngày |
| Dữ liệu | Static | Dynamic — thay đổi mỗi giây |
| Kích thước | Nhỏ (vạn phần tử) | Lớn (hàng triệu phần tử) |
| Yêu cầu | Sắp xếp 1 lần | Insert, update, rank query, range query — tất cả O(log N) |
| Giải pháp | Quick sort, merge sort | Redis Sorted Set (skip list + hash table) |
2. Deep Dive — Alex Xu 4-Step Framework
Step 1: Requirements — Hiểu và giới hạn bài toán
2.1.1 Functional Requirements
| Chức năng | Mô tả chi tiết |
|---|---|
| Display top 10 | Hiển thị bảng xếp hạng top 10 người chơi có điểm cao nhất |
| Show user rank | Người chơi xem được thứ hạng của mình trong toàn bộ leaderboard |
| Real-time update | Khi người chơi kết thúc trận đấu và điểm thay đổi, leaderboard cập nhật ngay |
| Score update | Hệ thống cập nhật điểm khi người chơi thắng/thua trận |
| Monthly reset | Leaderboard reset vào đầu mỗi tháng, bắt đầu lại từ 0 |
| Relative ranking | Hiển thị những người chơi có thứ hạng gần với mình (“users near me”) |
2.1.2 Non-Functional Requirements
| Yêu cầu | Mục tiêu | Lý do |
|---|---|---|
| Latency | P99 < 100ms cho mỗi thao tác | User experience — game player không chịu đợi |
| Throughput | 25 triệu score updates/ngày (~290 updates/sec avg, ~1,000/sec peak) | Game match results từ 5M DAU |
| Availability | 99.9% uptime | Leaderboard down = player mất động lực chơi |
| Scalability | 5 triệu DAU, 25 triệu scores/ngày | Scale theo số lượng player |
| Freshness | Real-time (< 1 giây) cho score update | Player muốn thấy kết quả ngay sau trận đấu |
| Consistency | Eventually consistent ok | Sai lệch vài giây chấp nhận được |
2.1.3 Clarifying Questions
Trong interview, Hieu nên hỏi những câu này trước khi thiết kế:
| Câu hỏi | Trả lời | Ghi chú |
|---|---|---|
| Score tính thế nào? | Mỗi trận thắng +1 điểm | Đơn giản, không có weighted score |
| Có bao nhiêu người chơi? | 5 triệu DAU, 25 triệu trận đấu/ngày | Mỗi người chơi trung bình 5 trận/ngày |
| Leaderboard reset chu kỳ nào? | Hàng tháng | Đầu tháng reset về 0 |
| Cần xem lịch sử leaderboard không? | Có — xem bảng xếp hạng tháng trước | Archive leaderboard cũ |
| Real-time hay near-real-time? | Real-time cho score update | Player thấy kết quả ngay |
| Có bao nhiêu loại leaderboard? | Global + per-region + per-friend | Nhiều leaderboard đồng thời |
| Tie-breaking rule? | Cùng điểm — ai đạt điểm trước xếp trước | Time-based tie-breaking |
2.1.4 Scale Summary
| Thông số | Giá trị |
|---|---|
| DAU | 5,000,000 |
| Trận đấu/ngày | 25,000,000 |
| Score updates/ngày | 25,000,000 |
| Leaderboard views/ngày | ~50,000,000 (mỗi người xem ~10 lần/ngày) |
| Tổng số người chơi có điểm | ~25,000,000 (1 tháng) |
| Reset cycle | Monthly |
Step 2: High-Level Design
2.2.1 Kiến trúc tổng quan
flowchart TB subgraph "Client Layer" PLAYER[Game Client<br/>Mobile / PC / Console] end subgraph "API Gateway" GW[API Gateway<br/>Auth, Rate Limiting, Routing] end subgraph "Game Services" GS[Game Service<br/>Match Result Processing] end subgraph "Leaderboard System" LS[Leaderboard Service<br/>Score Update & Query] REDIS[(Redis Cluster<br/>Sorted Sets)] end subgraph "Data Store" DB[(MySQL / PostgreSQL<br/>Player Profiles<br/>Match History)] MQ[Message Queue<br/>Kafka] end PLAYER --> GW GW --> GS GS -->|"Score Update"| LS GS -->|"Match Result"| DB GS -->|"Event"| MQ LS <-->|"ZADD, ZREVRANGE,<br/>ZREVRANK, ZSCORE"| REDIS LS -->|"Leaderboard Data"| GW GW -->|"Top 10, User Rank"| PLAYER MQ -->|"Analytics, Archive"| DB style REDIS fill:#e53935,color:#fff style LS fill:#1e88e5,color:#fff style GS fill:#43a047,color:#fff
2.2.2 Luồng xử lý điểm số (Score Update Flow)
sequenceDiagram participant Player participant Gateway as API Gateway participant GS as Game Service participant LS as Leaderboard Service participant Redis as Redis Sorted Set participant DB as Database Player->>Gateway: Match Completed (result: WIN) Gateway->>GS: Process Match Result GS->>GS: Validate Match Result GS->>DB: Save Match History GS->>LS: Update Score (user_id, +1) LS->>Redis: ZINCRBY leaderboard:2026-03 1 user_id Redis-->>LS: New Score: 42 LS-->>GS: Score Updated GS-->>Gateway: Match Result + New Rank Gateway-->>Player: "You won! New score: 42, Rank: #1,523"
2.2.3 Luồng truy vấn leaderboard (Leaderboard Query Flow)
sequenceDiagram participant Player participant Gateway as API Gateway participant LS as Leaderboard Service participant Redis as Redis Sorted Set participant DB as Database Player->>Gateway: Get Top 10 Gateway->>LS: getTopK(10) LS->>Redis: ZREVRANGE leaderboard:2026-03 0 9 WITHSCORES Redis-->>LS: [(user_A, 150), (user_B, 148), ..., (user_J, 120)] LS->>DB: Get player profiles for top 10 user IDs DB-->>LS: [{name: "ProGamer", avatar: "..."}, ...] LS-->>Gateway: Top 10 with profiles Gateway-->>Player: Display Leaderboard Player->>Gateway: Get My Rank Gateway->>LS: getUserRank(user_id) LS->>Redis: ZREVRANK leaderboard:2026-03 user_id Redis-->>LS: Rank: 1522 (0-indexed) LS->>Redis: ZSCORE leaderboard:2026-03 user_id Redis-->>LS: Score: 42 LS-->>Gateway: {rank: 1523, score: 42} Gateway-->>Player: "Your rank: #1,523 | Score: 42"
2.2.4 Các component chính và vai trò
| Component | Vai trò | Đặc điểm |
|---|---|---|
| Game Client | Gửi kết quả trận đấu, hiển thị leaderboard | Mobile, PC, Console — đa nền tảng |
| API Gateway | Xác thực, rate limiting, routing | Ngăn chặn abuse, phân phối tải |
| Game Service | Xử lý kết quả trận đấu, tính điểm | Business logic, validation |
| Leaderboard Service | Cập nhật điểm và truy vấn thứ hạng | Interface giữa business logic và Redis |
| Redis Sorted Set | Lưu trữ và sắp xếp điểm số | O(log N) cho mọi thao tác — trái tim của hệ thống |
| Database (MySQL/PostgreSQL) | Lưu player profile, match history | Persistent storage, không dùng cho real-time ranking |
| Message Queue (Kafka) | Event streaming cho analytics, archive | Async processing, decouple services |
Key Insight: Redis Sorted Set là trái tim của hệ thống. Mọi quyết định thiết kế xoay quanh việc tối ưu hóa cách sử dụng Sorted Set. Database chỉ lưu dữ liệu phụ (profile, history) — không bao giờ dùng database cho ranking query.
Step 3: Deep Dive — Chi tiết từng component
2.3.1 Naive Approach — Tại sao SQL không dùng được?
Trước khi đi vào giải pháp, hãy hiểu tại sao cách tiếp cận “đơn giản” không hoạt động.
Cách đơn giản nhất: Lưu score trong SQL table và query.
| Column | Type |
|---|---|
| user_id | BIGINT (PK) |
| score | INT |
| updated_at | TIMESTAMP |
Query lấy thứ hạng của 1 user:
SELECT COUNT(*) FROM leaderboard WHERE score > (SELECT score FROM leaderboard WHERE user_id = ?)
Vấn đề:
| Vấn đề | Giải thích |
|---|---|
| O(n) per query | Mỗi lần hỏi thứ hạng, database phải scan toàn bộ bảng để đếm có bao nhiêu người điểm cao hơn |
| 25 triệu rows | Với 25 triệu người chơi, mỗi query mất vài giây |
| 50 triệu queries/ngày | 50 triệu leaderboard views × O(n) query = database chết |
| Lock contention | Score update (write) và rank query (read) đều cần truy cập cùng bảng — deadlock, slow |
| No real-time | Index update sau mỗi INSERT/UPDATE mất thời gian, query result không real-time |
Thêm index có giúp được không?
Index trên score giúp ORDER BY score DESC LIMIT 10 (top K) chạy nhanh — O(log N). Nhưng query “rank của 1 user” vẫn là COUNT(*) — database phải scan toàn bộ index để đếm. Index B-tree không hỗ trợ “đếm số phần tử lớn hơn X” một cách hiệu quả.
Aha Moment: SQL B-tree index được thiết kế cho point query (tìm 1 giá trị) và range scan (lấy 1 khoảng). Nó không được thiết kế cho rank query (đếm số phần tử > X). Đây là lý do cần data structure đặc biệt như skip list.
Ngưỡng giới hạn của SQL approach:
| Kích thước | SQL Performance | Chấp nhận được? |
|---|---|---|
| 10,000 users | ~5ms | Có |
| 100,000 users | ~50ms | Tạm ổn |
| 1,000,000 users | ~500ms | Chậm |
| 25,000,000 users | ~5-10 giây | KHÔNG |
Kết luận: SQL chỉ hoạt động tốt với leaderboard nhỏ (< 100K users). Với 25M users, cần giải pháp khác.
2.3.2 Redis Sorted Set — Giải pháp hoàn hảo
Redis Sorted Set là gì?
Redis Sorted Set là data structure lưu trữ các cặp (member, score), tự động sắp xếp theo score. Mỗi member là duy nhất (giống Set), nhưng mỗi member có 1 giá trị score đi kèm.
4 operations cơ bản cho leaderboard:
| Operation | Mô tả | Complexity | Leaderboard Use Case |
|---|---|---|---|
| ZADD | Thêm hoặc cập nhật member với score | O(log N) | Cập nhật điểm người chơi |
| ZINCRBY | Tăng score của member thêm 1 giá trị | O(log N) | Cộng điểm sau mỗi trận thắng |
| ZREVRANGE | Lấy top K members (score giảm dần) | O(log N + K) | Hiển thị top 10 leaderboard |
| ZREVRANK | Lấy thứ hạng của 1 member (0-indexed, score giảm dần) | O(log N) | “Tôi xếp thứ bao nhiêu?” |
| ZSCORE | Lấy score của 1 member | O(1) | Hiển thị điểm hiện tại của người chơi |
| ZREVRANGEBYSCORE | Lấy members trong khoảng score | O(log N + K) | Tìm người chơi có điểm trong khoảng |
| ZCARD | Đếm tổng số members | O(1) | Tổng số người chơi trên leaderboard |
| ZREM | Xóa member | O(log N) | Xóa người chơi bị ban |
Tại sao Redis Sorted Set hoàn hảo cho leaderboard?
| Đặc điểm | Giải thích |
|---|---|
| O(log N) cho mọi thao tác | Với 25M members, log(25M) ~ 24 bước. Siêu nhanh. |
| In-memory | Không có disk I/O, mọi thao tác mất vài microseconds |
| Atomic operations | ZADD, ZINCRBY là atomic — không cần lock, không có race condition |
| Built-in ranking | ZREVRANK trả về thứ hạng ngay — không cần tính toán thêm |
| Range query | ZREVRANGE lấy top K hiệu quả — O(log N + K) |
| Single data structure | 1 Sorted Set giải quyết 100% các use case của leaderboard |
| Persistence | RDB snapshot + AOF log đảm bảo không mất dữ liệu khi restart |
Aha Moment: Redis Sorted Set không phải là “hack” hay “workaround”. Nó được thiết kế chính xác cho bài toán này. Mỗi operation bạn cần (add score, get rank, get top K) đều có sẵn và đều O(log N). Đây là ví dụ hoàn hảo của việc chọn đúng data structure giải quyết bài toán.
So sánh SQL vs Redis Sorted Set:
| Thao tác | SQL (25M rows) | Redis Sorted Set (25M members) |
|---|---|---|
| Update score | ~10ms (UPDATE + index rebuild) | ~0.01ms (ZADD/ZINCRBY) |
| Get top 10 | ~5ms (ORDER BY LIMIT) | ~0.01ms (ZREVRANGE) |
| Get user rank | ~5,000ms (COUNT WHERE) | ~0.01ms (ZREVRANK) |
| Get users near me | ~5,000ms (complex query) | ~0.01ms (ZREVRANGE with offset) |
Chênh lệch: 100,000 lần cho rank query. Đây không phải tối ưu — đây là sự khác biệt giữa được và không được.
2.3.3 Redis Sorted Set Internals — Skip List + Hash Table
Bên trong Redis Sorted Set có gì?
Redis Sorted Set sử dụng 2 data structure kết hợp:
| Data Structure | Vai trò | Operations |
|---|---|---|
| Skip List | Lưu members đã sắp xếp theo score | ZREVRANGE, ZREVRANK, range queries |
| Hash Table | Map member → score (O(1) lookup) | ZSCORE, kiểm tra member tồn tại |
Hai data structure này đồng bộ với nhau — khi ZADD được gọi, Redis cập nhật cả skip list và hash table.
Skip List là gì?
Skip list là một cấu trúc dữ liệu xác suất (probabilistic) được phát minh bởi William Pugh năm 1990. Nó giống như linked list nhiều tầng (multi-level linked list):
- Level 0 (bottom): Chứa TẤT CẢ các phần tử, sắp xếp theo score
- Level 1: Chứa ~50% phần tử (ngẫu nhiên)
- Level 2: Chứa ~25% phần tử
- Level k: Chứa ~1/2^k phần tử
- Top level: Chỉ có vài phần tử
Ví dụ Skip List với scores [10, 20, 30, 40, 50, 60, 70, 80]:
Level 3: HEAD ─────────────────────────────── 40 ─────────────────────────────── NIL
Level 2: HEAD ──────────── 20 ──────────────── 40 ──────────── 60 ──────────── NIL
Level 1: HEAD ──── 10 ──── 20 ──── 30 ──── 40 ──── 50 ──── 60 ──── 70 ──── NIL
Level 0: HEAD ── 10 ── 20 ── 30 ── 40 ── 50 ── 60 ── 70 ── 80 ── NIL
Tìm kiếm trong skip list (ví dụ: tìm 50):
- Bắt đầu từ Level 3, HEAD → 40 (40 < 50, đi tiếp) → NIL (quá lớn, xuống level)
- Level 2: 40 → 60 (60 > 50, xuống level)
- Level 1: 40 → 50 (TÌM THẤY!)
Số bước: 3-4 bước thay vì 5 bước (linear scan). Với 25M phần tử, skip list chỉ cần ~24 bước (log2(25M)).
Tại sao skip list mà không phải balanced BST (Red-Black Tree, AVL)?
| Tiêu chí | Skip List | Balanced BST (Red-Black Tree) |
|---|---|---|
| Implementation | Đơn giản hơn nhiều | Phức tạp — rotation, recoloring |
| Concurrent access | Lock-free algorithms dễ hơn | Lock toàn bộ cây khi rebalance |
| Range query | Duyệt linked list — cache-friendly | In-order traversal — pointer chasing |
| Memory overhead | ~1.33 pointers/node (average) | 3 pointers/node (left, right, parent) + color |
| Insert/Delete | Update pointers ở các level — local operation | Có thể trigger rebalance cascade |
| Complexity | O(log N) expected | O(log N) worst-case |
| Debug | Dễ visualize, dễ debug | Khó debug rotation/recoloring |
Aha Moment: Skip list là một trong những data structure “elegant” nhất trong computer science. Nó đạt được O(log N) performance chỉ bằng cách dùng random — không cần rotation, không cần rebalance, không cần phức tạp. Redis chọn skip list vì nó đơn giản, nhanh, và thân thiện với concurrent access. Antirez (tác giả Redis) đã nói: “Skip lists are simpler to implement, and allow me to do things I could not do with balanced trees.”
Redis Skip List đặc biệt ở chỗ nào?
Redis skip list có 2 đặc điểm khác với skip list chuẩn:
- Backward pointer: Mỗi node có pointer ngược về node trước — cho phép duyệt ngược (ZREVRANGE)
- Span field: Mỗi forward pointer lưu “khoảng cách” đến node tiếp theo ở level đó — cho phép tính rank mà không cần đếm
Span field là bí quyết để ZREVRANK đạt O(log N). Khi duyệt từ head đến target node, cộng tất cả span values = rank của node đó. Không cần đếm từng phần tử.
2.3.4 Handling Ties — Xử lý trùng điểm
Vấn đề: 2 người chơi cùng 42 điểm — ai xếp trước?
Rule: Người đạt 42 điểm trước (thời gian sớm hơn) xếp trước. Đây là quy tắc công bằng — ai đạt thành tích trước thì được ưu tiên.
Giải pháp: Composite Score
Thay vì lưu score thông thường, ta tạo composite score kết hợp điểm số và thời gian:
Trong đó:
score: Điểm thực tế của người chơi (ví dụ: 42)MAX_TIMESTAMP: Giá trị timestamp lớn nhất có thể (ví dụ: 9,999,999,999,999 — 13 chữ số)actual_timestamp: Thời điểm người chơi đạt được điểm này (Unix timestamp milliseconds)
Ví dụ cụ thể:
| Player | Score | Timestamp (ms) | Composite Score |
|---|---|---|---|
| Alice | 42 | 1,710,700,800,000 | 42 × 10^13 + (9,999,999,999,999 - 1,710,700,800,000) = 428,289,299,199,999 |
| Bob | 42 | 1,710,700,900,000 | 42 × 10^13 + (9,999,999,999,999 - 1,710,700,900,000) = 428,289,299,099,999 |
Alice có composite score lớn hơn Bob (vì Alice đạt điểm trước — timestamp nhỏ hơn → MAX_TIMESTAMP - timestamp lớn hơn). Redis sắp xếp score giảm dần → Alice xếp trước Bob.
Tại sao dùng MAX_TIMESTAMP - actual_timestamp?
| Cách | Kết quả | Đúng/Sai |
|---|---|---|
score * 10^13 + timestamp | Timestamp lớn hơn = score lớn hơn → người đạt điểm sau xếp trước | SAI — ngược logic |
score * 10^13 + (MAX - timestamp) | Timestamp nhỏ hơn = bonus lớn hơn → người đạt điểm trước xếp trước | ĐÚNG |
Tại sao 10^13?
Unix timestamp milliseconds hiện tại có 13 chữ số (ví dụ: 1,710,700,800,000). Ta cần 10^13 để đảm bảo phần score và phần timestamp không chồng lấn nhau.
Aha Moment: Composite score là kỹ thuật packing 2 giá trị vào 1 number. Đây là pattern phổ biến trong system design — khi data structure chỉ hỗ trợ 1 trường sắp xếp, ta “nhét” nhiều trường vào 1 bằng cách sử dụng hệ số. Tương tự như cách mà composite key hoạt động trong database.
Lưu ý về độ chính xác:
Redis lưu score dạng double-precision floating point (64-bit IEEE 754). Double có ~15-17 chữ số thập phân chính xác. Composite score của ta có tối đa:
15 chữ số nằm trong giới hạn chính xác của double. Nhưng nếu score có thể lên hàng triệu (6 chữ số), tổng là 19 chữ số — vượt quá giới hạn của double. Trong trường hợp đó, cần dùng cách khác (ví dụ: 2 Sorted Sets, hoặc sử dụng string encoding).
2.3.5 Relative Ranking — “Users Near Me”
Use case: Người chơi muốn thấy những người có thứ hạng gần mình — ví dụ: người xếp trước 5 bậc và sau 5 bậc.
Giải pháp: Sử dụng ZREVRANGE với offset.
Flow:
- Lấy rank của user:
ZREVRANK leaderboard:2026-03 user_id→ rank = 1522 - Lấy users xung quanh:
ZREVRANGE leaderboard:2026-03 1517 1527 WITHSCORES(rank 1518 đến 1528)
Kết quả:
| Rank | Player | Score |
|---|---|---|
| #1518 | player_A | 43 |
| #1519 | player_B | 43 |
| #1520 | player_C | 42 |
| #1521 | player_D | 42 |
| #1522 | player_E | 42 |
| #1523 | YOU | 42 |
| #1524 | player_F | 42 |
| #1525 | player_G | 41 |
| #1526 | player_H | 41 |
| #1527 | player_I | 41 |
| #1528 | player_J | 40 |
Complexity: O(log N) cho ZREVRANK + O(log N + K) cho ZREVRANGE = O(log N + K) tổng cộng. Với 25M users và K = 11: ~24 + 11 = 35 operations. Siêu nhanh.
UX considerations:
- Hiển thị avatar, username, và score của mỗi người — cần query thêm database cho profile info
- Highlight dòng của user hiện tại
- Cho phép tap vào profile người khác để xem chi tiết
- Hiển thị trend (lên bậc, xuống bậc so với ngày hôm qua)
2.3.6 Sharded Leaderboard — Khi 1 Redis không đủ
Khi nào cần shard?
| Tình huống | Cần shard? |
|---|---|
| 25M users, ~1.5GB memory | Không — 1 Redis node đủ sức (Redis hỗ trợ hàng chục GB) |
| 500M users, ~30GB memory | Có thể — memory vẫn ok nhưng throughput có thể là bottleneck |
| 5B users, ~300GB memory | Chắc chắn — vượt quá memory của 1 node |
| Write throughput > 100K/sec | Có thể — 1 Redis node có giới hạn write throughput |
Quan trọng: Với 25M users và 290 writes/sec trung bình, 1 Redis node hoàn toàn đủ. Sharding chỉ cần khi scale vượt quá giới hạn của single node. Trong interview, hãy nói rõ điều này trước khi thảo luận sharding.
Cách shard: Score Range Partitioning
Chia leaderboard thành N shard theo khoảng điểm (score range):
| Shard | Score Range | Số lượng users (ước tính) |
|---|---|---|
| Shard 0 | 0 - 99 | 10M (majority là low scores) |
| Shard 1 | 100 - 199 | 8M |
| Shard 2 | 200 - 299 | 4M |
| Shard 3 | 300 - 399 | 2M |
| Shard 4 | 400+ | 1M |
Vấn đề với score range partitioning: Data không đều — shard 0 (low scores) có nhiều user hơn shard 4 (high scores). Đây là data skew.
Cách khác: Hash Partitioning
Shard theo hash(user_id) % N. Data đều nhưng không thể lấy top K global — vì top K có thể nằm ở bất kỳ shard nào.
Giải pháp cho hash partitioning — Merge Top K:
flowchart TB subgraph "Client Request" REQ["Get Global Top 10"] end subgraph "Leaderboard Service" LS[Scatter-Gather<br/>Coordinator] end subgraph "Redis Shards" S1["Shard 1<br/>ZREVRANGE 0 9"] S2["Shard 2<br/>ZREVRANGE 0 9"] S3["Shard 3<br/>ZREVRANGE 0 9"] S4["Shard 4<br/>ZREVRANGE 0 9"] end subgraph "Merge" MRG["Merge Sort<br/>Top 10 from 4×10 = 40 results"] end REQ --> LS LS -->|"Parallel query"| S1 & S2 & S3 & S4 S1 & S2 & S3 & S4 --> MRG MRG -->|"Global Top 10"| LS LS --> REQ style MRG fill:#e53935,color:#fff style LS fill:#1e88e5,color:#fff
Scatter-Gather pattern:
- Gửi
ZREVRANGE 0 9đến tất cả N shards (song song) - Mỗi shard trả về top 10 của nó
- Coordinator merge N × 10 results và lấy top 10 global
Complexity: O(N × log M + N × K × log(N × K)) trong đó N = số shards, M = số members/shard, K = số kết quả mong muốn.
Vấn đề của sharded leaderboard cho rank query:
“User X xếp thứ bao nhiêu globally?” — đây là bài toán khó nhất của sharding.
| Approach | Mô tả | Độ chính xác |
|---|---|---|
| Exact rank | Query ZREVRANK trên tất cả shards, cộng rank lại | Chính xác 100% nhưng chậm (query N shards) |
| Approximate rank | Dùng sampling hoặc statistics | Nhanh nhưng không chính xác |
| Score range partition | Rank = tổng số users ở tất cả shard điểm cao hơn + rank trong shard hiện tại | Chính xác, nhưng cần lưu cardinality mỗi shard |
Exact rank với score range partitioning:
Ví dụ: User có score 250 nằm ở Shard 2 (200-299). Rank của user:
- ZCARD(Shard 3) = 2,000,000 (users có 300-399 điểm)
- ZCARD(Shard 4) = 1,000,000 (users có 400+ điểm)
- ZREVRANK(Shard 2, user) = 500 (rank 500 trong Shard 2)
- Global rank = 2,000,000 + 1,000,000 + 500 = 3,000,500
Aha Moment: Sharding leaderboard khó hơn rất nhiều so với single node. Top K query cần scatter-gather, rank query cần cộng rank từ nhiều shard, và data skew là vấn đề thường trực. Đây là lý do tại sao interview thường bắt đầu với single node và chỉ chuyển sang sharding khi interviewer hỏi “what if you need to scale beyond single node?“. Luôn ưu tiên single node nếu có thể.
2.3.7 Real-time vs Near-real-time — Chọn đúng cho đúng bài toán
Hai cách tiếp cận:
| Cách | Mô tả | Khi nào dùng |
|---|---|---|
| Real-time (Direct Write) | Game Service gọi trực tiếp Redis ZADD/ZINCRBY | Write throughput thấp-trung bình (< 10K/sec), cần real-time update |
| Near-real-time (Batch via MQ) | Game Service gửi event vào Kafka, consumer batch write vào Redis | Write throughput cao (> 10K/sec), chấp nhận delay 1-5 giây |
Real-time approach:
flowchart LR GS[Game Service] -->|"ZINCRBY"| REDIS[(Redis<br/>Sorted Set)] style REDIS fill:#e53935,color:#fff
- Ưu điểm: Điểm cập nhật ngay, người chơi thấy kết quả liên tục
- Nhược điểm: Redis chịu toàn bộ write load trực tiếp; nếu burst traffic, có thể quá tải
Near-real-time approach:
flowchart LR GS[Game Service] -->|"Produce"| KAFKA[Kafka<br/>Message Queue] KAFKA -->|"Consume batch"| CONSUMER[Score Consumer<br/>Batch ZADD] CONSUMER -->|"Batch ZINCRBY"| REDIS[(Redis<br/>Sorted Set)] style KAFKA fill:#43a047,color:#fff style REDIS fill:#e53935,color:#fff
- Ưu điểm: Kafka làm buffer, Redis không bị burst; consumer có thể batch nhiều update thành 1 pipeline command
- Nhược điểm: Delay 1-5 giây giữa khi trận đấu kết thúc và khi leaderboard cập nhật
Khi nào chọn cách nào?
| Tiêu chí | Real-time | Near-real-time |
|---|---|---|
| Write QPS | < 10,000/sec | > 10,000/sec |
| Latency requirement | < 1 giây | 1-5 giây ok |
| Burst traffic | Không có hoặc ít | Có burst lớn (ví dụ: event đặc biệt) |
| Complexity | Đơn giản hơn | Phức tạp hơn (thêm Kafka, consumer) |
Cho bài toán này (290 avg, 1000 peak): Real-time là đủ. 1,000 writes/sec hoàn toàn trong khả năng của 1 Redis node (Redis có thể xử lý 100K+ commands/sec). Chỉ cần near-real-time khi scale lên 100K+ writes/sec.
Key Insight: Đừng over-engineer. 1,000 writes/sec là con số nhỏ với Redis. Thêm Kafka vào chỉ tăng complexity mà không mang lại lợi ích rõ ràng. KISS — Keep It Simple, Stupid.
2.3.8 Monthly Reset — Reset leaderboard hàng tháng
Strategy: Sorted Set per month
Mỗi tháng, tạo 1 Sorted Set mới với key có tên tháng:
| Tháng | Redis Key |
|---|---|
| Tháng 1/2026 | leaderboard:2026-01 |
| Tháng 2/2026 | leaderboard:2026-02 |
| Tháng 3/2026 | leaderboard:2026-03 |
Flow đầu tháng mới:
- Khi tháng mới bắt đầu (ví dụ: 2026-04-01 00:00:00 UTC), Leaderboard Service tự động chuyển sang key
leaderboard:2026-04 - Key
leaderboard:2026-03vẫn tồn tại — người chơi vẫn xem được bảng xếp hạng tháng trước - Đặt TTL cho key cũ:
EXPIRE leaderboard:2026-01 7776000(90 ngày = 3 tháng lưu trữ) - Sau 3 tháng, Redis tự động xóa key cũ — không cần manual cleanup
Automation workflow:
| Bước | Thao tác | Thời điểm |
|---|---|---|
| 1 | Leaderboard Service detect tháng mới (từ config hoặc cron) | 00:00:00 UTC ngày 1 |
| 2 | Chuyển current_leaderboard pointer sang key mới | 00:00:01 UTC |
| 3 | Archive key cũ vào persistent storage (S3/database) nếu cần | 00:01:00 UTC (async) |
| 4 | Đặt TTL cho key cũ trên Redis | 00:01:01 UTC |
| 5 | Gửi notification cho players: “New season started!“ | 00:05:00 UTC |
Edge case — trận đấu bắt đầu tháng cũ, kết thúc tháng mới:
| Tình huống | Giải pháp |
|---|---|
| Trận bắt đầu 23:58 tháng 3, kết thúc 00:02 tháng 4 | Dùng match_start_time để quyết định ghi vào leaderboard nào |
Hoặc: dùng match_end_time | Ghi vào leaderboard của tháng mà trận kết thúc |
| Race condition khi chuyển tháng | Dùng atomic flag hoặc distributed lock để đảm bảo chỉ chuyển 1 lần |
Aha Moment: Strategy “1 Sorted Set per month” đơn giản nhưng hiệu quả. Không cần
DEL(xóa toàn bộ key — O(N), block Redis). Không cầnZREMRANGEBYSCORE(xóa từng phần tử). Chỉ cần tạo key mới và để key cũ tự hết hạn. Đây là pattern immutable data — thay vì sửa dữ liệu cũ, tạo dữ liệu mới.
2.3.9 Multiple Leaderboards — Per-game, Per-region, Per-friend
Use cases:
| Loại leaderboard | Mô tả | Ví dụ key |
|---|---|---|
| Global | Toàn bộ người chơi | lb:global:2026-03 |
| Per-game | Theo từng game mode | lb:mode:ranked:2026-03 |
| Per-region | Theo vùng địa lý | lb:region:asia:2026-03 |
| Per-friend | Chỉ trong danh sách bạn bè | lb:friends:{user_id}:2026-03 |
| Per-clan | Theo clan/guild | lb:clan:{clan_id}:2026-03 |
| Weekly | Reset hàng tuần | lb:weekly:2026-W12 |
| All-time | Không bao giờ reset | lb:alltime |
Namespaced keys:
Pattern key: lb:{scope}:{identifier}:{period}
| Phần | Ý nghĩa | Ví dụ |
|---|---|---|
lb | Prefix cho leaderboard | Cố định |
{scope} | Loại leaderboard | global, region, friends, clan |
{identifier} | ID cụ thể | asia, user_123, clan_456 |
{period} | Kỳ reset | 2026-03, 2026-W12, alltime |
Friend leaderboard — đặc biệt khó:
| Thách thức | Giải thích |
|---|---|
| Mỗi user có danh sách bạn bè khác nhau | Không thể dùng 1 Sorted Set chung |
| Số lượng leaderboard = số lượng users | 5M users = 5M friend leaderboards? KHÔNG |
| Bạn bè thay đổi (add/remove friend) | Leaderboard phải cập nhật theo |
Giải pháp cho friend leaderboard:
| Approach | Mô tả | Ưu/Nhược |
|---|---|---|
| On-demand computation | Khi user xem friend leaderboard, lấy score của tất cả friends từ global leaderboard, sort client-side | Đơn giản, không tốn memory. Nhưng chậm nếu có 500+ friends |
| Fan-out on write | Khi user update score, ghi vào friend leaderboard của tất cả bạn bè | Nhanh khi đọc, nhưng write amplification lớn (500 friends = 500 writes) |
| Hybrid | Cache friend leaderboard với short TTL (30 giây), invalidate khi có update | Cân bằng giữa read speed và write cost |
Recommendation: Với game thông thường (100-200 friends), on-demand computation là đủ tốt. Lấy 200 scores từ Redis (200 × ZSCORE) mất ~2ms. Sort 200 items mất ~0.001ms. Tổng: ~2ms — hoàn toàn chấp nhận được.
2.3.10 Pagination — Phân trang leaderboard
Vấn đề: Leaderboard có 25M users. Client chỉ hiển thị 20 users/trang. User muốn cuộn xuống xem nhiều hơn.
Giải pháp: Cursor-based pagination dùng ZREVRANGE với LIMIT
| Trang | Redis Command | Giải thích |
|---|---|---|
| Trang 1 (rank 1-20) | ZREVRANGE lb:2026-03 0 19 WITHSCORES | Lấy 20 users đầu tiên |
| Trang 2 (rank 21-40) | ZREVRANGE lb:2026-03 20 39 WITHSCORES | Lấy 20 users tiếp theo |
| Trang N | ZREVRANGE lb:2026-03 (N-1)*20 N*20-1 WITHSCORES | Lấy trang thứ N |
Complexity: O(log N + K) cho mỗi trang, trong đó K = page size (20). Fast cho bất kỳ trang nào — kể cả trang 1,000,000.
Cursor-based vs Offset-based:
| Cách | Mô tả | Vấn đề |
|---|---|---|
| Offset-based | ZREVRANGE ... start stop | Nếu leaderboard thay đổi giữa 2 request, user có thể thấy duplicate hoặc miss entries |
| Score-based cursor | ”Lấy 20 users có score < last_score_seen” | Chính xác hơn, nhưng khó xử lý ties (nhiều người cùng score) |
| Rank-based cursor | ”Lấy rank 21-40” | Giống offset-based, có thể shift khi data thay đổi |
Thực tế: Offset-based pagination (ZREVRANGE start stop) là đủ tốt cho leaderboard. Lý do:
- Leaderboard thay đổi chậm (vài giây giữa các update)
- User hiếm khi cuộn qua trang 10 (~rank 200)
- Duplicate/miss 1-2 entries không ảnh hưởng trải nghiệm game
Key Insight: Don’t over-engineer pagination. ZREVRANGE với start/stop là cách đơn giản nhất và đủ tốt cho 99% use cases. Chỉ cần cursor phức tạp khi leaderboard thay đổi cực kỳ nhanh (hàng nghìn updates/giây) và user cuộn sâu (trang 1000+).
2.3.11 Caching Top K — Giảm tải cho Redis
Vấn đề: Top 10 leaderboard là hot data — hàng triệu người xem mỗi phút. Dù Redis nhanh, nhưng hàng triệu request/phút cho cùng 1 query vẫn tạo load không cần thiết.
Giải pháp: Cache top K với short TTL
| Tầng | Dữ liệu | TTL | Người dùng |
|---|---|---|---|
| Application cache (local) | Top 10 | 5 giây | Giảm load xuống Redis |
| Redis Sorted Set | Toàn bộ 25M users | Không TTL (persistent) | Source of truth |
Flow với cache:
- Client request “Get Top 10”
- Leaderboard Service kiểm tra local cache
- Nếu cache hit (< 5 giây) → trả về ngay
- Nếu cache miss → query Redis
ZREVRANGE ... 0 9 WITHSCORES→ lưu vào local cache với TTL 5s → trả về
Tại sao TTL 5 giây?
| TTL | Freshness | Cache hit rate | Load giảm |
|---|---|---|---|
| 1 giây | Gần như real-time | ~50% | ~50% |
| 5 giây | Chấp nhận được cho game | ~90% | ~90% |
| 30 giây | Hơi cũ | ~98% | ~98% |
| 5 phút | Cũ | ~99.9% | ~99.9% |
5 giây là sweet spot: giảm 90% load trong khi data chỉ cũ tối đa 5 giây — hoàn toàn chấp nhận được cho game leaderboard.
Cache gì ngoài top 10?
| Data | Cache | TTL | Lý do |
|---|---|---|---|
| Top 10 | Có | 5s | Hot data — ai cũng xem |
| Top 100 | Có | 10s | Phổ biến, nhiều người cuộn xuống |
| User rank (individual) | Có | 3s | Mỗi user chỉ care rank của mình |
| Friend leaderboard | Có | 30s | Ít thay đổi, ít người xem liên tục |
Aha Moment: Cache ở đây không phải để Redis nhanh hơn — Redis đã đủ nhanh (0.01ms/query). Cache là để giảm số lượng request đến Redis, ngăn Redis bị overload khi có hàng triệu concurrent users. Đây là sự khác biệt giữa “caching for speed” và “caching for load reduction”.
3. Estimation — Ước lượng hệ thống
3.1 Score Update Throughput
Assumptions:
| Thông số | Giá trị | Giải thích |
|---|---|---|
| DAU | 5,000,000 | 5 triệu người chơi hoạt động mỗi ngày |
| Trận đấu/người/ngày | 5 | Trung bình mỗi người chơi 5 trận |
| Tổng trận đấu/ngày | 25,000,000 | 5M × 5 |
| Active hours/ngày | 24 giờ | Game online 24/7 |
| Peak/Average ratio | 3.5x | Giờ cao điểm (tối) |
Redis có thể xử lý 100,000+ commands/sec trên 1 node. 5,000 writes/sec chỉ chiếm 5% capacity của Redis. Single node hoàn toàn đủ.
3.2 Leaderboard Read QPS
Assumptions:
| Thông số | Giá trị | Giải thích |
|---|---|---|
| Leaderboard views/người/ngày | 10 | Xem top 10, xem rank, xem friends |
| Tổng views/ngày | 50,000,000 | 5M × 10 |
| Read/Write ratio | 2:1 | Xem nhiều hơn cập nhật |
3,042 commands/sec là ~3% capacity của Redis (100K+/sec). Single node chắc chắn đủ.
3.3 Memory Estimation
Redis Sorted Set memory per entry:
| Thành phần | Kích thước | Giải thích |
|---|---|---|
| Member (user_id) | ~20 bytes | String “user_12345678” |
| Score | 8 bytes | Double-precision float |
| Skip list node | ~40 bytes | Pointers (forward, backward, span) — trung bình ~2.67 levels |
| Hash table entry | ~56 bytes | Key + value + metadata của Redis dict |
| Redis overhead | ~16 bytes | Object header, encoding |
Lưu ý: Đây là ước tính. Thực tế có thể dao động 100-200 bytes/entry tùy vào độ dài của member string và số levels trong skip list.
Nhưng ta có nhiều leaderboards:
| Leaderboard | Số entries | Memory |
|---|---|---|
| Global tháng hiện tại | 25,000,000 | 3.5 GB |
| Global tháng trước (archive) | 25,000,000 | 3.5 GB |
| Per-region (5 regions) | 5 × 5,000,000 | 3.5 GB |
| Per-game-mode (3 modes) | 3 × 25,000,000 | 10.5 GB |
| Weekly | 10,000,000 | 1.4 GB |
Redis server với 64GB RAM có thể handle thoải mái. Không cần cluster cho memory — chỉ cần cluster cho high availability (replication).
3.4 Network Bandwidth Estimation
Write bandwidth:
Read bandwidth (mỗi response trả về top 10 với scores):
1 MB/sec là con số rất nhỏ. Network không phải bottleneck.
3.5 Tóm tắt Estimation
| Metric | Average | Peak | Design Target |
|---|---|---|---|
| Write QPS | 290/sec | 1,015/sec | 5,000/sec |
| Read QPS | 579/sec | 2,027/sec | 10,000/sec |
| Total QPS | 869/sec | 3,042/sec | 15,000/sec |
| Memory | 22.4 GB | — | 50 GB |
| Bandwidth | ~700 KB/sec | ~2.5 MB/sec | 10 MB/sec |
Kết luận từ estimation:
- Single Redis node đủ sức handle toàn bộ workload
- Memory, QPS, bandwidth đều nằm thoải mái trong giới hạn của 1 Redis instance
- Chỉ cần Redis replication (1 master + 2 replicas) cho high availability, không cần sharding
- Over-engineering (sharding, Kafka) là không cần thiết cho scale này
4. Security — Bảo vệ hệ thống leaderboard
4.1 Score Manipulation Prevention — Chống gian lận điểm
| Nguyên tắc | Mô tả |
|---|---|
| Server-side validation only | TUYỆT ĐỐI không cho client gửi score trực tiếp. Chỉ server mới được cập nhật score sau khi validate match result |
| Server authority | Game server là nguồn duy nhất của truth. Client chỉ gửi actions (di chuyển, bắn, skill), server tính toán kết quả |
| Match result verification | Server kiểm tra match result có hợp lý không trước khi cập nhật score |
| No client-side score | API không có endpoint “set my score to X”. Chỉ có “match completed, here’s the result” |
Flow an toàn:
ĐÚNG: Game Server tính toán kết quả → Validate → Cập nhật score
SAI: Client gửi "tôi được 100 điểm" → Server tin và cập nhật
4.2 Anti-Cheat — Phát hiện gian lận
| Loại gian lận | Mô tả | Cách phát hiện |
|---|---|---|
| Score injection | Hack API để gửi score giả | Server-side validation — không chấp nhận score từ client |
| Speed hack | Chơi nhanh bất thường (10 trận/phút) | Velocity check — giới hạn số trận/thời gian |
| Win trading | 2 người thỏa thuận — 1 người luôn thắng | Pattern detection — cùng 2 người chơi gặp nhau liên tục |
| Bot/automation | Dùng bot để chơi tự động | Behavioral analysis — thao tác quá chính xác, không có variance |
| Account boosting | Người giỏi chơi hộ tài khoản người yếu | IP/device fingerprint thay đổi bất thường |
| Exploit abuse | Lợi dụng bug để ghi điểm bất thường | Score anomaly detection — điểm tăng bất thường |
Anomaly Detection rules:
| Rule | Điều kiện | Hành động |
|---|---|---|
| Velocity check | > 20 trận/giờ | Flag account, yêu cầu review |
| Win rate anomaly | Win rate > 95% trong 50+ trận | Flag, kiểm tra match history |
| Score spike | Tăng > 50 điểm trong 1 giờ | Alert, manual review |
| Impossible score | Score vượt quá giá trị lý thuyết tối đa | Auto-reject, ban account |
| Time anomaly | Trận đấu kết thúc trong < 10 giây (minimum match duration) | Reject match result |
4.3 Rate Limiting — Giới hạn tần suất
| Endpoint | Rate Limit | Lý do |
|---|---|---|
| Score update | 10 requests/phút/user | Mỗi trận mất ít nhất 5 phút, 10 requests/phút là đủ |
| Get leaderboard (top K) | 30 requests/phút/user | Ngăn polling liên tục |
| Get my rank | 30 requests/phút/user | Tương tự top K |
| Get friend leaderboard | 10 requests/phút/user | Ít xem hơn global |
Tham chiếu: Tuan-09-Rate-Limiter — sử dụng Token Bucket hoặc Sliding Window algorithm.
4.4 Sybil Attack Prevention — Chống tạo tài khoản giả
| Tấn công | Mô tả | Phòng chống |
|---|---|---|
| Sybil attack | Tạo hàng nghìn tài khoản giả để chiếm top leaderboard | Phone verification, email verification, minimum level requirement |
| Farm accounts | Tài khoản giả “thua” cho tài khoản chính thắng | Match-making có kiểm tra: không cho tài khoản mới gặp nhau liên tục |
| Multi-accounting | 1 người có nhiều tài khoản trên leaderboard | Device fingerprinting, IP tracking, behavioral analysis |
Minimum eligibility: Chỉ hiển thị trên leaderboard khi:
- Account level >= 10 (chơi đủ 20+ trận)
- Email verified
- Không bị flag vì gian lận
4.5 Data Integrity
| Bảo vệ | Mô tả |
|---|---|
| Audit log | Mỗi score change được log: who, when, why, old_score, new_score |
| Immutable match history | Match results lưu trong database — không thể sửa đổi |
| Reconciliation | Định kỳ kiểm tra: tổng score trong Redis = tổng wins trong match history |
| Backup | Redis RDB snapshot + AOF — đảm bảo không mất dữ liệu |
5. DevOps — Vận hành hệ thống leaderboard
5.1 Redis Monitoring — Giám sát Redis
Các metric quan trọng
| Metric | Mô tả | Ngưỡng cảnh báo |
|---|---|---|
| used_memory | Tổng memory đang sử dụng | > 80% maxmemory |
| connected_clients | Số client đang kết nối | > 10,000 |
| instantaneous_ops_per_sec | Số commands/sec hiện tại | > 80,000 (80% capacity) |
| keyspace_hits / keyspace_misses | Hit rate của key lookup | Miss rate > 10% |
| rdb_last_bgsave_status | Trạng thái RDB snapshot gần nhất | Khác “ok” |
| aof_last_bgrewrite_status | Trạng thái AOF rewrite gần nhất | Khác “ok” |
| master_link_status | Trạng thái kết nối đến master (replica) | Khác “up” |
| master_last_io_seconds_ago | Thời gian từ lần đồng bộ gần nhất | > 10 giây |
5.2 Sorted Set Monitoring
| Metric | Command | Ngưỡng cảnh báo |
|---|---|---|
| Cardinality | ZCARD leaderboard:2026-03 | Bất thường nếu tăng/giảm đột ngột |
| Memory của 1 key | MEMORY USAGE leaderboard:2026-03 | > 5GB/key |
| Slow log | SLOWLOG GET 10 | Có command > 10ms |
5.3 Response Latency Monitoring
| Operation | Latency mong đợi | Alert threshold |
|---|---|---|
| ZADD / ZINCRBY | < 0.1ms | > 1ms |
| ZREVRANGE (top 10) | < 0.1ms | > 1ms |
| ZREVRANK | < 0.1ms | > 1ms |
| ZSCORE | < 0.05ms | > 0.5ms |
Cách đo latency:
- Sử dụng
redis-cli --latencyđể đo round-trip time - Application-side: đo thời gian từ gửi command đến nhận response
- Đo P50, P95, P99 — không chỉ average
5.4 Replication Lag Monitoring
| Metric | Mô tả | Ngưỡng cảnh báo |
|---|---|---|
| repl_backlog_active | Replication backlog có đang hoạt động | Phải luôn = 1 |
| master_repl_offset | Offset hiện tại của master | — |
| slave_repl_offset | Offset hiện tại của replica | Chênh lệch với master > 1MB |
| replication_lag_seconds | Độ trễ giữa master và replica | > 5 giây |
Tại sao replication lag quan trọng cho leaderboard?
Nếu read requests được gửi đến replica (read replica pattern), replication lag = stale data. Người chơi thấy rank cũ. Với lag > 5 giây, người chơi có thể thấy mình vẫn ở rank cũ dù đã thắng trận mới.
Giải pháp:
- Read writes (score update) từ master
- Reads có thể từ replica, nhưng cấu hình max lag tolerance
- Nếu lag > threshold, chuyển read về master
5.5 Monthly Reset Automation
| Bước | Tool | Cron/Trigger |
|---|---|---|
| Detect tháng mới | Cron job | 0 0 1 * * (00:00 ngày 1 mỗi tháng) |
| Create new Sorted Set | Leaderboard Service | Automatic khi detect tháng mới |
| Archive old leaderboard | Batch job | Export to S3/database |
| Set TTL for old Redis key | Script | EXPIRE leaderboard:old 7776000 |
| Send notification | Notification Service | ”New season has started!” |
| Verify new leaderboard | Health check | Kiểm tra ZCARD = 0 cho key mới |
Runbook cho monthly reset:
- Pre-reset check: Verify backup của leaderboard hiện tại đã hoàn tất
- Execute reset: Chuyển pointer sang key mới
- Post-reset verification:
- Key mới tồn tại và rỗng (ZCARD = 0)
- Key cũ vẫn truy cập được (archive)
- Score updates ghi vào key mới (không ghi vào key cũ)
- API trả về data từ key mới
- Rollback plan: Nếu có lỗi, chuyển pointer về key cũ
5.6 Alerting Rules
| Alert | Điều kiện | Severity | Hành động |
|---|---|---|---|
| Redis memory high | used_memory > 80% maxmemory | WARNING | Kiểm tra có leak không, tăng maxmemory |
| Redis memory critical | used_memory > 95% maxmemory | CRITICAL | Eviction bắt đầu — có thể mất data! |
| Redis down | Connection refused | CRITICAL | Failover sang replica |
| Replication broken | master_link_status = down | HIGH | Kiểm tra network, restart replica |
| Slow commands | Slowlog entries > 10/phút | WARNING | Kiểm tra có command O(N) không |
| Score update failure rate | > 1% failures | HIGH | Kiểm tra Redis connection, network |
| Leaderboard read latency | P99 > 100ms | WARNING | Kiểm tra Redis load, network latency |
5.7 Disaster Recovery
| Tình huống | Giải pháp |
|---|---|
| Redis master down | Sentinel/Cluster auto-failover sang replica |
| Data corruption | Restore từ RDB snapshot + replay AOF |
| Datacenter failure | Cross-region replica, failover sang region khác |
| Accidental DEL | RDB snapshot backup hàng giờ, AOF cho point-in-time recovery |
Redis high availability setup:
| Config | Giá trị | Lý do |
|---|---|---|
| Topology | 1 Master + 2 Replicas | Majority vote cho failover |
| Sentinel | 3 Sentinel instances | Monitor và auto-failover |
| Persistence | RDB (mỗi 1 giờ) + AOF (everysec) | Balance giữa performance và durability |
| maxmemory-policy | noeviction | Không tự động xóa data — báo lỗi thay vì mất data |
6. Mermaid Diagrams — Tổng hợp kiến trúc
6.1 High-Level Architecture (Chi tiết)
flowchart TB subgraph "Client Layer" MC[Mobile Client] PC[PC Client] WC[Web Client] end subgraph "Load Balancer" LB[Load Balancer<br/>Nginx / AWS ALB] end subgraph "API Gateway" GW[API Gateway<br/>Authentication<br/>Rate Limiting<br/>Request Routing] end subgraph "Application Services" GS[Game Service<br/>Match Processing<br/>Score Calculation] LS[Leaderboard Service<br/>Score Update<br/>Rank Query<br/>Top K Query] PS[Player Service<br/>Profile Management] NS[Notification Service<br/>Push Notifications] end subgraph "Cache Layer" LC[Local Cache<br/>Top 10/100<br/>TTL: 5s] end subgraph "Redis Cluster" RM[Redis Master<br/>Sorted Sets<br/>Write Operations] RR1[Redis Replica 1<br/>Read Operations] RR2[Redis Replica 2<br/>Read Operations] RS[Redis Sentinel<br/>Auto-Failover] end subgraph "Persistent Storage" DB[(MySQL<br/>Player Profiles<br/>Match History)] S3[(S3 / Object Storage<br/>Leaderboard Archives)] end subgraph "Monitoring" MON[Prometheus + Grafana<br/>Redis Metrics<br/>Latency Dashboards] end MC & PC & WC --> LB LB --> GW GW --> GS & LS & PS GS -->|"Score Update"| LS LS --> LC LS -->|"Write"| RM LS -->|"Read"| RR1 & RR2 RM -.->|"Replication"| RR1 & RR2 RS -.->|"Monitor"| RM & RR1 & RR2 GS --> DB LS --> PS PS --> DB GS --> NS LS -->|"Archive"| S3 RM & RR1 & RR2 -.-> MON style RM fill:#e53935,color:#fff style LS fill:#1e88e5,color:#fff style GS fill:#43a047,color:#fff style LC fill:#f9a825,color:#000
6.2 Redis Sorted Set Operations Flow
flowchart LR subgraph "Score Update" A1[Player wins match] --> A2["ZINCRBY lb:2026-03 1 user_123"] A2 --> A3[Skip List: reposition node<br/>Hash Table: update score] A3 --> A4["New score: 42<br/>New rank: auto-calculated"] end subgraph "Get Top 10" B1[Client: Show top 10] --> B2["ZREVRANGE lb:2026-03 0 9 WITHSCORES"] B2 --> B3[Skip List: traverse from tail<br/>collect 10 highest nodes] B3 --> B4["Result: [(Alice,150), (Bob,148), ...]"] end subgraph "Get My Rank" C1[Client: What's my rank?] --> C2["ZREVRANK lb:2026-03 user_123"] C2 --> C3[Hash Table: find node by member<br/>Skip List: sum spans to head] C3 --> C4["Rank: 1522 (0-indexed)"] end subgraph "Get My Score" D1[Client: What's my score?] --> D2["ZSCORE lb:2026-03 user_123"] D2 --> D3[Hash Table: direct O(1) lookup] D3 --> D4["Score: 42"] end style A2 fill:#e53935,color:#fff style B2 fill:#1e88e5,color:#fff style C2 fill:#43a047,color:#fff style D2 fill:#f9a825,color:#000
6.3 Sharded Leaderboard Merge Flow
flowchart TB subgraph "Request" CLIENT["Client: Get Global Top 10"] end subgraph "Coordinator" COORD[Leaderboard Service<br/>Scatter-Gather Coordinator] end subgraph "Shard Layer" S1["Shard 1 (hash 0-24%)<br/>ZREVRANGE 0 9<br/>→ [A:95, B:90, C:88, ...]"] S2["Shard 2 (hash 25-49%)<br/>ZREVRANGE 0 9<br/>→ [D:97, E:85, F:82, ...]"] S3["Shard 3 (hash 50-74%)<br/>ZREVRANGE 0 9<br/>→ [G:150, H:92, I:80, ...]"] S4["Shard 4 (hash 75-99%)<br/>ZREVRANGE 0 9<br/>→ [J:120, K:88, L:75, ...]"] end subgraph "Merge" MERGE["K-way Merge Sort<br/>Input: 4 × 10 = 40 entries<br/>Output: Global Top 10<br/>→ [G:150, J:120, D:97, A:95,<br/>H:92, B:90, C:88, K:88, E:85, F:82]"] end CLIENT --> COORD COORD -->|"Parallel"| S1 & S2 & S3 & S4 S1 & S2 & S3 & S4 --> MERGE MERGE --> COORD COORD --> CLIENT style MERGE fill:#e53935,color:#fff style COORD fill:#1e88e5,color:#fff
6.4 Score Update Pipeline (End-to-End)
flowchart TB subgraph "Step 1: Match Ends" MATCH["Game Server detects<br/>match result: Player A wins"] end subgraph "Step 2: Validate" VAL["Validation<br/>- Match ID exists?<br/>- Result consistent?<br/>- No duplicate submission?<br/>- Anti-cheat checks passed?"] end subgraph "Step 3: Persist" PERSIST["Save to MySQL<br/>- Match history record<br/>- Idempotency key"] end subgraph "Step 4: Update Leaderboard" UPDATE["Redis ZINCRBY<br/>leaderboard:2026-03 1 player_A<br/><br/>Also update:<br/>- Per-region leaderboard<br/>- Per-game-mode leaderboard<br/>- Weekly leaderboard<br/>- All-time leaderboard"] end subgraph "Step 5: Response" RESP["Return to client:<br/>- New score: 42<br/>- New rank: #1,523<br/>- Rank change: ↑ 5"] end subgraph "Step 6: Async" ASYNC["Async tasks:<br/>- Push notification if milestone<br/>- Analytics event<br/>- Achievement check"] end MATCH --> VAL VAL -->|"Valid"| PERSIST VAL -->|"Invalid"| REJECT["Reject<br/>Log suspicious activity"] PERSIST --> UPDATE UPDATE --> RESP UPDATE -.-> ASYNC style UPDATE fill:#e53935,color:#fff style VAL fill:#f9a825,color:#000 style REJECT fill:#757575,color:#fff
6.5 Monthly Reset Flow
flowchart TB subgraph "Trigger" CRON["Cron Job<br/>0 0 1 * * (1st of month, 00:00 UTC)"] end subgraph "Pre-check" CHECK["Pre-reset Verification<br/>- Backup current leaderboard?<br/>- Archive to S3?<br/>- New key name ready?"] end subgraph "Archive" ARCHIVE["Archive Old Leaderboard<br/>1. Export lb:2026-02 to S3 (JSON)<br/>2. Save top 100 to MySQL<br/>3. Generate season report"] end subgraph "Switch" SWITCH["Atomic Switch<br/>1. Set current_month = 2026-03<br/>2. New writes → lb:2026-03<br/>3. EXPIRE lb:2026-02 7776000"] end subgraph "Verify" VERIFY["Post-reset Verification<br/>- ZCARD lb:2026-03 = 0?<br/>- New scores go to new key?<br/>- Old key still readable?<br/>- API returns correct data?"] end subgraph "Notify" NOTIFY["Notifications<br/>- Push: 'New season started!'<br/>- Email: Season 2 summary<br/>- In-game: Season rewards"] end CRON --> CHECK CHECK -->|"OK"| ARCHIVE ARCHIVE --> SWITCH SWITCH --> VERIFY VERIFY -->|"Pass"| NOTIFY VERIFY -->|"Fail"| ROLLBACK["Rollback<br/>Switch back to old key<br/>Alert on-call engineer"] style SWITCH fill:#e53935,color:#fff style ROLLBACK fill:#757575,color:#fff
7. Aha Moments & Pitfalls
7.1 Aha Moments — Những insight quan trọng nhất
Insight #1: Redis Sorted Set là THE Answer cho Leaderboard
“Không có data structure nào khác phù hợp hơn Redis Sorted Set cho bài toán leaderboard. Nó giống như data structure được sinh ra để giải quyết vấn đề này.”
Mỗi operation của leaderboard đều map 1:1 với 1 Redis command:
- Cập nhật điểm → ZINCRBY
- Top 10 → ZREVRANGE
- Rank của tôi → ZREVRANK
- Điểm của tôi → ZSCORE
- Người quanh tôi → ZREVRANGE với offset
- Tổng số người chơi → ZCARD
Tất cả đều O(log N). Không cần viết thêm logic. Không cần custom data structure. Redis Sorted Set là complete solution.
Bài học: Trong system design interview, việc chọn đúng data structure có thể biến bài toán phức tạp thành bài toán đơn giản. Khi thấy bài toán leaderboard/ranking, nghĩ ngay đến Redis Sorted Set.
Insight #2: Skip List là Data Structure “Elegant”
“Skip list đạt O(log N) chỉ bằng random — không cần rotation, không cần rebalance. Elegant như một bài thơ.”
| So sánh | Balanced BST | Skip List |
|---|---|---|
| Insert | Rotate/rebalance (phức tạp) | Flip coin để chọn level (đơn giản) |
| Delete | Rotate/rebalance | Update pointers (đơn giản) |
| Range query | In-order traversal (pointer chasing) | Follow linked list (cache-friendly) |
| Concurrent | Cần lock toàn bộ subtree | Lock-free algorithms dễ implement |
| Implementation | 200+ dòng code | 100 dòng code |
Skip list là minh chứng rằng đơn giản có thể mạnh mẽ. Antirez chọn skip list cho Redis vì nó dễ implement, dễ debug, và đủ nhanh.
Bài học: Không phải lúc nào data structure “học thuật” nhất (Red-Black Tree, B+ Tree) cũng là lựa chọn tốt nhất. Đôi khi data structure đơn giản hơn (skip list, hash table) lại thực tế hơn vì dễ implement, dễ debug, và dễ maintain.
Insight #3: Sharding Leaderboard khó hơn nhiều so với Single Node
“Single Redis node giải quyết bài toán leaderboard trong 5 phút. Sharded leaderboard mất 5 ngày.”
| Thao tác | Single Node | Sharded |
|---|---|---|
| Top 10 | 1 command | N commands + merge |
| User rank | 1 command | N commands + sum |
| Update score | 1 command | 1 command (nhưng cần routing) |
| Monthly reset | 1 key | N keys |
| Consistency | Guaranteed | Eventual (cross-shard) |
Sharding biến mọi operation từ 1 bước thành N bước. Complexity tăng tuyến tính với số shards. Đây là lý do tại sao luôn bắt đầu với single node và chỉ shard khi thực sự cần.
Bài học: Trong interview, đừng nhảy vào sharding ngay. Trình bày single node trước, làm estimation để chứng minh single node đủ (hoặc không đủ), rồi mới thảo luận sharding. Interviewer muốn thấy bạn hiểu khi nào cần shard, không chỉ cách shard.
Insight #4: Estimation Quyết Định Kiến Trúc
“290 writes/sec và 3.5GB memory — những con số này cho thấy single Redis node là quá đủ. Estimation cứu bạn khỏi over-engineering.”
Nếu không làm estimation:
- Có thể thêm Kafka “cho an toàn” → tăng complexity không cần thiết
- Có thể shard Redis “phòng trường hợp” → tăng cost và complexity
- Có thể dùng distributed cache → overkill
Estimation cho thấy:
- Redis xử lý 100K+ ops/sec, ta chỉ cần 3K → 3% capacity
- Redis hỗ trợ hàng chục GB, ta chỉ cần 3.5GB → 10% capacity
- Không cần làm gì phức tạp — single node + replication là đủ
Bài học: Luôn làm estimation trước khi quyết định kiến trúc. Con số không nói dối. Tham chiếu Tuan-02-Back-of-the-envelope cho các kỹ thuật estimation.
Insight #5: Tie-breaking Cần Thiết Kế Cẩn Thận
“2 người cùng 42 điểm — ai xếp trước? Câu hỏi tưởng đơn giản nhưng giải pháp (composite score) cho thấy độ sâu của thiết kế.”
Composite score (score * 10^13 + (MAX_TIMESTAMP - timestamp)) là kỹ thuật packing 2 values into 1 number. Đây là pattern reusable cho nhiều bài toán:
- Leaderboard: score + time
- Task priority: priority level + creation time
- Version ordering: major version * 1000 + minor version
Bài học: Khi data structure chỉ hỗ trợ sort theo 1 field, hãy nghĩ đến composite key/composite score. Đây là trick nhỏ nhưng hữu ích trong nhiều tình huống thực tế.
7.2 Pitfalls — Những cái bẫy thường gặp
Pitfall #1: Dùng SQL cho real-time ranking
Sai:
SELECT COUNT(*) FROM scores WHERE score > ?cho mỗi rank query. Đúng: Redis Sorted Set — ZREVRANK trả về rank trong O(log N).
Lý do: SQL COUNT(*) là O(n) scan. Với 25M rows, mỗi query mất vài giây. Nhân với 50M queries/ngày = database chết.
Pitfall #2: Client gửi score trực tiếp
Sai: Client gửi API request “set my score to 100”. Đúng: Game server tính toán score và gửi kết quả từ server-side.
Lý do: Client có thể bị hack, modified, hoặc intercept. Bất kỳ data nào từ client đều không đáng tin cậy. Never trust the client.
Pitfall #3: Over-engineer với sharding từ đầu
Sai: “25M users — cần shard Redis thành 10 nodes!” Đúng: Làm estimation trước. 25M entries × 140 bytes = 3.5GB. 1 Redis node (64GB) đủ sức.
Lý do: Sharding tăng complexity O(N) cho mỗi operation. Chỉ shard khi estimation chứng minh single node không đủ.
Pitfall #4: Không xử lý tie-breaking
Sai: 2 người cùng 42 điểm — Redis trả về thứ tự ngẫu nhiên. Đúng: Composite score đảm bảo thứ tự deterministic.
Lý do: Redis Sorted Set sắp xếp member có cùng score theo lexicographic order của member name. Điều này không công bằng và không predictable. Composite score giải quyết vấn đề này.
Pitfall #5: Dùng DEL để reset leaderboard
Sai:
DEL leaderboard:2026-02(xóa toàn bộ Sorted Set). Đúng: Tạo key mớileaderboard:2026-03, để key cũ tự hết hạn bằng TTL.
Lý do: DEL trên Sorted Set 25M entries là O(N) operation — block Redis hàng giây. Trong thời gian đó, mọi operation khác bị queue lại. Dùng UNLINK (async delete) nếu bắt buộc phải xóa, hoặc tốt hơn — dùng TTL.
Pitfall #6: Không cache top K
Sai: Mỗi request “Get Top 10” đều query Redis. Đúng: Cache top 10 với TTL 5 giây.
Lý do: Top 10 là hot data — hàng triệu người xem. Dù Redis nhanh, hàng triệu concurrent requests vẫn tạo load không cần thiết. Cache 5 giây giảm 90% load mà data chỉ cũ tối đa 5 giây.
Pitfall #7: Quên archive leaderboard cũ trước khi reset
Sai: Reset leaderboard mà không lưu kết quả tháng trước. Đúng: Archive ra persistent storage (S3, database) trước khi set TTL cho key cũ.
Lý do: Người chơi muốn xem lại kết quả tháng trước. Rewards, badges, certificates đều cần dữ liệu lịch sử. Mất data lịch sử = mất lòng tin người chơi.
8. Internal Links — Liên kết với các tuần khác
| Tuần | Liên kết | Áp dụng trong Gaming Leaderboard |
|---|---|---|
| Tuan-06-Cache-Strategy | Cache strategy | Cache top K leaderboard với short TTL (5s) để giảm load lên Redis. Application-level cache cho hot data. Cache invalidation strategy cho leaderboard data. |
| Tuan-09-Rate-Limiter | Rate Limiting | Rate limit score update endpoints (10 req/phút/user) và leaderboard view endpoints (30 req/phút/user). Ngăn abuse và DDoS. Token Bucket hoặc Sliding Window algorithm. |
| Tuan-10-Consistent-Hashing | Consistent Hashing | Khi shard leaderboard, consistent hashing giúp phân phối users đều giữa các shards và giảm data migration khi thêm/bớt shard. |
| Tuan-02-Back-of-the-envelope | Estimation | Estimation cho QPS, memory, bandwidth — chứng minh single Redis node đủ cho 25M users. Estimation là cơ sở để quyết định kiến trúc (single vs sharded). |
| Tuan-07-Database-Sharding-Replication | Sharding & Replication | Redis replication cho high availability (1 master + 2 replicas). Sharding strategy khi vượt quá single node. Score range partitioning vs hash partitioning. |
| Tuan-08-Message-Queue | Message Queue | Kafka cho near-real-time approach (batch score updates). Event streaming cho analytics và archive. Decouple game service và leaderboard service. |
| Tuan-01-Scale-From-Zero-To-Millions | Scaling fundamentals | Bắt đầu với single Redis node, scale lên replication, rồi mới sharding. Vertical scaling (bigger Redis) trước horizontal scaling (more shards). |
9. Glossary — Từ điển thuật ngữ
| Thuật ngữ | Tiếng Việt | Mô tả |
|---|---|---|
| Leaderboard | Bảng xếp hạng | Danh sách người chơi sắp xếp theo điểm |
| Sorted Set | Tập hợp có thứ tự | Data structure của Redis lưu (member, score) đã sắp xếp |
| Skip List | Danh sách nhảy | Probabilistic data structure — multi-level linked list |
| ZADD | — | Redis command thêm/cập nhật member trong Sorted Set |
| ZINCRBY | — | Redis command tăng score của member |
| ZREVRANGE | — | Redis command lấy members theo thứ tự score giảm dần |
| ZREVRANK | — | Redis command lấy rank của member (score giảm dần, 0-indexed) |
| ZSCORE | — | Redis command lấy score của member |
| ZCARD | — | Redis command đếm tổng số members trong Sorted Set |
| Composite Score | Điểm kết hợp | Kỹ thuật pack nhiều giá trị (score + timestamp) vào 1 number |
| Tie-breaking | Xử lý trùng điểm | Quy tắc xếp hạng khi 2 người cùng điểm |
| Scatter-Gather | Phân tán — thu thập | Pattern gửi request đến nhiều shards rồi gộp kết quả |
| DAU | Người dùng hoạt động hàng ngày | Daily Active Users |
| QPS | Truy vấn mỗi giây | Queries Per Second |
| TTL | Thời gian sống | Time To Live — thời gian key tồn tại trước khi bị xóa |
| Sentinel | Lính canh | Redis Sentinel — hệ thống giám sát và auto-failover |
| RDB | — | Redis Database Backup — snapshot toàn bộ dữ liệu |
| AOF | — | Append-Only File — log mọi write command |
| Sybil Attack | Tấn công Sybil | Tạo nhiều tài khoản giả để thao túng hệ thống |
10. Tổng kết — Những điều Hieu cần nhớ
Top 5 Takeaways
-
Redis Sorted Set là THE answer cho leaderboard — Mọi operation (add, rank, top K, score) đều có sẵn và đều O(log N). Không cần custom data structure, không cần phức tạp.
-
Skip list là data structure elegant — Đạt O(log N) chỉ bằng random (coin flip). Đơn giản hơn balanced BST, thân thiện với concurrent access, dễ implement và debug. Redis chọn skip list là có lý do.
-
Estimation quyết định kiến trúc — 290 writes/sec, 3.5GB memory → single Redis node đủ thoải mái. Không cần Kafka, không cần sharding, không cần over-engineering. Làm estimation trước, thiết kế sau.
-
Sharding leaderboard khó hơn rất nhiều so với single node — Top K cần scatter-gather, rank cần sum across shards, consistency khó đảm bảo. Luôn ưu tiên single node. Chỉ shard khi estimation chứng minh bắt buộc.
-
Tie-breaking cần thiết kế từ đầu — Composite score (
score * 10^13 + MAX_TIMESTAMP - timestamp) đảm bảo thứ tự deterministic khi 2 người cùng điểm. Đây là chi tiết nhỏ nhưng quan trọng.
So sánh Gaming Leaderboard với các hệ thống khác
| Khía cạnh | Leaderboard | Stock Exchange | Payment System | Chat System |
|---|---|---|---|---|
| Data structure chính | Redis Sorted Set | In-memory Order Book | Database + Ledger | Message Queue + DB |
| Latency target | < 100ms | < 100 microseconds | < 1 giây | < 500ms |
| Consistency | Eventually consistent | Strict ordering | Strong consistency | Eventually consistent |
| Scaling strategy | Single node → Shard | Vertical (1 node) | Horizontal (microservices) | Horizontal (shard by user) |
| Data structure insight | Skip list | FIFO queue per price level | Double-entry ledger | Append-only message log |
| Core challenge | O(log N) rank query at scale | Microsecond matching | Exactly-once payment | Message ordering + delivery |
Câu hỏi tự kiểm tra cho Hieu
- Tại sao SQL
COUNT(*) WHERE score > Xkhông scale cho leaderboard? - Redis Sorted Set dùng data structure gì bên trong? Tại sao không dùng Red-Black Tree?
- Giải thích cách composite score xử lý tie-breaking. Tại sao dùng
MAX_TIMESTAMP - timestampthay vì chỉtimestamp? - Khi nào nên dùng real-time (direct Redis write) và khi nào nên dùng near-real-time (Kafka → Redis)?
- Tại sao nên bắt đầu với single Redis node thay vì sharding từ đầu?
- Giải thích scatter-gather pattern cho sharded leaderboard top K query.
DELtrên Sorted Set 25M entries có vấn đề gì? Giải pháp thay thế là gì?- Làm estimation: với 100M DAU và 500M scores/ngày, có cần shard Redis không? Memory và QPS là bao nhiêu?
“Bài toán leaderboard dạy Hieu một bài học quan trọng: chọn đúng data structure có thể biến bài toán O(n) thành O(log n), biến hệ thống không chạy được thành hệ thống chạy mượt mà. Redis Sorted Set không chỉ là tool — nó là minh chứng cho sức mạnh của computer science fundamentals.”