Xây dựng Leaderboard với Redis Sorted Sets

Photo of author

Văn Ngọc Tân

Xây dựng Leaderboard với Redis Sorted Sets

Bạn có biết? Khi bạn chơi một tựa game bất kỳ và nhìn thấy bảng xếp hạng điểm số, rất có thể hệ thống đang sử dụng Redis Sorted Set ở phía sau. Hãy tưởng tượng: hàng triệu người chơi cùng lúc, mỗi lần hoàn thành một màn chơi, điểm số được cập nhật ngay lập tức — và bảng xếp hạng luôn hiển thị đúng thứ tự, không cần chờ đợi. Đó chính là sức mạnh của Sorted Set trong Redis.

Trong bài viết này, chúng ta sẽ tìm hiểu cách xây dựng một hệ thống leaderboard hoàn chỉnh bằng Redis Sorted Set, từ các lệnh cơ bản đến triển khai thực tế bằng Python.

Redis Sorted Set là gì?

Sorted Set là một cấu trúc dữ liệu đặc biệt trong Redis, nơi mỗi phần tử được gán một score (điểm số). Redis tự động sắp xếp các phần tử theo score này, giúp bạn truy xuất dữ liệu theo thứ tự tăng dần hoặc giảm dần cực kỳ nhanh chóng — với độ phức tạp thời gian là O(log N).

Đây chính là lý do Sorted Set trở thành lựa chọn hàng đầu cho các hệ thống leaderboard, ranking, hay bất kỳ bài toán nào cần sắp xếp dữ liệu động.

Các lệnh Sorted Set quan trọng

ZADD — Thêm hoặc cập nhật điểm số

Lệnh ZADD dùng để thêm một phần tử mới vào Sorted Set hoặc cập nhật điểm số nếu phần tử đã tồn tại.

# Thêm người chơi với điểm số
ZADD leaderboard 1500 "player:nguyen_van_a"
ZADD leaderboard 2300 "player:tran_thi_b"
ZADD leaderboard 1800 "player:le_van_c"

# Thêm nhiều người chơi cùng lúc
ZADD leaderboard 2100 "player:pham_thi_d" 900 "player:hoang_van_e"

ZRANK và ZREVRANK — Xác định thứ hạng

ZRANK trả về thứ hạng của một phần tử theo thứ tự tăng dần (thấp nhất = hạng 0). ZREVRANK trả về thứ hạng theo thứ tự giảm dần (cao nhất = hạng 0).

# Xếp hạng tăng dần (người có điểm thấp nhất = hạng 0)
ZRANK leaderboard "player:tran_thi_b"
# Kết quả: 4 (hạng 5, vì đếm từ 0)

# Xếp hạng giảm dần (người có điểm cao nhất = hạng 0)
ZREVRANK leaderboard "player:tran_thi_b"
# Kết quả: 0 (hạng 1, điểm cao nhất)

ZSCORE — Lấy điểm số

Lệnh ZSCORE trả về điểm số hiện tại của một phần tử cụ thể.

ZSCORE leaderboard "player:nguyen_van_a"
# Kết quả: "1500"

ZRANGE và ZREVRANGE — Lấy danh sách theo thứ hạng

ZRANGE lấy các phần tử theo thứ tự tăng dần, ZREVRANGE lấy theo thứ tự giảm dần. Tham số WITHSCORES sẽ trả kèm điểm số.

# Top 3 người chơi cao điểm nhất
ZREVRANGE leaderboard 0 2 WITHSCORES
# Kết quả:
# 1) "player:tran_thi_b"
# 2) "2300"
# 3) "player:pham_thi_d"
# 4) "2100"
# 5) "player:le_van_c"
# 6) "1800"

# Top 3 người chơi thấp điểm nhất
ZRANGE leaderboard 0 2 WITHSCORES

Triển khai bằng Python

Chúng ta sẽ xây dựng một class Leaderboard hoàn chỉnh, bao bọc các thao tác Sorted Set thành các phương thức dễ sử dụng.

import redis


class Leaderboard:
    """Hệ thống xếp hạng sử dụng Redis Sorted Set."""

    def __init__(self, key="leaderboard", host="localhost", port=6379, db=0):
        self.key = key
        self.client = redis.Redis(
            host=host, port=port, db=db, decode_responses=True
        )

    def add_player(self, player_id: str, score: float) -> None:
        """Thêm hoặc cập nhật điểm số cho người chơi."""
        self.client.zadd(self.key, {player_id: score})

    def get_score(self, player_id: str) -> float | None:
        """Lấy điểm số hiện tại của người chơi."""
        score = self.client.zscore(self.key, player_id)
        return float(score) if score is not None else None

    def get_rank(self, player_id: str) -> int | None:
        """Lấy thứ hạng của người chơi (hạng 1 = điểm cao nhất)."""
        rank = self.client.zrevrank(self.key, player_id)
        return rank + 1 if rank is not None else None

    def get_top(self, n: int = 10) -> list[dict]:
        """Lấy top N người chơi cao điểm nhất."""
        results = self.client.zrevrange(
            self.key, 0, n - 1, withscores=True
        )
        return [
            {"rank": i + 1, "player_id": pid, "score": int(score)}
            for i, (pid, score) in enumerate(results)
        ]

    def get_players_around(self, player_id: str, range_size: int = 2) -> list[dict]:
        """Lấy danh sách người chơi xung quanh một người chơi cụ thể."""
        rank = self.client.zrevrank(self.key, player_id)
        if rank is None:
            return []

        start = max(0, rank - range_size)
        end = rank + range_size

        results = self.client.zrevrange(
            self.key, start, end, withscores=True
        )
        return [
            {"rank": start + i + 1, "player_id": pid, "score": int(score)}
            for i, (pid, score) in enumerate(results)
        ]

    def remove_player(self, player_id: str) -> bool:
        """Xóa người chơi khỏi bảng xếp hạng."""
        return bool(self.client.zrem(self.key, player_id))

    def total_players(self) -> int:
        """Đếm tổng số người chơi trong bảng xếp hạng."""
        return self.client.zcard(self.key)


