How to Build a Real‑Time Game Leaderboard with Redis Sorted Sets
This article explains how to implement a real‑time ranking system for a mobile tank game using Redis sorted sets, covering ranking categories, composite scoring formulas, dynamic updates, efficient data retrieval with pipelines, and a complete PHP class example.
1. Introduction
Recently a real‑time leaderboard was implemented for a mobile game. Main features include real‑time global ranking, individual player rank lookup, and support for two‑dimensional sorting. Data volume ranges from 10,000 to 500,000 records.
2. Ranking Categories
Rankings are divided by entity type:
Character
Guild (Clan)
Tank
Examples of specific leaderboards:
- Combat Power Leaderboard (1. Combat 2. Level)
- Personal Arena Leaderboard (1. Arena Rank)
- Tower Leaderboard (1. Floor 2. Completion Time)
- Prestige Leaderboard (1. Prestige 2. Level)Guild Level Leaderboard (1. Guild Level 2. Total Combat Power)
- Medium Tank
- Heavy Tank
- Anti‑Tank Gun
- Self‑Propelled Artillery3. Design Idea
Because of the real‑time requirement, Redis is chosen to implement the leaderboard. The article references the Redis online manual for command details.
Key problems to solve:
Composite sorting (2‑dimensional)
Dynamic updates of ranking data
How to retrieve the leaderboard
4. Implementing Composite Sorting
Redis Sorted Sets are used. Adding members with scores is done via ZADD key score member. By default, equal scores are ordered by member lexical order.
4.1 Level Leaderboard
Score formula: score = level * 10000000000 + combatPower . Player level ranges 1‑100, combat power up to 100,000,000, giving a 13‑digit score, well within Redis 64‑bit integer limits.
4.2 Tower Leaderboard
Requires higher floor first, then earlier completion time. Score is calculated as score = floor * 10^N + (baseTime - completionTimestamp) , where baseTime is a far future timestamp (e.g., 2050‑01‑01 00:00:00 = 2524579200). N is set to 10 to keep 10 digits for the relative time.
4.3 Tank Leaderboard
Member IDs are composite keys formed by uid_tankId , which must be handled carefully.
5. Dynamic Updates of Ranking Data
Using the level leaderboard as an example, the sorted set stores uid as member and the composite score as score. Player details (name, avatar, guild, VIP level, etc.) are stored separately in a Redis hash as JSON strings.
-- s1:rank:user:lv ---------- zset --
| playerId1 | score1
| ... | ...
| playerIdN | scoreN
-------------------------------------The hash key s1:rank:user:lv:item holds the JSON data for each player.
When a player's level or combat power changes, the composite score in the sorted set is updated. When other display data changes, the JSON in the hash is updated.
6. Retrieving the Leaderboard
To get the top 100 players from the level leaderboard: ZRANGE key start stop [WITHSCORES] Steps: zRange("s1:rank:user:lv", 0, 99) – obtain the top 100 uids. hGet("s1:rank:user:lv:item", $uid) – fetch each player's JSON data.
Executing 100 individual hGet commands can be costly. Using Redis pipelines reduces the round‑trip to two commands.
// PHP example using pipeline
$redis->multi(Redis::PIPELINE);
foreach ($uids as $uid) {
$redis->hGet($userDataKey, $uid);
}
$resp = $redis->exec(); // results returned as an arrayPipeline batches commands without guaranteeing atomicity, whereas MULTI provides atomic transactions.
7. Show The Code
<?php
class RankList
{
protected $rankKey;
protected $rankItemKey;
protected $sortFlag;
protected $redis;
public function __construct($redis, $rankKey, $rankItemKey, $sortFlag = SORT_DESC)
{
$this->redis = $redis;
$this->rankKey = $rankKey;
$this->rankItemKey = $rankItemKey;
$this->sortFlag = SORT_DESC;
}
public function getRedis()
{
return $this->redis;
}
public function updateScore($uid, $score = null, $rankItem = null)
{
if (is_null($score) && is_null($rankItem)) {
return;
}
$redis = $this->getRedis()->multi(Redis::PIPELINE);
if (!is_null($score)) {
$redis->zAdd($this->rankKey, $score, $uid);
}
if (!is_null($rankItem)) {
$redis->hSet($this->rankItemKey, $uid, $rankItem);
}
$redis->exec();
}
public function getRank($uid)
{
$redis = $this->getRedis()->multi(Redis::PIPELINE);
if ($this->sortFlag == SORT_DESC) {
$redis->zRevRank($this->rankKey, $uid);
} else {
$redis->zRank($this->rankKey, $uid);
}
$redis->hGet($this->rankItemKey, $uid);
list($rank, $rankItem) = $redis->exec();
return [$rank === false ? -1 : $rank + 1, $rankItem];
}
public function del($uid)
{
$redis = $this->getRedis()->multi(Redis::PIPELINE);
$redis->zRem($this->rankKey, $uid);
$redis->hDel($this->rankItemKey, $uid);
$redis->exec();
}
public function getList($topN, $withRankItem = false)
{
$redis = $this->getRedis();
if ($this->sortFlag === SORT_DESC) {
$list = $redis->zRevRange($this->rankKey, 0, $topN);
} else {
$list = $redis->zRange($this->rankKey, 0, $topN);
}
$rankItems = [];
if (!empty($list) && $withRankItem) {
$redis->multi(Redis::PIPELINE);
foreach ($list as $uid) {
$redis->hGet($this->rankItemKey, $uid);
}
$rankItems = $redis->exec();
}
return [$list, $rankItems];
}
public function flush()
{
$redis = $this->getRedis();
$redis->del($this->rankKey, $this->rankItemKey);
}
}This provides a simple yet complete implementation of a leaderboard where the score calculation is handled externally.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
