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
- Completion of Basic Data Types and Advanced Data Types
- A running Redis server and
redis-cli, or a Spring Boot project withspring-boot-starter-data-redis
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
# 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
# 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 WITHSCORESMultiple Leaderboards
Create separate keys for different game modes or time windows:
# 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 604800Pagination 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.
# 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:combinedIn Spring Boot:
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:
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:
score = (primary_score * 1000000) + secondary_scoreHow do I reset a weekly leaderboard?
Create a new key for each week and set an expiration:
ZADD leaderboard:week:43 1500 "alice"
EXPIRE leaderboard:week:43 1209600 # 14 daysOld 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.