使用Redis的有序集合实现排行榜功能

游戏中存在各类各样的排行榜,好比玩家的等级排名、分数排名等。玩家在排行榜中的名次是其实力的象征,位于榜单前列的玩家在虚拟世界中拥有无尚荣耀,因此名次也就成了核心玩家的追求目标。redis

一个典型的游戏排行榜包括如下常见功能:数据库

  1. 可以记录每一个玩家的分数;
  2. 可以对玩家的分数进行更新;
  3. 可以查询每一个玩家的分数和名次;
  4. 可以按名次查询排名前N名的玩家;
  5. 可以查询排在指定玩家先后M名的玩家。

更进一步,上面的操做都须要在短期内实时完成,这样才能最大程度发挥排行榜的效用。bash

因为一个玩家名次上升x位将会引发x+1位玩家的名次发生变化(包括该玩家),若是采用传统数据库(好比MySQL)来实现排行榜,当玩家人数较多时,将会致使对数据库的频繁修改,性能得不到知足,因此咱们只能另想它法。服务器

Redis做为NoSQL中的一员,近年来获得普遍应用。与Memcached相比,Redis拥有更多的数据类型和操做接口,具备更大的适用范围,其中的有序集合(sorted set,也称为zset)就很是适合于排行榜的构建。下面简要总结一下。微信

1. Redis的安装

Ubuntu下安装Redis很是简单,执行以下命令便可:数据结构

$ sudo apt-get install redis-server函数

安装完毕,运行命令行客户端redis-cli就能够访问本地redis服务器。性能

$ redis-cli redis 127.0.0.1:6379>优化

若是要使用最新版本,须要到Redis官网(redis.io)下载最新的代码自行编译,步骤略。ui

2. ZSet的经常使用命令

有序集合首先是集合,其成员(member)具备惟一性,其次,每一个成员关联了一个分数(score),使得成员能够按照分数排序。关于有序集合的介绍见redis.io/topics/data…,其命令见redis.io/commands#so…

下面介绍几个能用于排行榜的命令。

假设lb为排行榜名称,user一、user2等为玩家惟一标识。

1) zadd——设置玩家分数

命令格式:zadd 排行榜名称 分数 玩家标识 时间复杂度:O(log(N))

下面设置了4个玩家的分数,若是玩家分数已经存在,则会覆盖以前的分数。

> redis 127.0.0.1:6379> zadd lb 89 user1
> (integer) 1
> redis 127.0.0.1:6379> zadd lb 95 user2
> (integer) 1
> redis 127.0.0.1:6379> zadd lb 95 user3
> (integer) 1
> redis 127.0.0.1:6379> zadd lb 90 user4
> (integer) 1
复制代码
2) zscore——查看玩家分数

命令格式:zscore 排行榜名称 玩家标识 时间复杂度:O(1)

下面是查看user2这个玩家在lb排行榜中的分数。

redis 127.0.0.1:6379> zscore lb user2 “95”

3) zrevrange——按名次查看排行榜

命令格式:zrevrange 排行榜名称 起始位置 结束位置 [withscores] 时间复杂度:O(log(N)+M)

因为排行榜通常是按照分数由高到低排序的,因此咱们使用zrevrange,而命令zrange是按照分数由低到高排序。

起始位置和结束位置都是以0开始的索引,且都包含在内。若是结束位置为-1则查看范围为整个排行榜。

带上withscores则会返回玩家分数。

下面为查看全部玩家分数。

> redis 127.0.0.1:6379> zrevrange lb 0 -1 withscores
> 1) “user3”
> 2) “95”
> 3) “user2”
> 4) “95”
> 5) “user4”
> 6) “90”
> 7) “user1”
> 8) “89”
复制代码

下面为查询前三名玩家分数。

> redis 127.0.0.1:6379> zrevrange lb 0 2 withscores
> 1) “user3”
> 2) “95”
> 3) “user2”
> 4) “95”
> 5) “user4”
> 6) “90”
复制代码
4) zrevrank——查看玩家的排名

命令格式:zrevrank 排行榜名称 玩家标识 时间复杂度:O(log(N))

与zrevrange相似,zrevrank是以分数由高到低的排序返回玩家排名(实际返回的是以0开始的索引),对应的zrank则是以分数由低到高的排序返回排名。

下面是查询玩家user3和user4的排名。

> redis 127.0.0.1:6379> zrevrank lb user3
> (integer) 0
> redis 127.0.0.1:6379> zrevrank lb user1
> (integer) 3
复制代码
5) zincrby——增减玩家分数

