ข้ามไปยังเนื้อหา

Leaderboards

Leaderboard ของเกมหรือแอปต้องการสามสิ่ง: เก็บคะแนน, จัดอันดับผู้เล่น, และ เพิ่มคะแนนเมื่อเวลาผ่านไป Redis Sorted Sets จัดการทั้งสามแบบ native ด้วย O(log N) สำหรับการเขียนและ O(log N + M) สำหรับ range read

เริ่มด้วยการ insert ผู้เล่นและคะแนน คะแนนเป็น float64 จึงแทนได้ทั้งแต้ม, เปอร์เซ็นต์, timestamp, หรือค่าตัวเลขใดก็ได้

127.0.0.1:6379> ZADD game:lb 3200 "alice" 4100 "bob" 1750 "carol" 5800 "dave" 2900 "eve"
(integer) 5
ZADD game:lb 3200 "alice" 4100 "bob" 1750 "carol" 5800 "dave" 2900 "eve"

ZREVRANGE key 0 9 WITHSCORES คืนผู้เล่น 10 อันดับแรกตามลำดับคะแนนจากมากไปน้อย Index เป็น rank offset ดังนั้น 0 9 หมายถึงตำแหน่ง 1 ถึง 10 เสมอ

127.0.0.1:6379> ZREVRANGE game:lb 0 9 WITHSCORES
1) "dave"
2) "5800"
3) "bob"
4) "4100"
5) "alice"
6) "3200"
7) "eve"
8) "2900"
9) "carol"
10) "1750"
ZREVRANGE game:lb 0 9 WITHSCORES

ZREVRANK key member คืน rank แบบ 0-based ตามลำดับจากมากไปน้อย — 0 คืออันดับหนึ่ง บวก 1 เพื่อให้อ่านง่ายขึ้นสำหรับมนุษย์

127.0.0.1:6379> ZREVRANK game:lb "alice"
(integer) 2
127.0.0.1:6379> ZREVRANK game:lb "carol"
(integer) 4
127.0.0.1:6379> ZSCORE game:lb "alice"
"3200"
ZREVRANK game:lb "alice"
ZREVRANK game:lb "carol"
ZSCORE game:lb "alice"

เมื่อผู้เล่นได้แต้มเพิ่ม ใช้ ZINCRBY เพื่อบวกเข้ากับคะแนนที่มีอยู่แบบ atomic ไม่ต้องทำ read-modify-write cycle

127.0.0.1:6379> ZINCRBY game:lb 1500 "carol"
"3250"
127.0.0.1:6379> ZINCRBY game:lb 800 "alice"
"4000"
127.0.0.1:6379> ZREVRANGE game:lb 0 4 WITHSCORES
1) "dave"
2) "5800"
3) "bob"
4) "4100"
5) "alice"
6) "4000"
7) "carol"
8) "3250"
9) "eve"
10) "2900"
ZINCRBY game:lb 1500 "carol"
ZINCRBY game:lb 800 "alice"
ZREVRANGE game:lb 0 4 WITHSCORES
127.0.0.1:6379> ZADD lb 3200 "alice" 4100 "bob" 1750 "carol" 5800 "dave" 2900 "eve"
(integer) 5
127.0.0.1:6379> ZREVRANGE lb 0 9 WITHSCORES
1) "dave"
2) "5800"
3) "bob"
4) "4100"
5) "alice"
6) "3200"
7) "eve"
8) "2900"
9) "carol"
10) "1750"
127.0.0.1:6379> ZREVRANK lb "carol"
(integer) 4
127.0.0.1:6379> ZINCRBY lb 2500 "carol"
"4250"
127.0.0.1:6379> ZREVRANK lb "carol"
(integer) 1
ZADD lb 3200 "alice" 4100 "bob" 1750 "carol" 5800 "dave" 2900 "eve"
ZREVRANGE lb 0 9 WITHSCORES
ZREVRANK lb "carol"
ZINCRBY lb 2500 "carol"
ZREVRANK lb "carol"
ตัวเลือกBenefitCost
Sorted Set สำหรับ leaderboardranking แบบ real-time โดยไม่ต้อง JOIN หรือ ORDER BY, เขียนและอ่าน O(log N)คะแนนเป็น float64 เท่านั้น ต้อง encode tie-breaker เองถ้าต้องการ ranking ที่ซับซ้อนกว่าคะแนนตัวเลข
ZINCRBY แบบ atomicอัปเดตคะแนนโดยไม่ต้อง read-modify-write, ปลอดภัยเมื่อมีหลาย client เขียนพร้อมกันไม่มี event log ของการเปลี่ยนแปลง ถ้าต้องการ audit trail ว่าใครมอบแต้มเมื่อไรต้องเก็บแยกเอง
ดึง top N ด้วย ZREVRANGEเร็วและง่าย ไม่ต้อง cache ผลลัพธ์แยกเรียก fresh ทุกครั้งบน leaderboard ขนาดใหญ่ที่ traffic สูงมากอาจยังต้องมี cache layer เพิ่มเติม
  • ใช้คะแนนเดียวกันสำหรับผู้เล่นหลายคนโดยไม่ตั้งใจZRANGE/ZREVRANGE จะเรียงลำดับ tie ด้วย lexicographic order ของ member ทำให้อันดับดูสุ่มถ้าไม่ได้ตั้งใจออกแบบไว้ ควรผูก timestamp เข้ากับคะแนนเพื่อ break tie อย่างมีความหมาย
  • ลืมว่า ZREVRANK เป็นแบบ 0-based — อันดับหนึ่งคืนค่า 0 ไม่ใช่ 1 ถ้าแสดงผลตรงๆ ให้ผู้ใช้ต้องบวก 1 เอง มิฉะนั้นอันดับจะเพี้ยนไปหนึ่งตำแหน่งเสมอ
  • ใช้ ZADD แทน ZINCRBY เมื่อต้องการมอบแต้มเพิ่มZADD ตั้งคะแนนใหม่ทับของเดิมทันที ไม่ใช่บวกเพิ่ม ถ้าใช้ผิดจะทำให้คะแนนเดิมหายไปแทนที่จะสะสม

💡 ตัวอย่างจากของจริง

เกมออนไลน์หลายค่าย — ใช้ Redis Sorted Set เป็น backend ของ leaderboard real-time เพราะ ZADD/ZREVRANGE/ZREVRANK ให้ ranking ที่อัปเดตทันทีโดยไม่ต้อง batch job มาคำนวณใหม่

Reddit-style ranking — ใช้แนวคิดเดียวกันกับ Sorted Set โดยผูก “hot score” (คำนวณจาก vote และเวลา) เป็น score เพื่อจัดเรียงโพสต์แบบ real-time โดยไม่ต้อง query ฐานข้อมูลหลักซ้ำ

คำสั่งใดดึงผู้เล่น 10 อันดับแรกตามลำดับคะแนนจากมากไปน้อย?
ZREVRANK คืนค่า 0 สำหรับผู้เล่นใด?
เหตุใดจึงใช้ ZINCRBY แทน ZADD เมื่อมอบแต้ม?
ถ้า carol มีคะแนน 1750 และรัน ZINCRBY lb 2500 carol คะแนนใหม่คือเท่าไร?