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 |

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é!