命令格式:zincrby 排行榜名称 分数增量 玩家标识 时间复杂度:O(log(N))

有的排行榜是在变动时从新设置玩家的分数,而还有的排行榜则是以增量方式修改玩家分数,增量可正可负。若是执行zincrby时玩家尚不在排行榜中,则认为其原始分数为0,至关于执行zdd。

下面将user4的分数增长6,使其名次上升到第一位。

> redis 127.0.0.1:6379> zincrby lb 6 user4
> “96”
> redis 127.0.0.1:6379> zrevrange lb 0 -1 withscores
> 1) “user4”
> 2) “96”
> 3) “user3”
> 4) “95”
> 5) “user2”
> 6) “95”
> 7) “user1”
> 8) “89”
复制代码
6) zrem——移除某个玩家

命令格式:zrem 排行榜名称 玩家标识 时间复杂度:O(log(N))

下面移除玩家user4。

> redis 127.0.0.1:6379> zrem lb user4
> (integer) 1
> redis 127.0.0.1:6379> zrevrange lb 0 -1 withscores
> 1) “user3”
> 2) “95”
> 3) “user2”
> 4) “95”
> 5) “user1”
> 6) “89”
复制代码
7) del——删除排行榜

命令格式:del 排行榜名称

排行榜对象在咱们首次调用zadd或zincrby时被建立,当咱们要删除它时,调用redis通用的命令del便可。

> redis 127.0.0.1:6379> del lb
> (integer) 1
> redis 127.0.0.1:6379> get lb
> (nil)
复制代码

3. 相同分数问题

免费的方案总有那么一些不完美。从前面的例子咱们能够看到,user2和user3具备相同的分数,但在按分数逆序排序时,user3排在了user2前面。而在实际应用场景中,咱们更但愿看到user2排在user3前面,由于user2比user3先加入排行榜,也就是说user2先到达该分数。

但Redis在遇到分数相同时是按照集合成员自身的字典顺序来排序,这里便是按照”user2″和”user3″这两个字符串进行排序,以逆序排序的话user3天然排到了前面。

要解决这个问题,咱们能够考虑在分数中加入时间戳,计算公式为:

带时间戳的分数 = 实际分数*10000000000 + (9999999999 – timestamp)

timestamp咱们采用系统提供的time()函数,也就是1970年1月1日以来的秒数,咱们采用32位的时间戳(这能坚持到2038年),因为32位时间戳是10位十进制整数(最大值4294967295),因此咱们让时间戳占据低10位(十进制整数),实际分数则扩大10^10倍,而后把两部分相加的结果做为zset的分数。考虑到要按时间倒序排列,因此时间戳这部分须要颠倒一下,这即是用9999999999减去时间戳的缘由。当咱们要读取玩家实际分数时,只需去掉后10位便可。

初步看起来这个方案还不错,但这里面有两个问题。

第一个问题是小问题,采用秒为时间戳可能区分度还不够,若是同一秒出现两个分数相同的仍然会出现前面的问题,固然咱们能够选择精度更高的时间戳,但在实际场景中,同一秒谁排前面已经可有可无。

第二个问题是大问题,由于Redis的分数类型采用的是double,64位双精度浮点数只有52位有效数字,它能精确表达的整数范围为-2^53到2^53,最高只能表示16位十进制整数(最大值为9007199254740992,其实连16位也不能完整表示)。这就是说,若是前面时间戳占了10位的话,分数就只剩下6位了,这对于某些排行榜分数来讲是不够用的。咱们能够考虑缩减时间戳位数,好比从2015年1月1日开始计时,但这仍然增长不了几位。或者减小区分度,以分钟、小时来做为时间戳单位。

若是Redis的分数类型为int64,咱们就没有上面的烦恼。说到这里,其实Redis真应该再额外提供一个int64类型的ZSet,但目前只能是幻想,除非本身改其源码。

既然Redis也不能完美解决排行榜问题,那最终是否是有必要本身实现一个专门的排行榜数据结构呢?毕竟实际应用中的排行榜有不少能够优化的地方,比玩家呈金字塔分布,越是低分段玩家数量越多,同一分数拥有大量玩家,玩家增长一分均可能超越不少玩家,这就为优化提供了可能。

本文亦在微信公众号【小道资讯】发布,欢迎扫码关注!

相关文章
相关标签/搜索