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:

  1. Top 10 ai? — Ai đang dẫn đầu bảng xếp hạng?
  2. Tôi đang ở đâu? — Thứ hạng của tôi là bao nhiêu trong số hàng triệu người?
  3. Ai ở quanh tôi? — Những người có thứ hạng gần tôi là ai?
  4. 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ày25-250 triệu
Leaderboard views/ngày50-500 triệu
Latency yêu cầu< 100ms cho mỗi thao tác
FreshnessReal-time hoặc near-real-time (< 5 giây)

1.3 Tại sao đây là bài toán khó?

Thách thứcGiải thích
Scale25 triệu score updates/ngày, 5 triệu DAU — không thể dùng brute-force
Real-time rankingKhi 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-breaking2 người cùng điểm — ai xếp trước?
Monthly resetMỗi tháng reset leaderboard — nhưng phải lưu lịch sử
Multiple leaderboardsPer-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ý doGiải thích
Mọi sản phẩm đều cần rankingKhông chỉ game — e-commerce (top sellers), social media (trending), education (student ranking), fitness (step count)
Redis Sorted Set là pattern cơ bảnHiểu Sorted Set = hiểu được hàng chục bài toán ranking khác
Interview favoriteLeaderboard 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ìnhReal-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ạnhSort thông thườngLeaderboard system
Tần suất updateMột lần (batch)Liên tục (real-time)
Tần suất queryMột lầnHàng triệu lần/ngày
Dữ liệuStaticDynamic — thay đổi mỗi giây
Kích thướcNhỏ (vạn phần tử)Lớn (hàng triệu phần tử)
Yêu cầuSắp xếp 1 lầnInsert, update, rank query, range query — tất cả O(log N)
Giải phápQuick sort, merge sortRedis 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ăngMô tả chi tiết
Display top 10Hiển thị bảng xếp hạng top 10 người chơi có điểm cao nhất
Show user rankNgười chơi xem được thứ hạng của mình trong toàn bộ leaderboard
Real-time updateKhi người chơi kết thúc trận đấu và điểm thay đổi, leaderboard cập nhật ngay
Score updateHệ thống cập nhật điểm khi người chơi thắng/thua trận
Monthly resetLeaderboard reset vào đầu mỗi tháng, bắt đầu lại từ 0
Relative rankingHiể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ầuMục tiêuLý do
LatencyP99 < 100ms cho mỗi thao tácUser experience — game player không chịu đợi
Throughput25 triệu score updates/ngày (~290 updates/sec avg, ~1,000/sec peak)Game match results từ 5M DAU
Availability99.9% uptimeLeaderboard down = player mất động lực chơi
Scalability5 triệu DAU, 25 triệu scores/ngàyScale theo số lượng player
FreshnessReal-time (< 1 giây) cho score updatePlayer muốn thấy kết quả ngay sau trận đấu
ConsistencyEventually consistent okSai 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ỏiTrả lờiGhi 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àyMỗ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ướcArchive leaderboard cũ
Real-time hay near-real-time?Real-time cho score updatePlayer thấy kết quả ngay
Có bao nhiêu loại leaderboard?Global + per-region + per-friendNhiều leaderboard đồng thời
Tie-breaking rule?Cùng điểm — ai đạt điểm trước xếp trướcTime-based tie-breaking

2.1.4 Scale Summary

Thông sốGiá trị
DAU5,000,000
Trận đấu/ngày25,000,000
Score updates/ngày25,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 cycleMonthly

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ò

ComponentVai tròĐặc điểm
Game ClientGửi kết quả trận đấu, hiển thị leaderboardMobile, PC, Console — đa nền tảng
API GatewayXác thực, rate limiting, routingNgăn chặn abuse, phân phối tải
Game ServiceXử lý kết quả trận đấu, tính điểmBusiness logic, validation
Leaderboard ServiceCập nhật điểm và truy vấn thứ hạngInterface giữa business logic và Redis
Redis Sorted SetLư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 historyPersistent storage, không dùng cho real-time ranking
Message Queue (Kafka)Event streaming cho analytics, archiveAsync 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.

