用 Redis 搞定游戲中的實(shí)時(shí)排行榜,附源碼!
1. 前言
實(shí)時(shí)全服排名 可查詢單個(gè)玩家排名 支持雙維排序
2. 排行榜分類
角色 軍團(tuán)(公會(huì)) 坦克
該項(xiàng)目是個(gè)坦克手游, 大致情況是每個(gè)角色有N輛坦克, 坦克分為多種類型(輕型, 重型等), 玩家可加入一個(gè)軍團(tuán)(公會(huì)).
角色
- 戰(zhàn)斗力排行榜(1. 戰(zhàn)斗 2.等級(jí))
- 個(gè)人競(jìng)技場(chǎng)排行榜(1. 競(jìng)技場(chǎng)排名)
- 通天塔排行榜(1.通天塔層數(shù) 2.通關(guān)時(shí)間)
- 威望排行榜(1.威望值 2.等級(jí))
軍團(tuán)(公會(huì))
- 軍團(tuán)等級(jí)排行榜(1.軍團(tuán)等級(jí) 2.軍團(tuán)總戰(zhàn)斗力)
坦克(1.坦克戰(zhàn)斗力 2.坦克等級(jí))
- 中型
- 重型
- 反坦克炮
- 自行火炮
↑ 括號(hào)內(nèi)為排序維度
3. 思路
復(fù)合排序(2維) 排名數(shù)據(jù)的動(dòng)態(tài)更新 如何取排行榜
4. 實(shí)現(xiàn) 復(fù)合排序
添加 成員-積分 的操作是通過(guò)Redis的zAdd操作 ZADD key score member [[score member] [score member] ...]
4.1 等級(jí)排行榜
游戲中玩家等級(jí)范圍是1~100, 戰(zhàn)力范圍0~100000000.
有序集合的score取值是是64位整數(shù)值或雙精度浮點(diǎn)數(shù), 最大表示值是 9223372036854775807, 即能完整表示 18位 數(shù)值,因此用于此處的 13位score 綽綽有余.
4.2 通天塔排行榜
很明顯的, 通關(guān)時(shí)間越近(大), 則 基準(zhǔn)時(shí)間 - 通關(guān)時(shí)間 值越小, 符合該排行榜要求.
分?jǐn)?shù) = 層數(shù)_ 10^N + (2524579200 - 通過(guò)時(shí)間戳)述分?jǐn)?shù)公式中, N取10, 即保留10位數(shù)的相對(duì)時(shí)間.4.3 坦克排行榜
這點(diǎn)是需要注意的.
5. 排名數(shù)據(jù)的動(dòng)態(tài)更新
角色名 Uid 戰(zhàn)斗力 頭像 所屬公會(huì)名 VIP等級(jí)
用于存儲(chǔ)玩家等級(jí)排行榜有序集合如下
-- s1:rank:user:lv ---------- zset --
| 玩家id1 | score1
| ...
| 玩家idN | scoreN
-------------------------------------
member為角色uid, score為復(fù)合積分
-- s1:rank:user:lv:item ------- string --
| 玩家id1 | 玩家數(shù)據(jù)的json串
| ...
| 玩家idN |
-----------------------------------------
s1:rank:user:lv 該玩家的復(fù)合積分即可. 若玩家其他數(shù)據(jù)(用于排行榜顯示)有變化, 則也相應(yīng)地修改其在 s1:rank:user:lv:item 中的數(shù)據(jù)json串.6. 取排行榜
需要從 `s1:rank:user:lv` 中取出前100名玩家, 及其數(shù)據(jù).
[`ZRANGE key start stop [WITHSCORES]`](http://redisdoc.com/sorted_set/zrange.html)
時(shí)間復(fù)雜度: O(log(N)+M), N 為有序集的基數(shù),而 M 為結(jié)果集的基數(shù)。
zRange("s1:rank:user:lv", 0, 99)獲取前100個(gè)玩家的uidhGet("s1:rank:user:lv:item", $uid)逐個(gè)獲取前100個(gè)玩家的具體信息
zRange時(shí)間復(fù)雜度是O(log(N)+M) , N 為有序集的基數(shù),而 M 為結(jié)果集的基數(shù) hGet時(shí)間復(fù)雜度是 O(1) 步驟2由于最多需要獲取100個(gè)玩家數(shù)據(jù), 因此需要執(zhí)行100次, 此處的執(zhí)行時(shí)間還得加上與redis通信的時(shí)間, 即使單次只要1MS, 最多也需要100MS.
借助Redis的Pipeline, 整個(gè)過(guò)程可以降低到只與redis通信2次, 大大降低了所耗時(shí)間.
// $redis
$redis->multi(Redis::PIPELINE);
foreach ($uids as $uid) {
$redis->hGet($userDataKey, $uid);
}
$resp = $redis->exec(); // 結(jié)果會(huì)一次性以數(shù)組形式返回
參考: https://blog.csdn.net/weixin_...
Pipeline 管線化, 是在客戶端將命令緩沖, 因此可以將多條請(qǐng)求合并為一條發(fā)送給服務(wù)端. 但是 不保證原子性 !!! Multi 事務(wù), 是在服務(wù)端將命令緩沖, 每個(gè)命令都會(huì)發(fā)起一次請(qǐng)求, 保證原子性 , 同時(shí)可配合 WATCH實(shí)現(xiàn)事務(wù), 用途是不一樣的.
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;
}
/**
* 新增/更新單人排行數(shù)據(jù)
* @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();
}
/**
* 獲取單人排行
* @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];
}
/**
* 移除單人
* @param $uid
*/
public function del($uid)
{
$redis = $this->getRedis()->multi(Redis::PIPELINE);
$redis->zRem($this->rankKey, $uid);
$redis->hDel($this->rankItemKey, $uid);
$redis->exec();
}
/**
* 獲取排行榜前N個(gè)
* @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];
}
/**
* 清除排行榜
*/
public function flush()
{
$redis = $this->getRedis();
$redis->del($this->rankKey, $this->rankItemKey);
}
}
好書推薦
《深入理解計(jì)算機(jī)系統(tǒng)》
理解計(jì)算機(jī)系統(tǒng)首首選書目, 10余萬(wàn)程序員的共同選擇。卡內(nèi)基-梅隆、北京大學(xué)、清華大學(xué)、上海交通大學(xué)等國(guó)內(nèi)外眾多知名高校選用指定教材。從程序員視角全面剖析的實(shí)現(xiàn)細(xì)節(jié),使讀者深刻理解程序的行為,將所有計(jì)算機(jī)系統(tǒng)的相關(guān)知識(shí)融會(huì)貫通。每位不只想一直做碼農(nóng)的IT從業(yè)者都該仔細(xì)閱讀!
評(píng)論
圖片
表情

