使用Redis的有序集合Zset实现排行榜功能
游戏中存在各种各样的排行榜,比如玩家的等级排名、分数排名等。玩家在排行榜中的名次是其实力的象征,位于榜单前列的玩家在虚拟世界中拥有无尚荣耀,所以名次也就成了核心玩家的追求目标。
一个典型的游戏排行榜包括以下常见功能:
- 能够记录每个玩家的分数;
- 能够对玩家的分数进行更新;
- 能够查询每个玩家的分数和名次;
- 能够按名次查询排名前N名的玩家;
- 能够查询排在指定玩家前后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官网(redis.io)下载最新的代码自行编译,步骤略。
2. ZSet的常用命令
有序集合首先是集合,其成员(member)具有唯一性,其次,每个成员关联了一个分数(score),使得成员可以按照分数排序。关于有序集合的介绍见redis.io/topics/data…,其命令见redis.io/commands#so…。
下面介绍几个能用于排行榜的命令。
假设lb为排行榜名称( leaderboard ),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
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也不能完美解决排行榜问题,那最终是不是有必要自己实现一个专门的排行榜数据结构呢?毕竟实际应用中的排行榜有很多可以优化的地方,比玩家呈金字塔分布,越是低分段玩家数量越多,同一分数拥有大量玩家,玩家增加一分都可能超越很多玩家,这就为优化提供了可能。
链接:https://juejin.cn/post/6844903795131056135
使用Redis的有序集合Zset实现排行榜功能相关推荐
- redis延迟队列 实现_php使用redis的有序集合zset实现延迟队列
延迟队列就是个带延迟功能的消息队列,相对于普通队列,它可以在指定时间消费掉消息. 延迟队列的应用场景: 1.新用户注册,10分钟后发送邮件或站内信. 2.用户下单后,30分钟未支付,订单自动作废. 我 ...
- Redis 为什么这么快? Redis 的有序集合 zset 的底层实现原理是什么? —— 跳跃表 skiplist
Redis有序集合 zset 的底层实现--跳跃表skiplist Redis简介 Redis是一个开源的内存中的数据结构存储系统,它可以用作:数据库.缓存和消息中间件. 它支持多种类型的数据结构,如 ...
- Redis中有序集合zset数据类型(增加(添加元素)、获取(获取指定范围元素、返回权值在min和max之间的成员、返回成员member的score值)、删除(删除指定元素和指定权值范围的元素))
sorted set,有序集合 元素为string类型 元素具有唯⼀性,不重复 [值不能重复] 每个元素都会关联⼀个double类型的score,表示权重,通过权重将元素从⼩到⼤排序 说明:没有修 ...
- php redis 搜索,PHP+Redis有序集合(zset)实现博客园阅读排行榜功能
许多网站都有排行榜的功能,比如球员人气榜单.阅读排行榜,对于一些小网站,通过查数据库就能实现排行榜的功能,但是对于稍微有点用户量而且还是实时排名的网站,使用一些关系型数据库如(MySQL.Oracle ...
- 每个选项设置上限的投票_使用Redis有序集合实现投票排行榜系统
需求 业务开发中多多少少会遇到这种需求,需要一个排行榜,它需要对列表中成员的票数进行排序. 在一个排行榜中,成员的名次是要根据当前票数动态变化的,如果使用Mysql,将会造成频繁的修改,Mysql的性 ...
- PythonRedis 无序集合set、有序集合zset操作
1.Redis控制台操作无序集合set 无序集合 元素为string类型 元素具有唯一性,不重复 redis操作: 设置 添加元素 SADD key member [member ...] 获取 返回 ...
- Redis—列表(List)、集合(Set)、哈希(Hash)、有序集合 Zset
Redis-列表List.集合Set.哈希Hash.有序集合 Zset 列表List 单键多值 常用命令 数据结构 Redis 集合(Set) 常用命令 数据结构 Redis 哈希(Hash) 常用命 ...
- redis有序集合zset详解
zset 概述 Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合. 不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低 ...
- Redis-07Redis数据结构--有序集合ZSet
文章目录 概述 有序集合的数据结构 Redis 有序集合的部分命令 spring-data-redis 对有序集合的封装 使用 Spring 操作有序集合 注意 代码 概述 有序集合和集合类似,只是说 ...
最新文章
- 楷书书法规则_硬笔书法入门学习“三步法”,让练字不再难
- python课程主要介绍哪些内容_Python课程详细介绍
- 云炬随笔20210714(1)
- css 两边宽度固定中间自适应宽度
- [翻译] VLDContextSheet
- SpringBoot Bean配置
- C语言中局部变量和全局变量 变量的存储类别
- java实验1机动车实验目的_《Java程序设计》实验指导书.doc
- Appium python adb命令
- easyui-datagrid加载时的效率低下,解决方案
- C# 通过Exchange server 发送邮件
- python基础(2)
- 抑郁症是不可告人的病吗?
- 金融网络直播室软件的功能介绍
- Matlab中配置vlfeat库
- python 导入自定义包
- C语言基本的语法规定
- 机械臂抓取学习笔记三
- 蔬菜类别二级计算机,国家标准:蔬菜分类.doc
- javascript运行机制
热门文章
- 物联网技术周报第 106 期: 使用 27 个 Raspberry Pi Zero 制造 3D 人体扫描仪
- vue-cil+百度路书轨迹播放,removeOverlay单独删除覆盖物
- 富平春天服饰 怎么样呀
- R语言设置随机种子set.seed()函数使用
- 用c语言如何解压字符串,C语言 解压华为固件的实例代码
- 会深度学习就能为所欲为吗,先来一打表情包,再加个图说
- Easyui combotree 获取选中节点的值的方法
- xynuoj昆虫繁殖
- 用计算机名访问计算机,解决局域网用\\计算机名不能访问,用\\ip地址可访问
- win10修改计算机名