ColumnType
user_idBIGINT (PK)
scoreINT
updated_atTIMESTAMP

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 queryMỗ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 rowsVới 25 triệu người chơi, mỗi query mất vài giây
50 triệu queries/ngày50 triệu leaderboard views × O(n) query = database chết
Lock contentionScore update (write) và rank query (read) đều cần truy cập cùng bảng — deadlock, slow
No real-timeIndex 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ướcSQL PerformanceChấp nhận được?
10,000 users~5ms
100,000 users~50msTạm ổn
1,000,000 users~500msChậm
25,000,000 users~5-10 giâyKHÔ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:

OperationMô tảComplexityLeaderboard Use Case
ZADDThêm hoặc cập nhật member với scoreO(log N)Cập nhật điểm người chơi
ZINCRBYTăng score của member thêm 1 giá trịO(log N)Cộng điểm sau mỗi trận thắng
ZREVRANGELấy top K members (score giảm dần)O(log N + K)Hiển thị top 10 leaderboard
ZREVRANKLấ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?”
ZSCORELấy score của 1 memberO(1)Hiển thị điểm hiện tại của người chơi
ZREVRANGEBYSCORELấy members trong khoảng scoreO(log N + K)Tìm người chơi có điểm trong khoảng
ZCARDĐếm tổng số membersO(1)Tổng số người chơi trên leaderboard
ZREMXóa memberO(log N)Xóa người chơi bị ban

Tại sao Redis Sorted Set hoàn hảo cho leaderboard?

Đặc điểmGiải thích
O(log N) cho mọi thao tácVới 25M members, log(25M) ~ 24 bước. Siêu nhanh.
In-memoryKhông có disk I/O, mọi thao tác mất vài microseconds
Atomic operationsZADD, ZINCRBY là atomic — không cần lock, không có race condition
Built-in rankingZREVRANK trả về thứ hạng ngay — không cần tính toán thêm
Range queryZREVRANGE lấy top K hiệu quả — O(log N + K)
Single data structure1 Sorted Set giải quyết 100% các use case của leaderboard
PersistenceRDB 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ácSQL (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 StructureVai tròOperations
Skip ListLưu members đã sắp xếp theo scoreZREVRANGE, ZREVRANK, range queries
Hash TableMap 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):

  1. Bắt đầu từ Level 3, HEAD → 40 (40 < 50, đi tiếp) → NIL (quá lớn, xuống level)
  2. Level 2: 40 → 60 (60 > 50, xuống level)
  3. 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 ListBalanced BST (Red-Black Tree)
ImplementationĐơn giản hơn nhiềuPhức tạp — rotation, recoloring
Concurrent accessLock-free algorithms dễ hơnLock toàn bộ cây khi rebalance
Range queryDuyệt linked list — cache-friendlyIn-order traversal — pointer chasing
Memory overhead~1.33 pointers/node (average)3 pointers/node (left, right, parent) + color
Insert/DeleteUpdate pointers ở các level — local operationCó thể trigger rebalance cascade
ComplexityO(log N) expectedO(log N) worst-case
DebugDễ visualize, dễ debugKhó 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:

  1. Backward pointer: Mỗi node có pointer ngược về node trước — cho phép duyệt ngược (ZREVRANGE)
  2. 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ể:

PlayerScoreTimestamp (ms)Composite Score
Alice421,710,700,800,00042 × 10^13 + (9,999,999,999,999 - 1,710,700,800,000) = 428,289,299,199,999
Bob421,710,700,900,00042 × 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áchKết quảĐúng/Sai
score * 10^13 + timestampTimestamp lớn hơn = score lớn hơn → người đạt điểm sau xếp trướcSAI — 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:

  1. Lấy rank của user: ZREVRANK leaderboard:2026-03 user_id → rank = 1522
  2. Lấy users xung quanh: ZREVRANGE leaderboard:2026-03 1517 1527 WITHSCORES (rank 1518 đến 1528)

Kết quả:

