使用Redis实现实时排行榜
游戏中存在各种各样的排行榜,比如玩家的等级排名、分数排名等。玩家在排行榜中的名次是其实力的象征,位于榜单前列的玩家在虚拟世界中拥有无尚荣耀,所以名次也就成了核心玩家的追求目标。
一个典型的游戏排行榜包括以下常见功能:
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实现世界杯排行榜功能(实战)
转载请注明出处:https://www.cnblogs.com/wenjunwei/p/9754346.html 需求 前段时间,做了一个世界杯竞猜积分排行榜.对世界杯64场球赛胜负平进行猜测,猜对+ ...
- Redis的Sorted-Sets排行榜功能实现
Redis的ZSet排行榜功能实现 1. 功能需求 类似给用户n张图片, 用户左滑不喜欢右滑喜欢.所以每个用户就会有一些喜欢的图片集合和不喜欢的图片集合.现在我们要做一个将按照一个算法将喜欢的排到前面 ...
- l来自wentao:项目加入缓存(redis),实时调试时用 -----可视化缓存,flushdb
下文来自segmentfault,网站:一个交流网站:https://segmentfault.com/ 在做一个项目时如果加入缓存(例如redis),我如何进行实时调试呢? 缓存 高并发 架构 gz ...
- 我是如何用redis做实时订阅推送的
前阵子开发了公司领劵中心的项目,这个项目是以redis作为关键技术落地的. 先说一下领劵中心的项目吧,这个项目就类似京东app的领劵中心,当然图是截取京东的,公司的就不截了... 其中 ...
- Redis实现实时热点查询
Redis内存淘汰 定义: 指的是用户存储的一些键被可以被Redis主动地从实例中删除,从而产生读miss的情况 机制存在原因: Redis最常见的两种应用场景为缓存和持久存储 首先要明确的一个问题是 ...
- 我是如何用redis做实时订阅推送的(转)
前阵子开发了公司领劵中心的项目,这个项目是以redis作为关键技术落地的. 先说一下领劵中心的项目吧,这个项目就类似京东app的领劵中心,当然图是截取京东的,公司的就不截了... 其中 ...
- tp5 (自写) 实现redis消息队列 + 排行榜
1:小皮开启redis, 控制器按Ctrl 点击new Redis 进入 redis.php 进行封装 //向队列添加数据 // LPUSH key value1 [value2] //将一个或多个值 ...
- swoole+Redis实现实时数据推送
<?php /** * *************************************** * 单进程保护 * * ********************************* ...
- 浅谈 OpenResty,基于opebresty+redis进行实时线上限流
一.前言 我们都知道Nginx有很多的特性和好处,但是在Nginx上开发成了一个难题,Nginx模块需要用C开发,而且必须符合一系列复杂的规则,最重要的用C开发模块必须要熟悉Nginx的源代码,使得开 ...
随机推荐
- C#中文件下载的几种方法演示源码
内容过程,把内容过程比较重要的内容做个珍藏,如下的内容是关于C#中文件下载的几种方法演示的内容,应该是对各朋友有较大好处. using System;using System.Data;using S ...
- 【NodeJS】基础知识
nodejs基础 nodejs允许自己封装模块,使得编写程序可以模块化,便于维护整理.在一个js文件中写完封装的函数或对象后,可以使用exports或module.exports来将模块中的函数暴露给 ...
- Android Studio项目用Git上传至码云(OSChina)
工具: Git安装(官网下载,默认安装即可). 步骤一: 新建一个Android Studio项目: 步骤二: 申请OSChina账号.登陆并创建新项目 1. 2. 3.点击复制,下面在Android ...
- 数据文件实时同步(rsync + sersync2)
因近期项目需求,需要同步云端服务器的数据给**方做大数据分析. 思路: 起初只要数据同步,准备开放数据采集接口.但实时性较差,会有延迟. 故而寻觅各种解决方案,最终确定使用 rsync 进行文件同步, ...
- C++设计模式视频讲解
设计模式(C++) 视频网址: http://www.qghkt.com/ 设计模式(C++)视频地址: https://ke.qq.com/course/318637?tuin=a508ea62 目 ...
- 【原创】谈谈线上CPU100%排查套路
引言 不知道在大家面试中,有没有遇到这个问题 生产服务器上部署了几个java程序,突然出现了CPU100%的异常告警,你如何定位出问题呢? 这个问题分为两版回答! 高调版 对不起,我是做研发的,这个问 ...
- 【原创】平时的你VS面试的你
引言 大家在面试的时候,特别是最后一面HR面,是不是经常都说自己咳咳咳.博主特意总结了一下平时的你和面试的你区别在哪,整理成文,大家看看就好~ 正文 面对HR 问题:你是如何和你同事相处的? 平时的你 ...
- 我用Python爬取了李沧最近一年多的二手房成交数据得出以下结论
前言 去年年底,博主有购房的意愿,本来是打算在青岛市北购房,怎奈工作变动,意向转移到了李沧,坐等了半年以后,最终选择在红岛附近购置了期房. 也许一些知道青岛红岛的小伙伴会问我,为什么会跑到那鸟不拉屎的 ...
- CodeSmith 一、连接Mysql
下载了codesmith 8,连接Mysql却提示“找不到请求的 .Net Framework Data Provider". 1,下载MySql.Data.dll:https://dev. ...
- 记录一下不能使用let时如何创建局部变量(使用立即执行函数)
记录一下阮老师提及的立即执行函数模拟let(以前根本没想到可以这样做啊!) // IIFE 写法 (function () { var tmp = ...; ... }()); // 块级作用域写法 ...