前言
前面几篇文章给大家聊了下目前的常用的排行榜做法。
那么这篇文章将给大家带来如何使用redis来实现常见的游戏排行榜功能。
为什么使用redis
如果你已经是redis的高级玩家可以跳过这段介绍。下面这段redis的优势介绍仅对新手。
当然你如果想要了解更多的关于redis的使用你可以查看往期文章:
Redis :01---Redis简介和安装 ->Redis:23---info命令总结
Redis的特点:
- 内存数据库,速度快,也支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
- 支持事务
Redis的优势:
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。(事务)
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
Redis与其他key-value存储有什么不同?
- Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
- Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
单一维度的排行榜
在游戏中,我们经常会要对玩家的水晶,金币等资源数量进行做排行榜,数量较多的排行在前列。
这类的排行榜影响排名的因素单一,不会涉及到时间等其他维度的影响,当玩家的水晶,金币等资源增加的时候,我们希望能实时更新排行,这类排序相对来说比较简单, 所以采用redis的有序集合sorted set排序,将变动分值zadd到集合。
比如redis这个用户增加了1个金币,mongodb在打副本的时候增加了2个金币,渣渣mysql比较勤劳,分别增加了3,3,4个金币,最后排序的时候你可以通过ZRANGE 或者ZREVRANGE选择递增递减排序方式输出结果
多维度的复合排行榜
顾名思义,影响排名的因素是多个的,比如有这么一战力排行榜,先按照战力大小递减排序,战力大的排名在前,战力较小的排名靠后,如果战力相同,谁先到达此积分者排名靠前。
这里我们仍然使用sorted set,这里使用一个小技巧
score为long类型,但是我们的分值用不到这么大的类型,所以将score的高位表示分值,低位表示更新时间
具体公式如下:
score = ((value & 0xfffff) << 32) | (createTime & 0xffffffff)
1.战力排行榜
首先以等级排行榜(1. 等级 2.战力)为例, 该排行榜要求同等级的玩家, 战斗力大的排在前. 因此分数可以定为:
分数 = 等级*10000000000 + 战斗力
游戏中玩家等级范围是1~100, 战力范围0~100000000.
此处设计中为战斗力保留的值范围是 10位数值, 等级是 3位数值, 因此最大数值为 13位.
有序集合的score取值是是64位整数值或双精度浮点数, 最大表示值是 9223372036854775807, 即能完整表示18位数值, 因此用于此处的 13位score 绰绰有余.
2.爬塔排行榜
另一个典型排行榜是爬塔排行榜(1.层数 2.通关时间), 该排行榜要求通过层数相同的, 通关时间较早的优先.
由于要求的是通关时间较早的优先, 因此不能像之前那样直接 分数=层数*10^N+通关时间.
我们可以将通关时间转换为一个相对时间, 即 分数=层数*10^N + (基准时间 - 通关时间)
很明显的, 通关时间越近(大), 则 基准时间 - 通关时间
基准时间的选择则随意选择了较远的一个时间 2050-01-01 00:00:00, 对应时间戳2524579200
最终, *分数 = 层数10^N + (2524579200 - 通过时间戳)
上述分数公式中, N取10, 即保留10位数的相对时间.
日周月等周期性排行榜
首先,来个“今日积分榜”吧,排序规则是今日用户新增积分从多到少。
那么用户增加积分时,都操作一下记录当天积分增加的有序集合。假设今天是 2015 年 04 月 01 日,UID 为 1 的用户因为某个操作,增加了 5 个积分。Redis 命令如下:
假设还有其他几个用户也增加了积分:
看看现在有序集合 rank:20150401 中的数据(withscores 参数可以附带获取元素的 score):
按照分数从高到低,获取 top10:
因为只有三个元素,所以就查询出了这些数据。
如果每天记录当天的积分排行榜,那么其他花样百出的榜单也就简单了。比如“昨日积分榜”:
利用并集实现多天的积分总和,实现“上周积分榜”:
这样就将 7 天的积分记录合并到有序集合 rank:last_week 中了。权重因子 WEIGHTS 如果不给,默认就是 1。为了不隐藏细节,特意写出。那么查询上周积分榜 Top10 的信息就是:
“月度榜”、“季度榜”、“年度榜”等等就以此类推。
排行榜数据更新
还是以等级排行榜为例
游戏中展示的等级排行榜所需的数据包括(但不限于):
- 角色名
- Uid
- 战斗力
- 头像
- 所属公会名
- VIP等级
由于这些数据在游戏过程中是会动态变更的, 因此此处不考虑将这些数据直接作为 member 存储在有序集合中.
用于存储玩家等级排行榜有序集合如下
member为角色uid, score为复合积分
使用hash存储玩家的动态数据(json)
使用这种方案, 只需要在玩家创建角色时, 将该角色添加到等级排行榜中, 后续则是当玩家 等级战斗力 发生变化时需实时更新s1:rank:user:lv
该玩家的复合积分即可. 若玩家其他数据(用于排行榜显示)有变化, 则也相应地修改其在 s1:rank:user:lv:item
中的数据json串.
注意事项
如果你的redis没有开启持久化,所以需要将排行榜数据定时备份到数据库。由于是分布式服务,所以需要决定由哪个服务来保存数据,并且需要在redis里记录上次保存时间
这里使用了一个分布式锁,每个服务定时拉取排行榜数据到内存,然后竞争分布式锁,如果获取到锁并且判断上次保存的时间,大于保存间隔时间就执行保存操作。
每次保存数据,不是全保存,只保存变化的名次数据(或者有'脏数据'标记的记录),降低写入条数
停服的时候做一次全保存
起服的时候检测redis里是否有排行榜数据,如果没有就需要竞争一个分布式锁,从数据库加载数据到redis
拉取到内存的排行榜数据立马做好序列化,减少序列化次数
限制同一玩家同一榜单数据请求频率(30s),可以减少发送数据次数,降低流量。
排行榜数据的拉取
依旧以等级排行榜为例.
目的
用到的Redis命令
步骤
-
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代码
Tip: Pipeline 与 Multi 模式的区别
- Pipeline 管线化, 是在客户端将命令缓冲, 因此可以将多条请求合并为一条发送给服务端. 但是不保证原子性!!!
- Multi 事务, 是在服务端将命令缓冲, 每个命令都会发起一次请求, 保证原子性, 同时可配合 WATCH 实现事务, 用途是不一样的.
这里我提供部分排行的php代码,仅供参考
这就是一个排行榜最简单的实现了, 排行项的积分计算由外部自行处理.