RankPlayerScore
#1518player_A43
#1519player_B43
#1520player_C42
#1521player_D42
#1522player_E42
#1523YOU42
#1524player_F42
#1525player_G41
#1526player_H41
#1527player_I41
#1528player_J40

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ốngCần shard?
25M users, ~1.5GB memoryKhông — 1 Redis node đủ sức (Redis hỗ trợ hàng chục GB)
500M users, ~30GB memoryCó thể — memory vẫn ok nhưng throughput có thể là bottleneck
5B users, ~300GB memoryChắc chắn — vượt quá memory của 1 node
Write throughput > 100K/secCó 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):

ShardScore RangeSố lượng users (ước tính)
Shard 00 - 9910M (majority là low scores)
Shard 1100 - 1998M
Shard 2200 - 2994M
Shard 3300 - 3992M
Shard 4400+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:

  1. Gửi ZREVRANGE 0 9 đến tất cả N shards (song song)
  2. Mỗi shard trả về top 10 của nó
  3. 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.

ApproachMô tảĐộ chính xác
Exact rankQuery ZREVRANK trên tất cả shards, cộng rank lạiChính xác 100% nhưng chậm (query N shards)
Approximate rankDùng sampling hoặc statisticsNhanh nhưng không chính xác
Score range partitionRank = tổng số users ở tất cả shard điểm cao hơn + rank trong shard hiện tạiChí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áchMô tảKhi nào dùng
Real-time (Direct Write)Game Service gọi trực tiếp Redis ZADD/ZINCRBYWrite 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 RedisWrite 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-timeNear-real-time
Write QPS< 10,000/sec> 10,000/sec
Latency requirement< 1 giây1-5 giây ok
Burst trafficKhông có hoặc ítCó burst lớn (ví dụ: event đặc biệt)
ComplexityĐơn giản hơnPhứ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ángRedis Key
Tháng 1/2026leaderboard:2026-01
Tháng 2/2026leaderboard:2026-02
Tháng 3/2026leaderboard:2026-03

Flow đầu tháng mới:

  1. 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
  2. Key leaderboard:2026-03 vẫn tồn tại — người chơi vẫn xem được bảng xếp hạng tháng trước
  3. Đặt TTL cho key cũ: EXPIRE leaderboard:2026-01 7776000 (90 ngày = 3 tháng lưu trữ)
  4. Sau 3 tháng, Redis tự động xóa key cũ — không cần manual cleanup

Automation workflow:

BướcThao tácThời điểm
1Leaderboard Service detect tháng mới (từ config hoặc cron)00:00:00 UTC ngày 1
2Chuyển current_leaderboard pointer sang key mới00:00:01 UTC
3Archive key cũ vào persistent storage (S3/database) nếu cần00:01:00 UTC (async)
4Đặt TTL cho key cũ trên Redis00:01:01 UTC
5Gử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ốngGiải pháp
Trận bắt đầu 23:58 tháng 3, kết thúc 00:02 tháng 4Dùng match_start_time để quyết định ghi vào leaderboard nào
Hoặc: dùng match_end_timeGhi vào leaderboard của tháng mà trận kết thúc
Race condition khi chuyển thángDù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ần ZREMRANGEBYSCORE (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 leaderboardMô tảVí dụ key
GlobalToàn bộ người chơilb:global:2026-03
Per-gameTheo từng game modelb:mode:ranked:2026-03
Per-regionTheo vùng địa lýlb:region:asia:2026-03
Per-friendChỉ trong danh sách bạn bèlb:friends:{user_id}:2026-03
Per-clanTheo clan/guildlb:clan:{clan_id}:2026-03
WeeklyReset hàng tuầnlb:weekly:2026-W12
All-timeKhông bao giờ resetlb:alltime

Namespaced keys:

Pattern key: lb:{scope}:{identifier}:{period}

PhầnÝ nghĩaVí dụ
lbPrefix cho leaderboardCố định
{scope}Loại leaderboardglobal, region, friends, clan
{identifier}ID cụ thểasia, user_123, clan_456
{period}Kỳ reset2026-03, 2026-W12, alltime

Friend leaderboard — đặc biệt khó:

Thách thứcGiải thích
Mỗi user có danh sách bạn bè khác nhauKhông thể dùng 1 Sorted Set chung
Số lượng leaderboard = số lượng users5M 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:

ApproachMô tảƯu/Nhược
On-demand computationKhi 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 writeKhi 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)
HybridCache friend leaderboard với short TTL (30 giây), invalidate khi có updateCâ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

