Project: Leaderboard and Counter Systems

Introduction

Real-time rankings, daily签到 (check-ins), and unique visitor counts are classic problems that Redis solves with minimal code and exceptional performance. This chapter builds four systems—a game leaderboard, a签到 tracker with Bitmaps, a UV estimator with HyperLogLog, and a rate limiter with Strings. Each one demonstrates a different Redis data type in a realistic scenario.

Prerequisites

Real-Time Game Leaderboard

A Sorted Set is the natural choice for leaderboards. Scores update in constant time, and range queries return the top (or bottom) N players instantly.

Updating Scores

bash
# Seed the leaderboard
ZADD leaderboard:global 1500 "alice"
ZADD leaderboard:global 1200 "bob"
ZADD leaderboard:global 1800 "carol"
 
# Carol wins a match and gains 50 points
ZINCRBY leaderboard:global 50 "carol"
# "1850"

ZINCRBY is atomic. Two servers incrementing the same player's score at the same moment will never corrupt the data.

Retrieving Ranks

bash
# Top 10 players with scores
ZREVRANGE leaderboard:global 0 9 WITHSCORES
 
# Carol's global rank (0-based)
ZREVRANK leaderboard:global "carol"
# (integer) 0
 
# Players near Carol (ranks 0–4)
ZREVRANGE leaderboard:global 0 4 WITHSCORES
 
# Players ranked between 1000 and 1500 points
ZRANGEBYSCORE leaderboard:global 1000 1500 WITHSCORES

Multiple Leaderboards

Create separate keys for different game modes or time windows:

bash
# Per-mode leaderboards
ZADD leaderboard:mode:1v1 2000 "alice"
ZADD leaderboard:mode:team 1800 "alice"
 
# Weekly leaderboard with expiration
ZADD leaderboard:week:42 1500 "alice"
EXPIRE leaderboard:week:42 604800

Pagination and "Around Me"

Players often want to see their own position plus a few rivals above and below:

Check-In Tracking with Bitmaps

Track whether each user signs in each day using exactly one bit per user per day.

A website with one million daily active users consumes roughly 125 KB per day. A full year fits in about 45 MB.

Tip

User ID Requirements

Bitmaps work best when user IDs are compact integers starting near zero. If your IDs are sparse UUIDs, a Bitmap wastes space. In that case, use a Set or HyperLogLog instead.

UV Estimation with HyperLogLog

Counting unique visitors accurately across millions of page views would require an enormous Set. HyperLogLog estimates cardinality in a fixed 12 KB with about 0.81% error.

bash
# Each page view adds the user's ID
PFADD page:home:uv "user_a" "user_b" "user_c"
PFADD page:home:uv "user_d" "user_a"  # duplicate, ignored
 
# Estimate unique visitors
PFCOUNT page:home:uv
# (integer) 4
 
# Merge today's and yesterday's UV for a combined report
PFMERGE page:home:uv:combined page:home:uv page:home:uv:yesterday
PFCOUNT page:home:uv:combined

In Spring Boot:

java
public void recordVisitor(String page, String userId) {
    redisTemplate.opsForHyperLogLog()
        .add("page:" + page + ":uv", userId);
}
 
public long estimateUv(String page) {
    return redisTemplate.opsForHyperLogLog()
        .size("page:" + page + ":uv");
}

Rate Limiting with String Counters

A sliding-window rate limiter can be built with a String counter and expiration. This example limits each user to 100 API calls per minute.

This is a fixed-window limiter: the counter resets entirely after the TTL expires. For a smoother sliding-window limiter, use a Sorted Set with timestamps or a Lua script (see Transactions and Lua Scripts).

Combining Counters in a Dashboard

A real-time analytics dashboard might query several structures at once:

java
public DashboardStats getDashboard() {
    return new DashboardStats(
        redisTemplate.opsForValue().get("counter:orders:today"),
        redisTemplate.opsForHyperLogLog().size("site:uv:today"),
        redisTemplate.opsForZSet().reverseRangeWithScores(
            "leaderboard:global", 0, 4),
        redisTemplate.execute(
            (RedisCallback<Long>) conn ->
                conn.stringCommands().bitCount(
                    "checkin:today".getBytes()))
    );
}

FAQ

Can two players have the same score in a leaderboard?

Yes. Redis breaks ties lexicographically by member name. If you need deterministic tie-breaking by a secondary field (for example, faster completion time), encode both values into the score:

plaintext
score = (primary_score * 1000000) + secondary_score

How do I reset a weekly leaderboard?

Create a new key for each week and set an expiration:

bash
ZADD leaderboard:week:43 1500 "alice"
EXPIRE leaderboard:week:43 1209600  # 14 days

Old weeks fade from memory automatically.

Is HyperLogLog accurate enough for billing?

No. The 0.81% standard error is fine for analytics dashboards but unacceptable for financial calculations. Use a precise counter (Set or database table) when money is involved.

What happens if the rate limiter key expires between INCR and EXPIRE?

In the fixed-window example above, there is a tiny race window. For mission-critical rate limiting, use a Lua script to perform INCR + EXPIRE atomically, or adopt a token-bucket algorithm with Redis.

Can Bitmaps handle user IDs larger than a billion?

Technically yes, but memory usage scales with the highest bit offset, not the number of set bits. A single user with ID 4,000,000,000 allocates roughly 500 MB. Keep user ID sequences dense, or shard by ID range.