TT Lab
Get started
Learn Learning paths Courses

Redis and Caching

Implementing and Verifying a Leaderboard With ZSet

Continue in TT Lab

Goal

With a single sorted set, implement the full requirements of a real-time leaderboard — top N, individual rank, pagination, tie rule, rankings by period, and combined ranking — and confirm the lookup performance in numbers.

Why it matters

Ranking is one of the things a relational DB does worst. The top 10 is easy with an index, but "my rank" requires counting everyone with a higher score than mine, so a single lookup for a low-ranked user scans 990,000 rows. On top of that, that query arrives on every request from every user, and at the same time the score-update writes converge on the same index. A sorted set maintains a skip list and a hash table together, making both rank lookups and range lookups O(log N). Whether it is 1 million or 10 million users, it takes milliseconds. The part to pay particular attention to in this lab is the tie handling in step 6. Redis's default tie rule is the lexicographic order of the member string, but the product requirement is almost always "whoever achieved it first ranks higher". This technique of folding score and time into a single real number is something you will use every time you build a leaderboard.

Steps

  1. With /root/lb/load.py, read /opt/fixtures/rd/plays.csv (header user_id,points,ts) and put each user's cumulative score into lb:global. ZCARD lb:global must be 200.
  2. Write the top 10 with their scores to /root/lb/top10.txt as 10 lines in the format <순위> <user_id> <점수> (rank, user ID, score). Rank 1 is the first line.
  3. In /root/lb/myrank.txt, write the information for user u042 as a single line: user=u042 rank=<1부터> score=<점수> (rank counts from 1, followed by the score).
  4. With /root/lb/incr.py, atomically add 50 points to u042. The source must not have a pattern that reads with ZSCORE and writes with ZADD. After running it, the score must have increased by exactly 50.
  5. In /root/lb/page.txt, write the 10 users from rank 100 to rank 109 in the format <순위> <user_id> (rank, user ID). The rank on the first line must be 100.
  6. With /root/lb/tie.py, create lb:tie. Fold the score as points + (1 - ts / 1e10). Make the 5 tied users in /opt/fixtures/rd/ties.csv rank at the top in ascending ts order, and write the result to /root/lb/tie.txt as 5 lines.
  7. Create lb:weekly:2026-W34 and set its TTL to 604800 or less. Create the combined key with ZUNIONSTORE lb:combined 2 lb:global lb:weekly:2026-W34 WEIGHTS 1 2. ZCARD lb:combined must be at least 200.
  8. Start /root/lb/api.py on 127.0.0.1:8150. GET /rank/u042 returns {"user":"u042","rank":<정수>,"score":<수>,"top3":[...]} (rank is an integer, score is a number). In /root/lb/bench.txt, write queries=1000 total_ms=<정수> avg_ms=<수> (integer, number), and avg_ms must be under 5.

Notes

Fill the leaderboard from play records

With /root/lb/load.py, read /opt/fixtures/rd/plays.csv (header user_id,points,ts) and put each user's cumulative score into lb:global. ZCARD lb:global must be 200.

Read /opt/fixtures/rd/plays.csv and put each user's score into the sorted set. The same user appears many times — you must accumulate.

Get the top 10

Write the top 10 with their scores to /root/lb/top10.txt as 10 lines in the format <순위> <user_id> <점수> (rank, user ID, score). Rank 1 is the first line.

There is a range command that fetches from the highest score. You also need to receive the scores to write them to the file.

Get a specific user's rank

In /root/lb/myrank.txt, write the information for user u042 as a single line: user=u042 rank=<1부터> score=<점수> (rank counts from 1, followed by the score).

The rank command counts from 0. When showing it to a person, you must add 1.

Accumulate scores atomically

With /root/lb/incr.py, atomically add 50 points to u042. The source must not have a pattern that reads with ZSCORE and writes with ZADD. After running it, the score must have increased by exactly 50.

If you read, add, and write, values are lost under concurrent requests. There is a way to add with a single command.

Get a page in the 100s of ranks

In /root/lb/page.txt, write the 10 users from rank 100 to rank 109 in the format <순위> <user_id> (rank, user ID). The rank on the first line must be 100.

Calculate the start and end indexes of the range command. Pay attention to which index rank 100 is.

Sort ties by who achieved it first

With /root/lb/tie.py, create lb:tie. Fold the score as points + (1 - ts / 1e10). Make the 5 tied users in /opt/fixtures/rd/ties.csv rank at the top in ascending ts order, and write the result to /root/lb/tie.txt as 5 lines.

Fold the score and the time into a single real number. The fractional part must be between 0 and 1 so that it does not cross the integer boundary.

Build the weekly ranking and the combined ranking

Create lb:weekly:2026-W34 and set its TTL to 604800 or less. Create the combined key with ZUNIONSTORE lb:combined 2 lb:global lb:weekly:2026-W34 WEIGHTS 1 2. ZCARD lb:combined must be at least 200.

If you put the period in the key name and set an expiration, cleanup is automatic. There is a command that merges two sets with weights.

Start the rank lookup API and measure performance

Start /root/lb/api.py on 127.0.0.1:8150. GET /rank/u042 returns {"user":"u042","rank":<정수>,"score":<수>,"top3":[...]} (rank is an integer, score is a number). In /root/lb/bench.txt, write queries=1000 total_ms=<정수> avg_ms=<수> (integer, number), and avg_ms must be under 5.

Tie together what you built earlier into a single endpoint. If you measure the time of 1,000 lookups, the numbers show why you use this instead of the DB.