TrangRedis CommandGiải thích
Trang 1 (rank 1-20)ZREVRANGE lb:2026-03 0 19 WITHSCORESLấy 20 users đầu tiên
Trang 2 (rank 21-40)ZREVRANGE lb:2026-03 20 39 WITHSCORESLấy 20 users tiếp theo
Trang NZREVRANGE lb:2026-03 (N-1)*20 N*20-1 WITHSCORESLấ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áchMô tảVấn đề
Offset-basedZREVRANGE ... start stopNế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ầngDữ liệuTTLNgười dùng
Application cache (local)Top 105 giâyGiảm load xuống Redis
Redis Sorted SetToàn bộ 25M usersKhông TTL (persistent)Source of truth

Flow với cache:

  1. Client request “Get Top 10”
  2. Leaderboard Service kiểm tra local cache
  3. Nếu cache hit (< 5 giây) → trả về ngay
  4. 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?

TTLFreshnessCache hit rateLoad giảm
1 giâyGần như real-time~50%~50%
5 giâyChấp nhận được cho game~90%~90%
30 giâyHơi cũ~98%~98%
5 phút~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?

DataCacheTTLLý do
Top 105sHot data — ai cũng xem
Top 10010sPhổ biến, nhiều người cuộn xuống
User rank (individual)3sMỗi user chỉ care rank của mình
Friend leaderboard30sÍ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
DAU5,000,0005 triệu người chơi hoạt động mỗi ngày
Trận đấu/người/ngày5Trung bình mỗi người chơi 5 trận
Tổng trận đấu/ngày25,000,0005M × 5
Active hours/ngày24 giờGame online 24/7
Peak/Average ratio3.5xGiờ 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ày10Xem top 10, xem rank, xem friends
Tổng views/ngày50,000,0005M × 10
Read/Write ratio2:1Xem 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ầnKích thướcGiải thích
Member (user_id)~20 bytesString “user_12345678”
Score8 bytesDouble-precision float
Skip list node~40 bytesPointers (forward, backward, span) — trung bình ~2.67 levels
Hash table entry~56 bytesKey + value + metadata của Redis dict
Redis overhead~16 bytesObject 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:

LeaderboardSố entriesMemory
Global tháng hiện tại25,000,0003.5 GB
Global tháng trước (archive)25,000,0003.5 GB
Per-region (5 regions)5 × 5,000,0003.5 GB
Per-game-mode (3 modes)3 × 25,000,00010.5 GB
Weekly10,000,0001.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

MetricAveragePeakDesign Target
Write QPS290/sec1,015/sec5,000/sec
Read QPS579/sec2,027/sec10,000/sec
Total QPS869/sec3,042/sec15,000/sec
Memory22.4 GB50 GB
Bandwidth~700 KB/sec~2.5 MB/sec10 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ắcMô tả
Server-side validation onlyTUYỆ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 authorityGame 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 verificationServer kiểm tra match result có hợp lý không trước khi cập nhật score
No client-side scoreAPI 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ậnMô tảCách phát hiện
Score injectionHack API để gửi score giảServer-side validation — không chấp nhận score từ client
Speed hackChơi nhanh bất thường (10 trận/phút)Velocity check — giới hạn số trận/thời gian
Win trading2 người thỏa thuận — 1 người luôn thắngPattern detection — cùng 2 người chơi gặp nhau liên tục
Bot/automationDùng bot để chơi tự độngBehavioral analysis — thao tác quá chính xác, không có variance
Account boostingNgười giỏi chơi hộ tài khoản người yếuIP/device fingerprint thay đổi bất thường
Exploit abuseLợi dụng bug để ghi điểm bất thườngScore anomaly detection — điểm tăng bất thường

Anomaly Detection rules:

RuleĐiều kiệnHành động
Velocity check> 20 trận/giờFlag account, yêu cầu review
Win rate anomalyWin rate > 95% trong 50+ trậnFlag, kiểm tra match history
Score spikeTăng > 50 điểm trong 1 giờAlert, manual review
Impossible scoreScore vượt quá giá trị lý thuyết tối đaAuto-reject, ban account
Time anomalyTrậ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

