使用Redis实现实时排行榜功能


Posted in Redis onJuly 02, 2021

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

一个典型的游戏排行榜包括以下常见功能:

1. 能够记录每个玩家的分数;

2. 能够对玩家的分数进行更新;

3. 能够查询每个玩家的分数和名次;

4. 能够按名次查询排名前N名的玩家;

5. 能够查询排在指定玩家前后M名的玩家。

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

由于一个玩家名次上升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官网([http://redis.io](http://redis.io/))下载最新的代码自行编译,步骤略。

## 2\. ZSet的常用命令

有序集合首先是集合,其成员(member)具有唯一性,其次,每个成员关联了一个分数(score),使得成员可以按照分数排序。关于有序集合的介绍见[http://redis.io/topics/data-types#sorted-sets](http://redis.io/topics/data-types#sorted-sets),其命令见[http://redis.io/commands#sorted_set](http://redis.io/commands#sorted_set)。

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

假设lb为排行榜名称,user1、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也不能完美解决排行榜问题,那最终是不是有必要自己实现一个专门的排行榜数据结构呢?毕竟实际应用中的排行榜有很多可以优化的地方,比玩家呈金字塔分布,越是低分段玩家数量越多,同一分数拥有大量玩家,玩家增加一分都可能超越很多玩家,这就为优化提供了可能。

到此这篇关于使用Redis实现实时排行榜功能的文章就介绍到这了,更多相关Redis实时排行榜内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
解决redis sentinel 频繁主备切换的问题
Apr 12 Redis
Redis IP地址的绑定的实现
May 08 Redis
解析Redis Cluster原理
Jun 21 Redis
redis cluster支持pipeline的实现思路
Jun 23 Redis
Redis字典实现、Hash键冲突及渐进式rehash详解
Sep 04 Redis
使用redis生成唯一编号及原理示例详解
Sep 15 Redis
高并发下Redis如何保持数据一致性(避免读后写)
Mar 18 Redis
源码分析Redis中 set 和 sorted set 的使用方法
Mar 22 Redis
Redis 异步机制
May 15 Redis
Redis批量生成数据的实现
Jun 05 Redis
Redis实现分布式锁的五种方法详解
Jun 14 Redis
Redis主从复制操作和配置详情
Sep 23 Redis
redis使用不当导致应用卡死bug的过程解析
Redis主从配置和底层实现原理解析(实战记录)
浅谈Redis中的RDB快照
聊一聊Redis与MySQL双写一致性如何保证
k8s部署redis cluster集群的实现
Jun 24 #Redis
浅析Redis Sentinel 与 Redis Cluster
redis cluster支持pipeline的实现思路
You might like
phpmyadmin提示The mbstring extension is missing的解决方法
2014/12/17 PHP
ThinkPHP、ZF2、Yaf、Laravel框架路由大比拼
2015/03/25 PHP
php实现获取农历(阴历)、节日、节气的类与用法示例
2017/11/20 PHP
Ucren Virtual Desktop V2.0
2006/11/07 Javascript
在textarea文本域中显示HTML代码的方法
2007/03/06 Javascript
jsTree树控件(基于jQuery, 超强悍)[推荐]
2009/09/01 Javascript
用Javascript实现Sleep暂停功能代码
2010/09/03 Javascript
jQuery使用removeClass方法删除元素指定Class的方法
2015/03/26 Javascript
JavaScript计算值然后把值嵌入到html中的实现方法
2016/10/29 Javascript
JS如何生成一个不重复的ID的函数
2016/12/25 Javascript
微信小程序三级联动地址选择器的实例代码
2017/07/12 Javascript
jQuery实现的电子时钟效果完整示例
2018/04/28 jQuery
详解VUE自定义组件中用.sync修饰符与v-model的区别
2018/06/26 Javascript
JavaScript中set与get方法用法示例
2018/08/15 Javascript
vue实现鼠标移过出现下拉二级菜单功能
2019/12/12 Javascript
微信小程序云函数添加数据到数据库的方法
2020/03/04 Javascript
JS画布动态实现黑客帝国背景效果
2020/11/08 Javascript
[14:51]DOTA2 HEROS教学视频教你分分钟做大人-卓尔游侠
2014/06/13 DOTA
用Python编写一个每天都在系统下新建一个文件夹的脚本
2015/05/04 Python
python2.7和NLTK安装详细教程
2018/09/19 Python
python批量获取html内body内容的实例
2019/01/02 Python
python 将html转换为pdf的几种方法
2020/12/29 Python
使用Filters滤镜弥补CSS3的跨浏览器问题以及兼容低版本IE
2013/01/23 HTML / CSS
CSS3 flex布局之快速实现BorderLayout布局
2015/12/03 HTML / CSS
屈臣氏乌克兰:Watsons UA
2019/10/29 全球购物
信息工程学院毕业生推荐信
2013/11/05 职场文书
物业总经理岗位职责
2014/02/28 职场文书
库房管理员岗位职责
2014/03/09 职场文书
运动会宣传口号
2014/06/09 职场文书
个人批评与自我批评
2014/10/15 职场文书
简单的离婚协议书范本
2014/11/16 职场文书
2015年度个人教学工作总结
2015/05/20 职场文书
mysql字符串截取函数小结
2021/04/05 MySQL
利用javaScript处理常用事件详解
2021/04/14 Javascript
如何开启Apache,Nginx和IIS服务器的GZIP压缩功能
2022/04/29 Servers
win10系统xps文件怎么打开?win10打开xps文件的两种操作方法
2022/07/23 数码科技