用Redis搞定游戏中的实时排行榜,附源码
1. 前言
前段时间刚为项目(手游)实现了一个实时排行榜功能, 主要特性: 实时全服排名 可查询单个玩家排名 支持双维排序
数据量不大 , 大致在 1W ~ 50W区间(开服, 合服会导致单个服角色数越来越多). 2. 排行榜分类
按照排行主体类型划分, 主要分为: 角色 军团(公会) 坦克
该项目是个坦克手游, 大致情况是每个角色有N辆坦克, 坦克分为多种类型(轻型, 重型等), 玩家可加入一个军团(公会).
具体又可以细分为: 角色 - 战斗力排行榜(1. 战斗 2.等级) - 个人竞技场排行榜(1. 竞技场排名) - 通天塔排行榜(1.通天塔层数 2.通关时间) - 威望排行榜(1.威望值 2.等级) 军团(公会) - 军团等级排行榜(1.军团等级 2.军团总战斗力) 坦克(1.坦克战斗力 2.坦克等级) - 中型 - 重型 - 反坦克炮 - 自行火炮
↑ 括号内为排序维度 3. 思路
基于实时性的考虑, 决定使用Redis来实现该排行榜.
文章中用到的redis命令如有不清楚的, 可参照 Redis在线手册 .
需要解决如下问题: 复合排序(2维) 排名数据的动态更新 如何取排行榜 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 为结果集的基数。
步骤 zRange("s1:rank:user:lv", 0, 99) 获取前100个玩家的uid 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 <?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; } /** * 新增/更新单人排行数据 * @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); } }
这就是一个排行榜最简单的实现了, 排行项的积分计算由外部自行处理.
转载自:https://segmentfault.com/a/1190000019139010
静下心玩游戏,这些手游别错过狂斩之刃狂斩之刃是一款魔幻风格的正统热血横版ACT手游,玩家将跟随第一主角视角下,一路斩妖伏魔,在微观世界中带给世界和平。时空旅人一场和自己的旅行,当你陷入困境,而唯一能帮助你的人
手游中的大作,这些游戏别错过鬼泣巅峰之战鬼泣巅峰之战是云畅游戏研发CAPCOM鬼泣官方团队深度参与共同打造的鬼泣正版手游!游戏继承了鬼泣自由灵活兼备技巧与策略的组合技能,华丽张扬无束缚的战斗风格,同时还以行业
这些别错过,这些手游让你春运路上不寂寞人气王漫画社你生活在一个漫画世界中,经营一家漫画摩天大楼,每一层楼都可以画不同种类的漫画,有搞笑恐怖魔幻热血职场纯爱等数十种工作室,层层解锁,你能想到的漫画类型这里都有每个工作室安
二月精品手游,这些你玩过了吗?银翼战机还记得电玩城的街机飞机射击游戏吗?立即下载银翼战机,让你随时随地,想打就打!致敬经典DanmakuSTG弹幕射击游戏玩法,添加战机天赋系统,收集系统,养成系统,战斗伴侣系统
休闲必玩,这些手游真有趣魔塔与英雄魔塔与英雄是一款外观随成长不断变化的魔幻题材格斗手游。六位勇士都有自己的背景故事和奋斗的目标,对抗梦魇,最终得到巨龙的验证。游戏场景宏大制作精良,极具特色的场景音效和打斗
无聊就来玩这些,武侠手游味道满满狂浪乾坤少年莫名穿越然而这地方怎么跟奇幻世界一样这仙禽异兽武功魔法太离谱了吧六大门派,群雄并起,神州动荡这百家争鸣的异世界,没有个趁手的武器就连一级小怪都打不过好在主角光环在身,穿
这些手游让你尽情享受周末老农种树领养荒山,种植树木,享受无所事事的快乐吧!种下一棵树,岁月自然发生。在俺们山上,一切都随时间流逝获得新卡片,解锁新剧情,一切都安静平稳御仙缘天下风云出我辈,一入江湖岁月催。
周末好游戏,远离无聊全靠它们圆桌誓约圆桌誓约讲述神裔与灾厄,相互争夺圣杯之血使得世界陷入混乱,是个被神所遗弃的黑暗时代。你将在梅林的指引下取得石中剑,并创建属于自己的骑士团,通过桂妮薇尔兰斯洛特等强大神裔的协
休闲好选择,这些手游让人沉迷上古王冠上古王冠是一款以西方魔幻冒险为背景,全3D建模的放置卡牌手游。运用次世代建模拟真场景GPU粒子特效和PBR渲染技术打磨出的高品质美术表现,高颜值的英雄个个有颜又有料,等你来
不输王者荣耀,这些手游款款不容错过龙族血统基于Unity引擎技术的支持,龙族血统在画面上呈现出精细到发丝的端游级质感特效,整个世界层次感丰富并栩栩如生。同时还加入了真实天空沙盒,甚至能够还原在3D御龙空战时,人物视
寻找简单快乐,这些游戏有手机就行像素小屋逃离不可思议之家这是一款像素画风的密室解谜逃脱类型的游戏,玩家们将会进入到一个不可思议的房间之中,在这里隐藏着很多的秘密,你也不是一个人孤军奋战,还有一只可爱的柴犬在这里陪
博德之门黑暗联盟现已登陆PC售价191。99元博德之门黑暗联盟是一款动作rpg游戏,今年早些时候,一个重新制作的版本出人意料地出现在PlayStation和Xbox主机上,发行商BlackIsleStudios也证实了这款游戏
请问王者里出装顺序有没有直接的影响?很多朋友都不认识合理出装,太对死顺序了,我看身边有一大把朋友,出装在外面都编辑好,然后在游戏就不懂更换,应对局面,反正不管三七二十一,都是照样出装顺序,明明对面没有法师,居然还出着
魔兽世界现在还好玩吗?AFK几年了,现在变成什么样子了?目前魔兽世界是8。0版本,又被俗称军团再临的7。4版本,因为有太多的内容相似了,从地图的声望势力副本的大秘境没日没夜肝神器装备靠脸的年代。而问主所问的,AFK几年了,现在变的怎么样
英雄联盟玩AD遇到对方切脆皮的英雄怎么办?作为强力的ADC,害怕的也仅仅只有四个!中单,打野,上单和辅助,无论敌我!可是,这四个位置,无论他们怎么玩,死的都是我大ADC!难道是因为AD就应该被人凌辱,欺负吗?好像是的诶,闭
如何克制刘备和李元芳的体系?关注胖哥,上分王者,不再成盒!我是月半电竞说,一个关注吃鸡和王者,但也喜欢发现新游戏的神秘玩家!刘备原来是射手,现在转型战士后也依旧走着原来是射手时的老路打野李元芳则一直是射手,自
王者荣耀匿光追影者特效展示,撞色蝴蝶炫酷,472史诗堪比传说2021王者荣耀KPL秋季总决赛将在本周六(25号)正式打响,总决赛的开启意味着全新KPL限定皮肤的上线,在官方公布新皮肤属于镜后,想必大家都很好奇该皮肤的局内实战特效如何?一起随
捣碎成球ASMR解压手游,用石臼捣碎一切,再揉成团子?原标题Wanna模拟捣碎手游Crushintoball带你体验ASMR的乐趣这款解压游戏不仅能帮你把所有物品都压碎,还能将它们再团成一个球你敢信?没错,就是这款由WannaPlay
英国周销量FIFA22圣诞周登顶新一周的英国实体游戏销量榜已经公布,欧美地区已进入了年底的圣诞假期,这周EA旗下足球模拟系列新作FIFA22上涨了72,示意本作已连续三周登顶周销量榜,看来英国玩家今年又是抱着足球
要打胜仗,装备先行,战舰世界军械库历久弥新说到军械库,要数威尼斯军械库的历史最悠久,始建于1104年,形成了涵盖造船厂兵工厂和武器商店的庞大建筑群。其目的是为了满足威尼斯海军的需求,依靠兵工厂,威尼斯共和国才能维护其自身独
侠客冰雪传奇无任何属性神装探测项链,到底是哪里爆出来的?上期说到黑旗人手都有这款传奇奢侈品探测项链,这东西到底是哪里爆出来的?有什么加成属性吗?这期就给大家解说解说这个探测项链这传奇里好东西很多,但这款探测项链没有任何的属性却也被大佬追
金铲铲之战S1千分王者版本唯一T0神将游侠Hello,大家好,我是猫呦,版本不是更新了嘛?版本更新里面倒是不是很大,但是暗改了很多,我也是拉面熊告诉我的,不然我都没注意到,我跟你们说一下我知道的哪些英雄被改了吧,韦鲁斯三星