EndpointRate LimitLý do
Score update10 requests/phút/userMỗi trận mất ít nhất 5 phút, 10 requests/phút là đủ
Get leaderboard (top K)30 requests/phút/userNgăn polling liên tục
Get my rank30 requests/phút/userTương tự top K
Get friend leaderboard10 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ôngMô tảPhòng chống
Sybil attackTạo hàng nghìn tài khoản giả để chiếm top leaderboardPhone verification, email verification, minimum level requirement
Farm accountsTài khoản giả “thua” cho tài khoản chính thắngMatch-making có kiểm tra: không cho tài khoản mới gặp nhau liên tục
Multi-accounting1 người có nhiều tài khoản trên leaderboardDevice 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 logMỗi score change được log: who, when, why, old_score, new_score
Immutable match historyMatch 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
BackupRedis 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

MetricMô tảNgưỡng cảnh báo
used_memoryTổng memory đang sử dụng> 80% maxmemory
connected_clientsSố client đang kết nối> 10,000
instantaneous_ops_per_secSố commands/sec hiện tại> 80,000 (80% capacity)
keyspace_hits / keyspace_missesHit rate của key lookupMiss rate > 10%
rdb_last_bgsave_statusTrạng thái RDB snapshot gần nhấtKhác “ok”
aof_last_bgrewrite_statusTrạng thái AOF rewrite gần nhấtKhác “ok”
master_link_statusTrạng thái kết nối đến master (replica)Khác “up”
master_last_io_seconds_agoThời gian từ lần đồng bộ gần nhất> 10 giây

5.2 Sorted Set Monitoring

MetricCommandNgưỡng cảnh báo
CardinalityZCARD leaderboard:2026-03Bất thường nếu tăng/giảm đột ngột
Memory của 1 keyMEMORY USAGE leaderboard:2026-03> 5GB/key
Slow logSLOWLOG GET 10Có command > 10ms

5.3 Response Latency Monitoring

OperationLatency mong đợiAlert 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

MetricMô tảNgưỡng cảnh báo
repl_backlog_activeReplication backlog có đang hoạt độngPhải luôn = 1
master_repl_offsetOffset hiện tại của master
slave_repl_offsetOffset hiện tại của replicaChê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ướcToolCron/Trigger
Detect tháng mớiCron job0 0 1 * * (00:00 ngày 1 mỗi tháng)
Create new Sorted SetLeaderboard ServiceAutomatic khi detect tháng mới
Archive old leaderboardBatch jobExport to S3/database
Set TTL for old Redis keyScriptEXPIRE leaderboard:old 7776000
Send notificationNotification Service”New season has started!”
Verify new leaderboardHealth checkKiểm tra ZCARD = 0 cho key mới

Runbook cho monthly reset:

  1. Pre-reset check: Verify backup của leaderboard hiện tại đã hoàn tất
  2. Execute reset: Chuyển pointer sang key mới
  3. 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
  4. Rollback plan: Nếu có lỗi, chuyển pointer về key cũ

5.6 Alerting Rules

AlertĐiều kiệnSeverityHành động
Redis memory highused_memory > 80% maxmemoryWARNINGKiểm tra có leak không, tăng maxmemory
Redis memory criticalused_memory > 95% maxmemoryCRITICALEviction bắt đầu — có thể mất data!
Redis downConnection refusedCRITICALFailover sang replica
Replication brokenmaster_link_status = downHIGHKiểm tra network, restart replica
Slow commandsSlowlog entries > 10/phútWARNINGKiểm tra có command O(N) không
Score update failure rate> 1% failuresHIGHKiểm tra Redis connection, network
Leaderboard read latencyP99 > 100msWARNINGKiểm tra Redis load, network latency

5.7 Disaster Recovery

Tình huốngGiải pháp
Redis master downSentinel/Cluster auto-failover sang replica
Data corruptionRestore từ RDB snapshot + replay AOF
Datacenter failureCross-region replica, failover sang region khác
Accidental DELRDB snapshot backup hàng giờ, AOF cho point-in-time recovery

Redis high availability setup:

