⽤Redis搞定游戏中的实时排⾏榜(附源码)1. 前⾔
前段时间刚为项⽬(⼿游)实现了⼀个实时排⾏榜功能, 主要特性:
实时全服排名
可查询单个玩家排名
⽀持双维排序
数据量不⼤, ⼤致在 1W ~ 50W区间(开服, 合服会导致单个服⾓⾊数越来越多).
2. 排⾏榜分类
按照排⾏主体类型划分, 主要分为:
⾓⾊
军团(公会)
坦克
该项⽬是个坦克⼿游, ⼤致情况是每个⾓⾊有N辆坦克, 坦克分为多种类型(轻型, 重型等), 玩家可加⼊⼀个军团(公会).具体⼜可以细分为:
⾓⾊
- 战⽃⼒排⾏榜(1. 战⽃ 2.等级)
- 个⼈竞技场排⾏榜(1. 竞技场排名)
- 通天塔排⾏榜(1.通天塔层数 2.通关时间)
- 威望排⾏榜(1.威望值 2.等级)
军团(公会)
- 军团等级排⾏榜(1.军团等级 2.军团总战⽃⼒)
坦克(1.坦克战⽃⼒ 2.坦克等级)
- 中型
- 重型
- 反坦克炮
- ⾃⾏⽕炮
↑ 括号内为排序维度
3. 思路
基于实时性的考虑, 决定使⽤Redis来实现该排⾏榜.
⽂章中⽤到的redis命令如有不清楚的, 可参照 Redis在线⼿册 .
全国手游排行榜需要解决如下问题:
1. 复合排序(2维)
2. 排名数据的动态更新
3. 如何取排⾏榜
4. 实现复合排序
基于Redis的排⾏榜主要使⽤的是Redis的 有序集合(SortedSet)来实现
添加 成员-积分 的操作是通过Redis的zAdd操作
ZADD key score member [[score member] [score member] ...]
默认情况下, 若score相同, 则按照 member 的字典顺序排序.
4.1 等级排⾏榜
⾸先以等级排⾏榜(1. 等级 2.战⼒)为例, 该排⾏榜要求同等级的玩家, 战⽃⼒⼤的排在前. 因此分数可以定为:
分数 = 等级*10000000000 + 战⽃⼒
游戏中玩家等级范围是1~100, 战⼒范围0~100000000.
此处设计中为战⽃⼒保留的值范围是 10位数值, 等级是 3位数值, 因此最⼤数值为 13位 .
有序集合的score取值是是64位整数值或双精度浮点数, 最⼤表⽰值是 9223372036854775807, 即能完整表⽰ 18位 数值,因此⽤于此处的 13位score 绰绰有余.
4.2 通天塔排⾏榜
另⼀个典型排⾏榜是 通天塔排⾏榜(1.层数 2.通关时间) , 该排⾏榜要求通过层数相同的, 通关时间较早的优先.
由于要求的是通关时间较早的优先, 因此不能像之前那样直接 分数=层数*10^N+通关时间 .
我们可以将通关时间转换为⼀个相对时间, 即 分数=层数*10^N + (基准时间 - 通关时间)
很明显的, 通关时间越近(⼤), 则 基准时间 - 通关时间 值越⼩, 符合该排⾏榜要求.
基准时间的选择则随意选择了较远的⼀个时间 2050-01-01 00:00:00 , 对应时间戳2524579200
最终, 分数 = 层数_ 10^N + (2524579200 - 通过时间戳)述分数公式中, N取10, 即保留10位数的相对时间.
4.3 坦克排⾏榜
坦克排⾏榜跟其他排⾏榜的区别在于, 有序集合中的 member 是⼀个复合id, 由 uid_tankId 组成.
这点是需要注意的.
5. 排名数据的动态更新
还是以等级排⾏榜为例
游戏中展⽰的等级排⾏榜所需的数据包括(但不限于):
⾓⾊名
Uid
战⽃⼒
头像
所属公会名
VIP等级
由于这些数据在游戏过程中是会动态变更的, 因此此处不考虑将这些数据直接作为 member 存储在有序集合中.
⽤于存储玩家等级排⾏榜有序集合如下
-- s1:rank:user:lv ---------- zset --
| 玩家id1 | score1
| ...
| 玩家idN | scoreN
-------------------------------------
member为⾓⾊uid, score为复合积分
使⽤hash存储玩家的动态数据(json)
-- s1:rank:user:lv:item ------- string --
| 玩家id1 | 玩家数据的json串
| ...
| 玩家idN |
-----------------------------------------
使⽤这种⽅案, 只需要在玩家创建⾓⾊时, 将该⾓⾊添加到等级排⾏榜中, 后续则是当玩家 等级战⽃⼒ 发⽣变化时需实时更
新 s1:rank:user:lv 该玩家的复合积分即可. 若玩家其他数据(⽤于排⾏榜显⽰)有变化, 则也相应地修改其在 s1:rank:user:lv:item 中的数据json 串.
6. 取排⾏榜
依旧以等级排⾏榜为例.
⽬的
需要从 `s1:rank:user:lv` 中取出前100名玩家, 及其数据.
⽤到的Redis命令
步骤
1. zRange("s1:rank:user:lv", 0, 99) 获取前100个玩家的uid
2. hGet("s1:rank:user:lv:item", $uid) 逐个获取前100个玩家的具体信息
具体实现时, 上⾯的步骤2是可以优化的.
分析
zRange时间复杂度是O(log(N)+M) , N 为有序集的基数,⽽ M 为结果集的基数
hGet时间复杂度是 O(1)
步骤2由于最多需要获取100个玩家数据, 因此需要执⾏100次, 此处的执⾏时间还得加上与redis通信的时间, 即使单次只要1MS, 最多也需要100MS.
解决
借助Redis的Pipeline, 整个过程可以降低到只与redis通信2次, ⼤⼤降低了所耗时间.
以下⽰例为php代码
// $redis
$redis->multi(Redis::PIPELINE);
foreach ($uids as $uid) {
$redis->hGet($userDataKey, $uid);
}
$resp = $redis->exec(); // 结果会⼀次性以数组形式返回
Tip: Pipeline 与 Multi 模式的区别
Pipeline 管线化, 是在客户端将命令缓冲, 因此可以将多条请求合并为⼀条发送给服务端. 但是 不保证原⼦性
Multi 事务, 是在服务端将命令缓冲, 每个命令都会发起⼀次请求, 保证原⼦性 , 同时可配合 WATCH 实现事务, ⽤途是不⼀样的. 7. Show The Code
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;
}
/**
* 新增/更新单⼈排⾏数据
* @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个
* @param $topN
* @param bool $withRankItem
* @return array
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论