Implementing a Real‑Time Leaderboard with Redis and PHP
This article explains how to build a real‑time game leaderboard using Redis sorted sets, covering ranking categories, composite scoring formulas, dynamic updates, efficient data retrieval with pipelines, and provides a complete PHP class implementation.
1. Introduction
Recently a real‑time leaderboard was added to a mobile tank game, featuring global ranking, individual player lookup, and dual‑dimension sorting. The data volume ranges from 10,000 to 500,000 entries.
2. Leaderboard Categories
Rankings are divided by entity type:
Characters
Guilds (Clans)
Tanks
The game includes multiple tank types (light, heavy, etc.) and allows players to join a guild.
Each category can be further refined, for example:
- Combat Power Ranking (1. Combat 2. Level)
- Personal Arena Ranking (1. Arena Rank)
- Tower Ranking (1. Floor 2. Completion Time)
- Prestige Ranking (1. Prestige Value 2. Level) - Guild Level Ranking (1. Guild Level 2. Total Combat Power) - Tank Rankings: Medium, Heavy, AT‑Gun, Self‑Propelled ArtilleryValues in parentheses indicate sorting dimensions.
3. 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 (two‑dimensional) sorting
Dynamic updates of ranking data
Efficient retrieval of the leaderboard
4. Implementing Composite Sorting
Redis Sorted Sets (ZSET) are used. Adding a member with a score is done via ZADD key score member [score member] ... . By default, equal scores are ordered by member name.
4.1 Level Ranking
Score is calculated as Score = Level * 10000000000 + CombatPower . Levels range from 1‑100 and combat power up to 100,000,000, fitting comfortably within Redis 64‑bit integer limits.
4.2 Tower Ranking
Ranking by floor number and earlier completion time uses the formula Score = Floor * 10^N + (ReferenceTime - CompletionTimestamp) , with a far future reference time (e.g., 2050‑01‑01 00:00:00, timestamp 2524579200) and N=10 to keep ten digits for the relative time.
4.3 Tank Ranking
Tank rankings use a composite member ID formed by uid_tankId , which must be handled carefully.
5. Dynamic Data Updates
Only the UID is stored in the sorted set; dynamic player data (name, avatar, guild, VIP level, etc.) is kept in a Redis hash as a JSON string. When a player's level or combat power changes, the ZSET score is updated, and when display data changes, the hash entry is modified.
-- s1:rank:user:lv ---------- zset --
| playerId1 | score1
| ...
| playerIdN | scoreN
------------------------------------- -- s1:rank:user:lv:item ------- string --
| playerId1 | player JSON data
| ...
| playerIdN |
-----------------------------------------Updating a player's score and data is done via a Redis pipeline to minimize round‑trips.
6. Retrieving the Leaderboard
To get the top 100 players of the level ranking:
ZRANGE key start stop [WITHSCORES]Steps:
Use zRange("s1:rank:user:lv", 0, 99) to obtain the top 100 UIDs.
For each UID, call hGet("s1:rank:user:lv:item", $uid) to fetch the JSON data.
Using a pipeline reduces the number of network calls from up to 101 to just 2, greatly improving performance.
7. Code Example (PHP)
// $redis
$redis->multi(Redis::PIPELINE);
foreach ($uids as $uid) {
$redis->hGet($userDataKey, $uid);
}
$resp = $redis->exec(); // results returned as an array redis = $redis;
$this->rankKey = $rankKey;
$this->rankItemKey = $rankItemKey;
$this->sortFlag = SORT_DESC;
}
public function getRedis()
{
return $this->redis;
}
public function setRedis($redis)
{
$this->redis = $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 simple class demonstrates how to maintain a leaderboard, with score calculation handled externally.
IT Xianyu
We share common IT technologies (Java, Web, SQL, etc.) and practical applications of emerging software development techniques. New articles are posted daily. Follow IT Xianyu to stay ahead in tech. The IT Xianyu series is being regularly updated.
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.