ConfigGiá trịLý do
Topology1 Master + 2 ReplicasMajority vote cho failover
Sentinel3 Sentinel instancesMonitor và auto-failover
PersistenceRDB (mỗi 1 giờ) + AOF (everysec)Balance giữa performance và durability
maxmemory-policynoevictionKhô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ánhBalanced BSTSkip List
InsertRotate/rebalance (phức tạp)Flip coin để chọn level (đơn giản)
DeleteRotate/rebalanceUpdate pointers (đơn giản)
Range queryIn-order traversal (pointer chasing)Follow linked list (cache-friendly)
ConcurrentCần lock toàn bộ subtreeLock-free algorithms dễ implement
Implementation200+ dòng code100 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ácSingle NodeSharded
Top 101 commandN commands + merge
User rank1 commandN commands + sum
Update score1 command1 command (nhưng cần routing)
Monthly reset1 keyN keys
ConsistencyGuaranteedEventual (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ới leaderboard: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.


TuầnLiên kếtÁp dụng trong Gaming Leaderboard
Tuan-06-Cache-StrategyCache strategyCache 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-LimiterRate LimitingRate 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-HashingConsistent HashingKhi 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-envelopeEstimationEstimation 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-ReplicationSharding & ReplicationRedis 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-QueueMessage QueueKafka 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-MillionsScaling fundamentalsBắ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ệtMô tả
LeaderboardBảng xếp hạngDanh sách người chơi sắp xếp theo điểm
Sorted SetTập hợp có thứ tựData structure của Redis lưu (member, score) đã sắp xếp
Skip ListDanh sách nhảyProbabilistic data structure — multi-level linked list
ZADDRedis command thêm/cập nhật member trong Sorted Set
ZINCRBYRedis command tăng score của member
ZREVRANGERedis command lấy members theo thứ tự score giảm dần
ZREVRANKRedis command lấy rank của member (score giảm dần, 0-indexed)
ZSCORERedis command lấy score của member
ZCARDRedis command đếm tổng số members trong Sorted Set
Composite ScoreĐiểm kết hợpKỹ thuật pack nhiều giá trị (score + timestamp) vào 1 number
Tie-breakingXử lý trùng điểmQuy tắc xếp hạng khi 2 người cùng điểm
Scatter-GatherPhân tán — thu thậpPattern gửi request đến nhiều shards rồi gộp kết quả
DAUNgười dùng hoạt động hàng ngàyDaily Active Users
QPSTruy vấn mỗi giâyQueries Per Second
TTLThời gian sốngTime To Live — thời gian key tồn tại trước khi bị xóa
SentinelLính canhRedis Sentinel — hệ thống giám sát và auto-failover
RDBRedis Database Backup — snapshot toàn bộ dữ liệu
AOFAppend-Only File — log mọi write command
Sybil AttackTấn công SybilTạ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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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ạnhLeaderboardStock ExchangePayment SystemChat System
Data structure chínhRedis Sorted SetIn-memory Order BookDatabase + LedgerMessage Queue + DB
Latency target< 100ms< 100 microseconds< 1 giây< 500ms
ConsistencyEventually consistentStrict orderingStrong consistencyEventually consistent
Scaling strategySingle node → ShardVertical (1 node)Horizontal (microservices)Horizontal (shard by user)
Data structure insightSkip listFIFO queue per price levelDouble-entry ledgerAppend-only message log
Core challengeO(log N) rank query at scaleMicrosecond matchingExactly-once paymentMessage ordering + delivery

Câu hỏi tự kiểm tra cho Hieu

  1. Tại sao SQL COUNT(*) WHERE score > X không scale cho leaderboard?
  2. Redis Sorted Set dùng data structure gì bên trong? Tại sao không dùng Red-Black Tree?
  3. Giải thích cách composite score xử lý tie-breaking. Tại sao dùng MAX_TIMESTAMP - timestamp thay vì chỉ timestamp?
  4. 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)?
  5. Tại sao nên bắt đầu với single Redis node thay vì sharding từ đầu?
  6. Giải thích scatter-gather pattern cho sharded leaderboard top K query.
  7. DEL trên Sorted Set 25M entries có vấn đề gì? Giải pháp thay thế là gì?
  8. 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.”