Implementation of real-time leaderboard based on redis game

Time:2019-12-9

[TOC]

Last modified: 18:18:37, June 4, 2019

1. Preface

Recently, a real-time ranking function has been implemented for the project (mobile game), with main features:

  • Real time full service ranking
  • Single player ranking can be queried
  • Support two-dimensional sorting

The amount of data is not large, roughly between 1W and 50W (the number of roles in a single service will increase with the opening and closing of a service)

2. Ranking classification

According to the type of ranking subject, it is mainly divided into:

  • role
  • Legion (guild)
  • Tank

This project is a tank tour. The general situation is that each character has n tanks. The tanks are divided into various types (light, heavy, etc.), and players can join a corps (guild)

It can be further divided into:

  • role

    -Rank ranking (1. Rank 2. Combat power)
    -Battle effectiveness ranking (1. Battle 2. Level)
    -Personal arena ranking (1. Arena ranking)
    -Ranking list of Tongtian tower (1. Number of floors of Tongtian tower 2. Customs clearance time)
    -Prestige ranking (1. Prestige value 2. Rank)
  • Legion (guild)

    -Battle effectiveness ranking of the Corps (1. Total combat effectiveness of the corps 2. Rank of the Corps)
    -Rank list of corps (1. Rank of corps 2. Total combat effectiveness of corps)
  • Tank (1. Tank combat power 2. Tank level)

    -Battle effectiveness ranking of light tanks
    Medium size
    - heavy duty
    -Antitank gun
    -Self propelled gun

↑ sorting dimension in brackets

3. way of thinking

Based on the consideration of real-time, we decided to use redis to implement the ranking

If the redis command used in this article is unclear, please refer to the redis online manual


The following problems need to be solved:

  1. Composite sorting (2D)
  2. Dynamic update of ranking data
  3. How to get the leaderboard

4. Implement composite sorting

Redis based leaderboards mainly use redis’s sorted set to achieve

The operation of adding members and points is through the zadd operation of redis
ZADD key score member [[score member] [score member] ...]

By default, if the scores are the same, they are sorted in the dictionary order of members

4.1 ranking

First of all, take the rank ranking (1. Rank 2. Combat power) as an example. This ranking requires players of the same level, and those with high combat power rank first. Therefore, the score can be determined as:
Score = level * 100000000000 + combat effectiveness

The level range of players in the game is 1-100, and the combat power range is 0-100000000

In this design, the value range reserved for combat effectiveness is 10 digits, and the level is 3 digits, so the maximum value is13 place.
The score value of ordered set is 64 bit integer value or double precision floating-point number, and the maximum representation value is 9223372036854775807, that is to say, it can be expressed completely18 placeValue, so the 13 digit score used here is more than enough

4.2 ranking of Tongtian tower

Another typical leaderboard isRanking list of Tongtian tower (1. Number of floors 2. Customs clearance time), the ranking list requires that those with the same number of floors and earlier clearance time are preferred

Due to the priority of earlier clearance time, it can not be as direct as beforeScore = number of floors * 10 ^ n + customs clearance time.

We can convert the clearance time into a relative time, i.eScore = number of floors * 10 ^ n + (base time clearance time)
Obviously, the closer the clearance time is, theBase time – customs clearance timeThe smaller the value is, the better it will be

The choice of reference time randomly chooses a far time2050-01-01 00:00:00, corresponding time stamp 2524579200

Final,*Fraction = number of layers10 ^ n + (2524579200 – via timestamp)
In the above fraction formula, n is taken as 10, that is, the relative time to retain 10 digits

4.3 tank ranking

The difference between tank leaderboards and other leaderboards is that the member in the ordered set is a composite ID, which is determined byuid_tankIdForm.
This point needs attention

5. Dynamic update of ranking data

Take the ranking list as an example

The data required for ranking displayed in the game includes (but is not limited to):

  • Role name
  • Uid
  • Combat effectiveness
  • Head portrait
  • Guild name
  • VIP level

Because these data will change dynamically in the process of the game, it is not considered to store these data directly as members in an ordered set here
It is used to store the orderly collection of player level leaderboards as follows

-- s1:rank:user:lv ---------- zset --
|Player Id1 | Score1
| ...
|Player IDN | scoren
-------------------------------------

Member is the role uid, and score is the composite integral

Using hash to store players’ dynamic data (JSON)

-- s1:rank:user:lv:item ------- string --
|Player Id1 | JSON string of player data
| ...
|Player IDN| 
-----------------------------------------

With this scheme, you only need to add the character to the ranking list when the player creates the character, and then when the playerLevel combat effectivenessReal time update when changes occurs1:rank:user:lvIf the player’s other data (used for ranking display) is changed, then the player’s composite points will be modified accordinglys1:rank:user:lv:itemData JSON string in

6. Ranking

Take the ranking list as an example

objective

You need to retrieve the top 100 players and their data from 'S1: Rank: user: LV'

Redis command used

[`ZRANGE key start stop [WITHSCORES]`](http://redisdoc.com/sorted_set/zrange.html)
Time complexity: O (log (n) + m), n is the cardinality of the ordered set, and M is the cardinality of the result set.

step

  1. zRange("s1:rank:user:lv", 0, 99)Get UIDs of the top 100 players
  2. hGet("s1:rank:user:lv:item", $uid)Get the specific information of the top 100 players one by one

The above step 2 can be optimized in the specific implementation

Analysis

  • Zrange time complexity is O (log (n) + m), n is the cardinality of ordered set, and M is the cardinality of result set
  • Hget time complexity is O (1)
  • Step 2 since you need to obtain 100 player data at most, you need to execute 100 times. The execution time here must be added with the time of communication with redis. Even if the single time is only 1ms, you need 100ms at most

Solve

  • With the help of redis pipeline, the whole process can be reduced to only communicate with redis twice, greatly reducing the time consumed

The following example is the PHP code

// $redis
$redis->multi(Redis::PIPELINE);
foreach ($uids as $uid) {
    $redis->hGet($userDataKey, $uid);
}
$resp = $redis - > exec(); // the result will be returned as an array once

Tip: the difference between pipeline and multi mode

Reference: https://blog.csdn.net/weixin

  • Pipeline pipeline is used to buffer commands on the client side, so multiple requests can be combined into one and sent to the serverNo guarantee of atomicity!!!
  • Multi transaction is to buffer commands on the server side, and each command will initiate a request,Guarantee atomicityAt the same time, it can cooperate withWATCHThe purpose of implementing a transaction is different

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;
    }

    /**
     * @return Redis
     */
    public function getRedis()
    {
        return $this->redis;
    }

    /**
     * @param Redis $redis
     */
    public function setRedis($redis)
    {
        $this->redis = $redis;
    }

    /**
     *Add / update single ranking data
     * @param string|int $uid
     * @param null|double $score
     * @param null|string $rankItem
     */
    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();
    }

    /**
     *Get single ranking
     * @param string|int $uid
     * @return array
     */
    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];
    }

    /**
     *Remove single person
     * @param $uid
     */
    public function del($uid)
    {
        $redis = $this->getRedis()->multi(Redis::PIPELINE);
        $redis->zRem($this->rankKey, $uid);
        $redis->hDel($this->rankItemKey, $uid);
        $redis->exec();
    }

    /**
     *Get top n
     * @param $topN
     * @param bool $withRankItem
     * @return array
     */
    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];
    }

    /**
     *Clear Leaderboard
     */
    public function flush()
    {
        $redis = $this->getRedis();
        $redis->del($this->rankKey, $this->rankItemKey);
    }
}

This is the simplest implementation of a leaderboard. The integral calculation of leaderboards is handled by the external