原文:segmentfault.com/a/1190000019139010

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命令

[`ZRANGE key start stop [WITHSCORES]`](http://redisdoc.com/sorted_set/zrange.html)时间复杂度: O(log(N)+M), N 为有序集的基数,而 M 为结果集的基数。

步骤

  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 模式的区别

参考:  https://blog.csdn.net/weixin_...

  • Pipeline 管线化, 是在客户端将命令缓冲, 因此可以将多条请求合并为一条发送给服务端. 但是 不保证原子性 !!!

  • Multi 事务, 是在服务端将命令缓冲, 每个命令都会发起一次请求, 保证原子性 , 同时可配合 WATCH 实现事务, 用途是不一样的.

7. Show The Code

<?phpclass 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*/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);}}

这就是一个排行榜最简单的实现了, 排行项的积分计算由外部自行处理.

用 Redis 搞定游戏中的实时排行榜,附源码!相关推荐

  1. python贪吃蛇源码_Python:游戏:贪吃蛇(附源码)

    Python:游戏:贪吃蛇(附源码) 发布时间:2018-09-05 09:59, 浏览次数:1295 , 标签: Python 贪吃蛇是个非常简单的游戏,适合练手. 首先分析一下这个游戏 1.蛇怎么 ...

  2. C++小游戏笔记——射击小行星(附源码)

    C++小游戏笔记--射击小行星(附源码) 游戏展示图 一.飞船 1.飞船的绘制 2.飞船的角度 二.小行星 1.小行星的绘制 2."凹凸不平"效果的形成 3.小行星的分裂 三.子弹 ...

  3. 暑期Android游戏开发——小兔子跳铃铛(附源码)

    暑期Android游戏开发--小兔子跳铃铛(附源码) 一. 背景说明 我在南京的一所高校学习软件工程.学院里每年会举行一次"创新杯"软件比赛,鼓励同学自主学习和创新.我和几个好兄弟 ...

  4. 美!视差滚动在图片滑块中的应用【附源码下载】

    视差滚动(Parallax Scrolling)已经被广泛应用于网页设计中,这种技术能够让原本平面的网页界面产生动感的立体效果.下面分享的这个图片滑块效果是难得一见的结合视差滚动的例子,之前的文章给大 ...

  5. 美!视差滚动特效在图片滑块中的应用【附源码下载】

    视差滚动(Parallax Scrolling)已经被广泛应用于网页设计中,这种技术能够让原本平面的网页界面产生动感的立体效果.下面分享的这个图片滑块效果是难得一见的结合视差滚动的例子,之前的文章给大 ...

  6. 利用C++打造双人对战的五子棋游戏,界面新颖「附源码」

    五子棋游戏的历史可谓源远流长,是一款老少皆宜的两人对弈纯策略游戏,讲究的是有攻有守的五子棋技巧,玩法简单易上手,五个棋子连成一线就可获胜.单机五子棋,双人五子棋,好友联机对战模式-你想要的都能在这里找 ...

  7. [原创]游戏中的实时排行榜实现

    1. 前言 2. 排行榜分类 3. 思路 4. 实现 复合排序 4.1 等级排行榜 4.2 通天塔排行榜 4.3 坦克排行榜 5. 排名数据的动态更新 6. 取排行榜 7. Show The Code ...

  8. linux俄罗斯方块源程序,《俄罗斯方块游戏》项目实作【附源码】

    此游戏的界面分为3种,即游戏前界面.游戏时界面和游戏后界面 1.游戏前界面:即在游戏开始之前为用户呈现的界面,其中包括1张背景图和3个按钮,3个按钮分别为"开始"."设置 ...

  9. 猜数游戏(实现) 后附源码

    亲爱的玩家,在初学C语言的阶段,您有没有预想过用C语言来实现一个猜数游戏呢? 如果有,请允许我来打开您的思路. 思路: 1.系统随机生成一个1--100的随机数,并在本轮游戏中不改变大小. 2.您可以 ...

最新文章

  1. CMake实战之安装测试和添加环境生成安装包
  2. python soup提取叶子标签_python 利用beautifulSoup提取页面多个标签的文本内容
  3. vue路由守卫判断用户是否登录,如果没登陆就跳转到登录
  4. 依赖注入底层反射原理_PHP基于反射机制实现自动依赖注入的方法详解_php技巧...
  5. 使用内存映射文件来共享数据
  6. asp.net 2.0中实现防盗链
  7. 三调业务摘要201709
  8. java中html网页转化成pdf(itext)
  9. 【UE4】UE4蓝图基础
  10. iOS面试题与核心基础之性能优化
  11. Storj:区块链在云存储上的应用
  12. 数据分析行业的发展前景怎么样? 未来关于十年数据分析行业的5种预测
  13. 数据挖掘十大经典算法--CART: 分类与回归树
  14. 2007年世界杀毒软件排行榜
  15. 自组织(竞争型)神经网络
  16. RISC-CPU设计(一):基本概念
  17. 可爱的python测试开发库(python测试开发工具库汇总)
  18. python3.6爬虫案例:爬取朝秀帮图片
  19. python代码实现文本编辑器删除空格,替换标点,英文大写转换功能
  20. android surfaceview截图 系统截图

热门文章

  1. 小白兔写话_小学二年级写话-我的小白兔
  2. 如何安装体验 Ubuntu on Windows
  3. Trie树合并 + SG函数 ---- BZOJ4730. Alice和Bob又在玩游戏(动态开点Trie 树上全局异或标记 + 合并 + 博弈论)
  4. 侠客风云传服务器维护,《侠客风云传online》4月20日服务器数据互通公告
  5. kafka-2.11-2.3.0版本配置文件参数详解_Kafka版本特性总结
  6. php判断数组不重复的元素,php从数组中随机选择若干不重复元素
  7. Selenium 2.0的由来及设计架构(二)
  8. 小微贷是美团的上坡之路?
  9. 创建响应式布局的优秀网格工具集锦《系列五》
  10. angularjs-ngModel 控制页面的宽度