智能摘要文章源自JAVA秀-https://www.javaxiu.com/38120.html
O(log(N)+M),N为有序集的基数,而M为结果集的基数。Pipeline管线化,是在客户端将命令缓冲,因此可以将多条请求合并为一条发送给服务端.但是不保证原子性!!!这就是一个排行榜最简单的实现了,排行项的积分计算由外部自行处理。最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。文章源自JAVA秀-https://www.javaxiu.com/38120.html
原文约 2559 字 | 图片 2 张 | 建议阅读 6 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/38120.html
用 Redis 搞定游戏中的实时排行榜,附源码!
戳一戳→ 程序员的成长之路 文章源自JAVA秀-https://www.javaxiu.com/38120.html
文章源自JAVA秀-https://www.javaxiu.com/38120.html
程序员的成长之路文章源自JAVA秀-https://www.javaxiu.com/38120.html
互联网/程序员/技术/资料共享 文章源自JAVA秀-https://www.javaxiu.com/38120.html
关注文章源自JAVA秀-https://www.javaxiu.com/38120.html
阅读本文大概需要 6 分钟。文章源自JAVA秀-https://www.javaxiu.com/38120.html
来源:嘉兴ing文章源自JAVA秀-https://www.javaxiu.com/38120.html
来源:segmentfault.com/a/1190000019139010文章源自JAVA秀-https://www.javaxiu.com/38120.html
1. 前言
前段时间刚为项目(手游)实现了一个实时排行榜功能, 主要特性:文章源自JAVA秀-https://www.javaxiu.com/38120.html
实时全服排名文章源自JAVA秀-https://www.javaxiu.com/38120.html
可查询单个玩家排名文章源自JAVA秀-https://www.javaxiu.com/38120.html
支持双维排序文章源自JAVA秀-https://www.javaxiu.com/38120.html
数据量不大, 大致在 1W ~ 50W区间(开服, 合服会导致单个服角色数越来越多).文章源自JAVA秀-https://www.javaxiu.com/38120.html
2. 排行榜分类
按照排行主体类型划分, 主要分为:文章源自JAVA秀-https://www.javaxiu.com/38120.html
角色文章源自JAVA秀-https://www.javaxiu.com/38120.html
军团(公会)文章源自JAVA秀-https://www.javaxiu.com/38120.html
坦克文章源自JAVA秀-https://www.javaxiu.com/38120.html
该项目是个坦克手游, 大致情况是每个角色有N辆坦克, 坦克分为多种类型(轻型, 重型等), 玩家可加入一个军团(公会).文章源自JAVA秀-https://www.javaxiu.com/38120.html
具体又可以细分为:文章源自JAVA秀-https://www.javaxiu.com/38120.html
角色文章源自JAVA秀-https://www.javaxiu.com/38120.html
- 战斗力排行榜(1. 战斗 2.等级) - 个人竞技场排行榜(1. 竞技场排名) - 通天塔排行榜(1.通天塔层数 2.通关时间) - 威望排行榜(1.威望值 2.等级)
军团(公会)文章源自JAVA秀-https://www.javaxiu.com/38120.html
- 军团等级排行榜(1.军团等级 2.军团总战斗力)
坦克(1.坦克战斗力 2.坦克等级)文章源自JAVA秀-https://www.javaxiu.com/38120.html
- 中型 - 重型 - 反坦克炮 - 自行火炮
↑ 括号内为排序维度文章源自JAVA秀-https://www.javaxiu.com/38120.html
3. 思路
基于实时性的考虑, 决定使用Redis来实现该排行榜.文章源自JAVA秀-https://www.javaxiu.com/38120.html
文章中用到的redis命令如有不清楚的, 可参照 Redis在线手册 .文章源自JAVA秀-https://www.javaxiu.com/38120.html
需要解决如下问题:文章源自JAVA秀-https://www.javaxiu.com/38120.html
复合排序(2维)文章源自JAVA秀-https://www.javaxiu.com/38120.html
排名数据的动态更新文章源自JAVA秀-https://www.javaxiu.com/38120.html
如何取排行榜文章源自JAVA秀-https://www.javaxiu.com/38120.html
4. 实现 复合排序
基于Redis的排行榜主要使用的是Redis的 有序集合(SortedSet)来实现文章源自JAVA秀-https://www.javaxiu.com/38120.html
添加 成员-积分 的操作是通过Redis的zAdd操作
ZADD key score member [[score member] [score member] ...]
文章源自JAVA秀-https://www.javaxiu.com/38120.html
默认情况下, 若score相同, 则按照 member 的字典顺序排序.文章源自JAVA秀-https://www.javaxiu.com/38120.html
4.1 等级排行榜
首先以等级排行榜(1. 等级 2.战力)为例, 该排行榜要求同等级的玩家, 战斗力大的排在前. 因此分数可以定为:文章源自JAVA秀-https://www.javaxiu.com/38120.html
分数 = 等级*10000000000 + 战斗力文章源自JAVA秀-https://www.javaxiu.com/38120.html
游戏中玩家等级范围是1~100, 战力范围0~100000000.文章源自JAVA秀-https://www.javaxiu.com/38120.html
此处设计中为战斗力保留的值范围是 10位数值, 等级是 3位数值, 因此最大数值为 13位 .有序集合的score取值是是64位整数值或双精度浮点数, 最大表示值是 9223372036854775807, 即能完整表示 18位 数值,因此用于此处的 13位score 绰绰有余.文章源自JAVA秀-https://www.javaxiu.com/38120.html
4.2 通天塔排行榜
另一个典型排行榜是 通天塔排行榜(1.层数 2.通关时间) , 该排行榜要求通过层数相同的, 通关时间较早的优先.文章源自JAVA秀-https://www.javaxiu.com/38120.html
由于要求的是通关时间较早的优先, 因此不能像之前那样直接 分数=层数*10^N+通关时间 .文章源自JAVA秀-https://www.javaxiu.com/38120.html
我们可以将通关时间转换为一个相对时间, 即 分数=层数*10^N + (基准时间 - 通关时间)很明显的, 通关时间越近(大), 则 基准时间 - 通关时间 值越小, 符合该排行榜要求.文章源自JAVA秀-https://www.javaxiu.com/38120.html
基准时间的选择则随意选择了较远的一个时间 2050-01-01 00:00:00 , 对应时间戳2524579200文章源自JAVA秀-https://www.javaxiu.com/38120.html
最终, 分数 = 层数_ 10^N + (2524579200 - 通过时间戳)
述分数公式中, N取10, 即保留10位数的相对时间.文章源自JAVA秀-https://www.javaxiu.com/38120.html
4.3 坦克排行榜
坦克排行榜跟其他排行榜的区别在于, 有序集合中的 member 是一个复合id, 由 uid_tankId 组成.这点是需要注意的.文章源自JAVA秀-https://www.javaxiu.com/38120.html
5. 排名数据的动态更新
还是以等级排行榜为例文章源自JAVA秀-https://www.javaxiu.com/38120.html
游戏中展示的等级排行榜所需的数据包括(但不限于):文章源自JAVA秀-https://www.javaxiu.com/38120.html
角色名文章源自JAVA秀-https://www.javaxiu.com/38120.html
Uid文章源自JAVA秀-https://www.javaxiu.com/38120.html
战斗力文章源自JAVA秀-https://www.javaxiu.com/38120.html
头像文章源自JAVA秀-https://www.javaxiu.com/38120.html
所属公会名文章源自JAVA秀-https://www.javaxiu.com/38120.html
VIP等级文章源自JAVA秀-https://www.javaxiu.com/38120.html
由于这些数据在游戏过程中是会动态变更的, 因此此处不考虑将这些数据直接作为 member 存储在有序集合中.用于存储玩家等级排行榜有序集合如下文章源自JAVA秀-https://www.javaxiu.com/38120.html
-- s1:rank:user:lv ---------- zset -- | 玩家id1 | score1 | ... | 玩家idN | scoreN -------------------------------------
member为角色uid, score为复合积分文章源自JAVA秀-https://www.javaxiu.com/38120.html
使用hash存储玩家的动态数据(json)文章源自JAVA秀-https://www.javaxiu.com/38120.html
-- s1:rank:user:lv:item ------- string -- | 玩家id1 | 玩家数据的json串 | ... | 玩家idN | -----------------------------------------
使用这种方案, 只需要在玩家创建角色时, 将该角色添加到等级排行榜中, 后续则是当玩家 等级战斗力 发生变化时需实时更新 s1:rank:user:lv
该玩家的复合积分即可. 若玩家其他数据(用于排行榜显示)有变化, 则也相应地修改其在 s1:rank:user:lv:item
中的数据json串.文章源自JAVA秀-https://www.javaxiu.com/38120.html
6. 取排行榜
依旧以等级排行榜为例.文章源自JAVA秀-https://www.javaxiu.com/38120.html
目的文章源自JAVA秀-https://www.javaxiu.com/38120.html
需要从 `s1:rank:user:lv` 中取出前100名玩家, 及其数据.
用到的Redis命令文章源自JAVA秀-https://www.javaxiu.com/38120.html
[`ZRANGE key start stop [WITHSCORES]`](http://redisdoc.com/sorted_set/zrange.html) 时间复杂度: O(log(N)+M), N 为有序集的基数,而 M 为结果集的基数。
步骤文章源自JAVA秀-https://www.javaxiu.com/38120.html
zRange("s1:rank:user:lv", 0, 99)
获取前100个玩家的uid文章源自JAVA秀-https://www.javaxiu.com/38120.htmlhGet("s1:rank:user:lv:item", $uid)
逐个获取前100个玩家的具体信息文章源自JAVA秀-https://www.javaxiu.com/38120.html
具体实现时, 上面的步骤2是可以优化的.文章源自JAVA秀-https://www.javaxiu.com/38120.html
分析文章源自JAVA秀-https://www.javaxiu.com/38120.html
zRange时间复杂度是O(log(N)+M) , N 为有序集的基数,而 M 为结果集的基数文章源自JAVA秀-https://www.javaxiu.com/38120.html
hGet时间复杂度是 O(1)文章源自JAVA秀-https://www.javaxiu.com/38120.html
步骤2由于最多需要获取100个玩家数据, 因此需要执行100次, 此处的执行时间还得加上与redis通信的时间, 即使单次只要1MS, 最多也需要100MS.文章源自JAVA秀-https://www.javaxiu.com/38120.html
解决文章源自JAVA秀-https://www.javaxiu.com/38120.html
借助Redis的Pipeline, 整个过程可以降低到只与redis通信2次, 大大降低了所耗时间.文章源自JAVA秀-https://www.javaxiu.com/38120.html
以下示例为php代码文章源自JAVA秀-https://www.javaxiu.com/38120.html
// $redis $redis->multi(Redis::PIPELINE); foreach ($uids as $uid) { $redis->hGet($userDataKey, $uid); } $resp = $redis->exec(); // 结果会一次性以数组形式返回
Tip: Pipeline 与 Multi 模式的区别文章源自JAVA秀-https://www.javaxiu.com/38120.html
参考: https://blog.csdn.net/weixin_...文章源自JAVA秀-https://www.javaxiu.com/38120.html
Pipeline 管线化, 是在客户端将命令缓冲, 因此可以将多条请求合并为一条发送给服务端. 但是 不保证原子性 !!!文章源自JAVA秀-https://www.javaxiu.com/38120.html
Multi 事务, 是在服务端将命令缓冲, 每个命令都会发起一次请求, 保证原子性 , 同时可配合
WATCH
实现事务, 用途是不一样的.文章源自JAVA秀-https://www.javaxiu.com/38120.html
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); } }
这就是一个排行榜最简单的实现了, 排行项的积分计算由外部自行处理。文章源自JAVA秀-https://www.javaxiu.com/38120.html
<END>文章源自JAVA秀-https://www.javaxiu.com/38120.html
推荐阅读:文章源自JAVA秀-https://www.javaxiu.com/38120.html
Java 8 失宠!开发人员向 Java 11 转移...文章源自JAVA秀-https://www.javaxiu.com/38120.html
SQL 注入真是防不胜防!文章源自JAVA秀-https://www.javaxiu.com/38120.html
永久白嫖,新发现的外卖漏洞,请低调使用文章源自JAVA秀-https://www.javaxiu.com/38120.html
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
文章源自JAVA秀-https://www.javaxiu.com/38120.html
获取方式:点个「在看」,点击上方小卡片,进入公众号后回复「面试题」领取,更多内容陆续奉上。文章源自JAVA秀-https://www.javaxiu.com/38120.html
朕已阅 文章源自JAVA秀-https://www.javaxiu.com/38120.html

评论