基于Redis游戏中的实时排行榜[目录]最后修改时间:2019年6月4日18:18:371。前言前段时间项目(手游)实现了一个实时排行榜功能。主要特点:实时全服排名可查询,支持个人玩家排名二维排序数据量不大,大致在1W~50W范围内(开服合服会导致更多以及单个服务器中的更多字符)。Guild)tank本项目是一款坦克手游。一般情况是每个角色都有N辆坦克。坦克分为多种类型(轻型、重型等),玩家可以加入一个军团(行会)。具体可以细分为:人物-段位排行(1.等级2.战力)-战力排行(1.战斗2.等级)-个人竞技场排行(1.竞技场排行)-巴别塔排行(1.巴别塔)楼层数2.通关时间)-声望排行榜(1.声望2.等级)军团(Guild)-军团战力排行榜(1.军团总战力2.军团等级)-军团等级排行榜(1.军团等级2).TotalCombatPowerofLegion)Tanks(1.TankCombatPower2.TankLevel)-LightTankCombatPowerRankings-Medium-Heavy-Anti-TankGuns-自行火炮↑括号内的排序维度为3.思路基于实时性的考虑,所以决定使用Redis来实现排行榜。如果文中使用的redis命令不清楚,请参考Redis在线手册。需要解决以下问题:如何动态更新复合排序(2维)的排名数据,获取排行榜4、实现复合排序基于Redis的排行榜主要是利用Redis的有序集(SortedSet)实现添加成员——积分的操作是通过Redis的zAdd操作ZADDkeyscoremember[[scoremember][scoremember]...]default某些情况下,如果score相同,会按照成员的字典顺序排序.4.1等级排行榜首先以等级排行榜(1.等级2.战力)为例。前。因此,分数可以定义为:分数=等级*10000000000+战力。游戏中,玩家的等级从1到100,战力从0到100000000,这里设计的战力预留的数值范围是10位数值,等级是3位数值,所以最大值为13位。有序集合的score值为64位整数值或双精度浮点数,最大值为9223372036854775807,完全可以表示18位的数值,所以这里用13位的分数是绰绰有余的。4.2BabelLeaderboard另一种比较典型的排行榜是BabelTower排行榜(1.楼层数2.通关时间),排行榜要求通过相同的楼层数,通关时间越早越好。由于更早的通关时间需要优先级,所以不能像之前一样直接打分=层数*10^N+通关时间。我们可以把清关时间换算成一个Relative时间,即score=楼层数*10^N+(基准时间-清关时间)显然,越接近(越大)的清关时间,基准时间越小-通关时间值为,符合排行榜要求。Basetime的选择,随机选择一个时间2050-01-0100:00:00,对应时间戳2524579200。最后,*score=层数10^N+(2524579200-passedtimestamp)上述分数公式中,N取10,即保留10位的相对时间。4.3坦克排行榜坦克排行榜与其他排行榜的区别在于,有序集中的成员是一个复合id,由uid_tankId组成。这一点需要注意。5.排名数据的动态更新仍然以关卡排行榜为例。游戏中显示的等级排行榜需要的数据包括(但不限于):角色名、Uid、战力、头像、公会名、VIP等级,因为这些数据会在游戏过程中显示。它是动态变化的,所以这里不考虑将这些数据直接作为有序集合中的成员存储。用于存储玩家等级排行榜的有序集如下--s1:rank:user:lv--------zset--|玩家id1|得分1|...|玩家IDN|scoreN----------------------------------------member是角色uid,score是综合分和使用哈希存储玩家的动态数据(json)--s1:rank:user:lv:item------string--|玩家id1|播放器数据的json字符串|...|玩家IDN|----------------------------------------使用该方案,只需要添加玩家创建角色时将角色加入rank排行榜,然后在玩家战力变化时实时更新玩家的s1:rank:user:lv综合点数就足够了。如果玩家如果其他数据(用于排行榜显示)发生变化,相应地修改其在s1:rank:user:lv:item中的数据json字符串。6、以排行榜为例。目的需要从`s1:rank:user:lv`中取出前100名玩家和他们的数据。Redis命令使用[`ZRANGEkeystartstop[WITHSCORES]`](http://redisdoc.com/sorted_set/zrange.html)时间复杂度:O(log(N)+M),N为有序的基数集,M为结果集的基数。StepzRange("s1:rank:user:lv",0,99)获取前100名玩家的uidhGet("s1:rank:user:lv:item",$uid)获取前100名的具体信息玩家一一执行时,可以优化上面的第2步。分析zRange的时间复杂度为O(log(N)+M),N为有序集的基数,M为结果集的基数hGet时间复杂度为O(1)由于第2步需要获取at最多100个玩家数据,需要执行100次。这里的执行时间还必须包括与redis的通信时间。即使单次只需要1MS,最多也要100MS。借助Redis的Pipeline解决,整个过程可以减少到只和redis通信两次,大大减少了耗时。下面的例子是php代码//$redis$redis->multi(Redis::PIPELINE);foreach($uidsas$uid){$redis->hGet($userDataKey,$uid);}$resp=$redis->执行();//结果会一次性以数组的形式返回提示:Pipeline和Multi模式的区别参考:https://blog.csdn.net/weixin_...Pipeline是在客户端缓冲命令,所以multiple请求可以合并为一个并发送到服务器。但是原子性是不保证的!!!多事务是在服务器端缓冲命令,每个命令都会发起一个请求保证原子性,可以配合WATCH实现事务,目的不同。7.显示代码redis=$redis;$this->rankKey=$rankKey;$this->rankItemKey=$rankItemKey;$this->sortFlag=SORT_DESC;}/***@returnRedis*/publicfunctiongetRedis(){return$this->redis;}/***@paramRedis$redis*/publicfunctionsetRedis($redis){$this->redis=$redis;}/***新增/更新单人排名数据*@paramstring|int$uid*@paramnull|double$score*@paramnull|string$rankItem*/publicfunctionupdateScore($uid,$score=null,$rankItem=null){如果(is_null($score)&&is_null($rankItem)){返回;$redis=$this->getRedis()->multi(Redis::PIPELINE);如果(!is_null($score)){$redis->zAdd($this->rankKey,$score,$uid);}if(!is_null($rankItem)){$redis->hSet($this->rankItemKey,$uid,$rankItem);$redis->exec();}/***获取单人队列*@paramstring|int$uid*@returnarray*/publicfunctiongetRank($uid){$redis=$this->getRedis()->multi(Redis::PIPELINE);如果($this->sortFlag==SORT_DESC){$redis->zRevRank($this->rankKey,$uid);}else{$redis->zRank($this->rankKey,$uid);$redis->hGet($this->rankItemKey,$uid);列表($rank,$rankItem)=$redis->exec();返回[$rank===false?-1:$排名+1,$排名项];}/***移去单人*@param$uid*/publicfunctiondel($uid){$redis=$this->getRedis()->multi(Redis::PIPELINE);$redis->zRem($this->rankKey,$uid);$redis->hDel($this->rankItemKey,$uid);$redis->exec();}/***获取排行榜前N项*@param$topN*@parambool$withRankItem*@returnarray*/publicfunctiongetList($topN,$withRankItem=false){$redis=$this->getRedis();如果($this->sortFlag===SORT_DESC){$list=$redis->zRevRange($this->rankKey,0,$topN);}else{$list=$redis->zRange($this->rankKey,0,$topN);}$rankItems=[];如果(!empty($list)&&$withRankItem){$redis->multi(Redis::PIPELINE);foreach($listas$uid){$redis->hGet($this->rankItemKey,$uid);}$rankItems=$redis->exec();}返回[$list,$rankItems];}/***清除排行榜*/publicfunctionflush(){$redis=$this->getRedis();$redis->del($this->rankKey,$this->rankItemKey);这是排行榜的最简单实现,排名项的积分计算是在外部处理的。