# --- Sử dụng ---
if __name__ == "__main__":
    lb = Leaderboard()

    # Thêm người chơi
    lb.add_player("nguyen_van_a", 1500)
    lb.add_player("tran_thi_b", 2300)
    lb.add_player("le_van_c", 1800)
    lb.add_player("pham_thi_d", 2100)
    lb.add_player("hoang_van_e", 900)

    # Xem top 3
    print("=== Top 3 ===")
    for p in lb.get_top(3):
        print(f"  Hạng {p['rank']}: {p['player_id']} — {p['score']} điểm")

    # Xem thứ hạng của một người
    rank = lb.get_rank("le_van_c")
    print(f"\nle_van_c đang xếp hạng: {rank}")

    # Xem những người xung quanh
    print("\n=== Xung quanh le_van_c ===")
    for p in lb.get_players_around("le_van_c", range_size=1):
        print(f"  Hạng {p['rank']}: {p['player_id']} — {p['score']} điểm")

Kết quả chạy chương trình:

=== Top 3 ===
  Hạng 1: tran_thi_b — 2300 điểm
  Hạng 2: pham_thi_d — 2100 điểm
  Hạng 3: le_van_c — 1800 điểm

le_van_c đang xếp hạng: 3

=== Xung quanh le_van_c ===
  Hạng 2: pham_thi_d — 2100 điểm
  Hạng 3: le_van_c — 1800 điểm
  Hạng 4: nguyen_van_a — 1500 điểm

Ứng dụng thực tế

Sorted Set không chỉ dùng cho game. Dưới đây là một số trường hợp sử dụng phổ biến:

Ứng dụng Score là gì Mô tả
Bảng xếp hạng game Điểm số Xếp hạng người chơi theo thành tích
Top sản phẩm bán chạy Số lượng bán Hiển thị sản phẩm được mua nhiều nhất
Bài viết xu hướng Lượt xem / tương tác Đưa bài viết hot lên đầu trang
Rate limiting Timestamp Theo dõi số lần request trong khung thời gian
Priority queue Mức độ ưu tiên Xử lý tác vụ theo thứ tự ưu tiên
Giao diện bảng xếp hạng trên ứng dụng di động
Leaderboard là tính năng phổ biến trong các ứng dụng game và mạng xã hội

So sánh Sorted Set vs SQL ORDER BY

Nhiều bạn sẽ thắc mắc: tại sao không dùng SQL đơn giản với ORDER BY score DESC? Hãy so sánh:

Tiêu chí Redis Sorted Set SQL ORDER BY
Độ phức tạp O(log N) cho insert/update O(N log N) cho sort
Tốc độ truy vấn top N O(log N + M) Tùy thuộc index
Cập nhật real-time Rất nhanh, trong bộ nhớ Chậm hơn, ghi đĩa
Khả năng mở rộng Horizontal scaling dễ dàng Khó scale ngang
Độ bền dữ liệu Có thể mất nếu không persistence Bền vững trên đĩa
Truy vấn phức tạp Hạn chế JOIN, GROUP BY, WHERE linh hoạt

Lời khuyên: Dùng Redis Sorted Set cho các truy vấn xếp hạng cần tốc độ cao và real-time. Vẫn giữ dữ liệu gốc trong SQL để đảm bảo tính bền vững và phục vụ các truy vấn phức tạp khác.

Best Practices

Để sử dụng Sorted Set hiệu quả trong môi trường production, hãy lưu ý những điểm sau:

1. Đặt tên key có tiền tố rõ ràng

# Tốt
ZADD game:leaderboard:daily:2026-03-30 1500 "user:123"

# Không tốt
ZADD lb 1500 "user:123"

2. Sử dụng TTL cho bảng xếp hạng theo thời gian

# Bảng xếp hạng hàng ngày — tự động xóa sau 48 giờ
EXPIRE game:leaderboard:daily:2026-03-30 172800

3. Giới hạn kích thước bảng xếp hạng

# Chỉ giữ top 1000 người chơi, xóa bớt người có điểm thấp
ZREMRANGEBYRANK leaderboard 0 -1001

4. Dùng pipeline khi cập nhật hàng loạt

pipe = client.pipeline()
for player_id, score in batch_scores.items():
    pipe.zadd("leaderboard", {player_id: score})
pipe.execute()

5. Kết hợp Redis và SQL

# Redis: lưu trữ và truy vấn xếp hạng nhanh
# SQL: lưu trữ dữ liệu chi tiết người chơi
# Khi hiển thị top 10, lấy player_id từ Redis,
# sau đó truy vấn thông tin chi tiết từ SQL

Bước tiếp theo

Sau khi nắm vững Sorted Set cho leaderboard, bạn có thể mở rộng kiến thức Redis sang lĩnh vực khác. Hãy tiếp tục tìm hiểu cách xây dựng Message Queue với Redis — một kỹ thuật quan trọng khác giúp xử lý các tác vụ bất đồng bộ trong hệ thống phân tán.

Nếu có câu hỏi hoặc muốn chia sẻ kinh nghiệm triển khai leaderboard, hãy để lại bình luận bên dưới nhé!

0 0 đánh giá
Đánh giá bài viết
Theo dõi
Thông báo của
guest
0 Góp ý
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất
Phản hồi nội tuyến
Xem tất cả bình luận