Sorted Sets trong Redis: Lý thuyết và Thực hành Leaderboard
Bảng xếp hạng game, trending topics, rate limiting theo thời gian — tất cả đều xây được bằng Sorted Sets với độ phức tạp O(log N). Lý thuyết đầy đủ kèm thực hành Python chi tiết trong bài này.
11 phút đọc•
Mục lục bài viết25
Bạn có biết?
Khi bạn chơi game và nhìn thấy bảng xếp hạng top 100 người chơi, hay khi bạn lướt Twitter và thấy “trending topics” — rất có thể Redis Sorted Sets đang làm việc phía sau. Đây là cấu trúc dữ liệu mạnh mẽ nhất trong Redis cho các bài toán xếp hạng và ưu tiên.
Sorted Sets là gì?
Sorted Set (ZSET) là một tập hợp các phần tử không trùng lặp, mỗi phần tử có một score (điểm số) dùng để sắp xếp. Khác với Set thông thường, Sorted Sets luôn được duy trì theo thứ tự tăng dần của score.
Tưởng tượng như một bảng xếp hạng:
Member — Tên người chơi (ví dụ: “player:1001”)
Score — Điểm số (ví dụ: 9500)
Redis tự động sắp xếp theo score từ thấp đến cao
Tại sao dùng Sorted Sets?
✅ O(log N) cho insert, update, delete — siêu nhanh
✅ O(log N + M) cho range queries — lấy top N phần tử
Ứng dụng chạy ngon ở dev nhưng lên production, Redis vừa restart là mọi request treo 30 giây rồi trả 500. Vấn đề nằm ở cách quản lý kết nối — bài này dạy bạn cấu hình ioredis để hệ thống sống khỏe.
Ứng dụng giao đồ ăn hiện 20 quán quanh mình, sắp xếp theo khoảng cách chỉ trong mili giây — không cần database spatial hay Google Maps API, chỉ với vài lệnh Geo xây trên nền Sorted Sets.
Đếm DAU cho 10 triệu user bằng Set tốn hàng trăm MB RAM, nhưng HyperLogLog chỉ cần 12KB và Bitmap theo dõi trạng thái từng user với vỏn vẹn 1.25MB. Hai vũ khí bí mật cho analytics quy mô lớn.
1 phút đọc
6) "player:1001" (9500)
#
7) "player:1003" (9900)
#
Lấy top 3 (với scores)
#
1) "player:1003" 2) "9900"
#
3) "player:1001" 4) "9500"
#
5) "player:1007" 6) "9300"
Lấy theo rank và score
# Xếp hạng của một phần tử (0-indexed, thấp nhất = 0)
ZRANK leaderboard "player:1001"
# (integer) 5 → xếp hạng 6 (từ dưới lên)
# Xếp hạng từ cao xuống thấp
ZREVRANK leaderboard "player:1001"
# (integer) 1 → xếp hạng 2 (từ trên xuống)
# Lấy score của một phần tử
ZSCORE leaderboard "player:1001"
# "9500"
# Đếm số phần tử có score trong khoảng
ZCOUNT leaderboard 8000 9500
# (integer) 5
Cập nhật score
# Tăng score
ZINCRBY leaderboard 500 "player:1002"# "9200" → player:1002 từ 8700 lên 9200# Giảm score
ZINCRBY leaderboard -200 "player:1003"# "9700" → player:1003 từ 9900 xuống 9700# Cập nhật score trực tiếp
ZADD leaderboard 10000 "player:1001" XX
# XX: chỉ cập nhật nếu member đã tồn tại
Xóa phần tử
# Xóa một phần tử
ZREM leaderboard "player:1004"# Xóa theo rank (xóa 3 người cuối)
ZREMRANGEBYRANK leaderboard 0 2
# Xóa theo score (xóa người có điểm < 8000)
ZREMRANGEBYSCORE leaderboard 0 7999
Bảng xếp hạng game — Sorted Sets là cấu trúc dữ liệu lý tưởng cho leaderboard real-time
Range Queries nâng cao
Lấy theo khoảng score
# Lấy tất cả người chơi có điểm từ 9000 đến 10000
ZRANGEBYSCORE leaderboard 9000 10000
# 1) "player:1005" (9100)# 2) "player:1007" (9300)# 3) "player:1001" (9500)# 4) "player:1003" (9700)
# Với scores
ZRANGEBYSCORE leaderboard 9000 10000 WITHSCORES
# Giới hạn kết quả (offset 0, lấy 2 kết quả)
ZRANGEBYSCORE leaderboard 9000 10000 LIMIT 0 2
# Lấy theo score từ cao xuống thấp
ZREVRANGEBYSCORE leaderboard 10000 9000
Dưới đây là ví dụ thực tế về xây dựng leaderboard (bảng xếp hạng) bằng Redis Sorted Sets với Python:
1. Tạo Leaderboard cơ bản
import redis
import time
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
LEADERBOARD_KEY = "game:leaderboard"defupdate_score(player_id, score):
"""Cập nhật điểm cho người chơi"""
r.zadd(LEADERBOARD_KEY, {player_id: score})
print(f"Đã cập nhật {player_id}: {score} điểm")
defget_top_n(n=10):
"""Lấy top N người chơi"""
players = r.zrevrange(LEADERBOARD_KEY, 0, n-1, withscores=True)
print(f"\n=== TOP {n} ===")
for rank, (player, score) inenumerate(players, 1):
print(f"#{rank} - {player}: {int(score)}")
defget_player_rank(player_id):
"""Lấy xếp hạng của người chơi"""
rank = r.zrevrank(LEADERBOARD_KEY, player_id)
score = r.zscore(LEADERBOARD_KEY, player_id)
if rank isnotNone:
print(f"{player_id}: #{rank + 1} với {int(score)} điểm")
else:
print(f"{player_id} chưa có trong bảng xếp hạng")
# Su dung
update_score("player_1001", 9500)
update_score("player_1002", 8700)
update_score("player_1003", 9900)
get_top_n(3)
get_player_rank("player_1001")
2. Leaderboard thời gian thực
defget_players_around(player_id, range_n=3):
"""Lấy những người chơi xung quanh 1 player"""
rank = r.zrevrank(LEADERBOARD_KEY, player_id)
if rank isNone:
return []
start = max(0, rank - range_n)
end = rank + range_n
players = r.zrevrange(LEADERBOARD_KEY, start, end, withscores=True)
print(f"\n=== Xung quanh {player_id} ===")
for i, (p, s) inenumerate(players, start + 1):
marker = " <<<"if p == player_id else""print(f"#{i} - {p}: {int(s)}{marker}")
get_players_around("player_1002")
3. Cập nhật điểm theo thời gian thực
defincrement_score(player_id, points):
"""Tăng điểm cho người chơi (atomic)"""
new_score = r.zincrby(LEADERBOARD_KEY, points, player_id)
print(f"{player_id} +{points} = {int(new_score)}")
increment_score("player_1001", 500) # Tăng 500 điểm
4. Xóa định kỳ dữ liệu cũ
defcleanup_old_players(threshold_score=1000):
"""Xóa người chơi có điểm thấp hơn ngưỡng"""
removed = r.zremrangebyscore(LEADERBOARD_KEY, 0, threshold_score - 1)
print(f"Đã xóa {removed} người chơi dưới {threshold_score} điểm")
return removed
Sorted Sets vs Lists vs Sets
Tiêu chí
Lists
Sets
Sorted Sets
Sắp xếp
Theo thứ tự thêm
Khôngcó thứ tự
Theo score
Trùng lặp
✅ Cho phép
❌ Không
❌ Không
Thêm
O(1)
O(1)
O(log N)
Lấy top N
O(N)
O(N log N)
O(log N + M)
Use case
Queue, Stack
Tags, Unique items
Leaderboard, Ranking
Best Practices
Dùng score hợp lý — Timestamp cho time-based, điểm số cho ranking
Giới hạn kích thước — Dùng ZREMRANGEBYRANK để giữ leaderboard nhỏ
Tránh ZRANGE 0 -1 — Với sorted set lớn, dùng LIMIT
Kết hợp EXPIRE — Đặt TTL cho rate limiting keys
Dùng REV — ZREVRANGE cho leaderboard (cao xuống thấp)
Bước tiếp theo
Bạn đã nắm vững Sorted Sets trong Redis! Tiếp theo, chúng ta sẽ tìm hiểu về Redis Streams — cơ chế event log thế hệ mới, mạnh mẽ hơn Pub/Sub với consumer groups và message persistence.
0 bình luận
Đang tải bình luận...
Để lại bình luận