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

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

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

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

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

能够按名次查询排名前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为排行榜名称,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的有序集合实现排行榜功能相关推荐

  1. Redis—列表(List)、集合(Set)、哈希(Hash)、有序集合 Zset

    Redis-列表List.集合Set.哈希Hash.有序集合 Zset 列表List 单键多值 常用命令 数据结构 Redis 集合(Set) 常用命令 数据结构 Redis 哈希(Hash) 常用命 ...

  2. redis 读取mysql数据类型_认识Redis与Redis的数据类型

    本文作为Redis的入门教程,旨在让大家对Redis有一个概念性和整体性的认识,并且可以快速上手,为深入Redis打下基础. 文章概要: 1. Redis的介绍 2. Redis与其他数据库的对比 3 ...

  3. redis集合数据过期_关于redis性能问题分析和优化

    一.如何查看Redis性能 info命令输出的数据可以分为10个分类,分别是: server,clients,memory,persistence,stats,replication,cpu,comm ...

  4. redis mysql 原子计数器_使用redis的increment()方法实现计数器功能案例

    一直知道redis可以用来实现计数器功能,但是之前没有实际使用过,昨天碰到一个需求:用户扫码当天达到20次即提示:当日扫码次数达到上限! 当时就想到使用redis的递增方法increment()来实现 ...

  5. list redis 怎样做排行_学 Redis 的 7000 字小结!!!

    来源:http://rrd.me/ekrCq Redis 简介 Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key - value 数据库 Redis 与 其他 key - va ...

  6. 为什么redis取出来是null_跳表:为什么Redis一定要用跳表来实现有序集合

    上两节我们讲了二分查找算法.当时我讲到,因为二分查找底层依赖的是数组随机访问的特性,所以只能用数组来实现.如果数据存储在链表中,就真的没法用二分查找算法了吗? 实际上,我们只需要对链表稍加改造,就可以 ...

  7. Redis源码剖析(十二)有序集合跳表实现

    有序集合是Redis对象系统中的一部分,其底层采用跳表和压缩列表两种形式存储,在上一篇介绍了跳表实现,就趁热打铁看一下有序集合的跳表实现 本篇主要涉及的是有序集合添加数据的命令,后面会看到,在命令的底 ...

  8. 17 | 跳表:为什么Redis一定要用跳表来实现有序集合?

    问题:如果数据存储在链表中,就真的没法用二分查找算法了吗?可以对链表进行"改造",就可以支持类似"二分"的查找算法. 跳表 定义:对链表经过改造之后的数据结构叫 ...

  9. redis订阅怎么退出_关于redis,学会这8点就够了

    1,redis是什么 redis是一种支持Key-Value等多种数据结构的存储系统.可用于缓存,事件发布或订阅,高速队列等场景.该数据库使用ANSI C语言编写,支持网络,提供字符串,哈希,列表,队 ...

最新文章

  1. Java中的occur_time,PLSQL报错: ORA-12170:TNS connect timeout occurred
  2. boost::proto::make_expr相关的测试程序
  3. session喜欢丢值且占内存,Cookis不安全,用什么可以代替呢?
  4. LeetCode 441. 排列硬币(数学解方程)
  5. delstr函数python_Python Day26:多态、封装、内置函数:__str__、__del__、反射(反省)、动态导入模块...
  6. git head指向老版本_Git最全总结
  7. 大并发下Timeout waiting for connection from pool 解决方案
  8. OutOfMemoryError(内存溢出)解决办法
  9. Ox2ac是C语言常量,计算机等级考试二级C++语言程序设计标准预测试卷二
  10. ispostback之坑
  11. 非线性动力学_利用非线性动力学系统研究混沌现象
  12. 滤波器截止频率理解?
  13. 说一说Qpython3在Android手机上的应用
  14. [Place 30-58] IO placement is infeasible. Number of unplaced terminals (1) is greate
  15. 通过云打码实现验证码识别
  16. Solidity入门-开发众筹智能合约
  17. mt950报文解析_系列之五 | MT759报文升级内容解析
  18. jsp、servlet与javabean的区别180110
  19. 计算机三级考试网络技术——速成
  20. Kafka GroupCoordinator机制(十六):GroupCoordinator之LeaveGroupRequest分析

热门文章

  1. 如何用Xshell设置代理服务器?
  2. linux 外部内核模块设定 CFLAGS
  3. day01 HTML课堂笔记
  4. esp32连接海萤物联网显示南京室外温度
  5. scrcpy: 在电脑上使用Android手机
  6. 量子计算机,开启中国速度比人类历史上第一台电子管计算机和晶体管计算机运行速度快10—100倍...
  7. 基于java SSM框架+微信小程序实现电子书城阅读器演示【附项目源码+论文说明】
  8. 应用软件运行提示缺少*.dll文件的解决方法
  9. 测试人员如何提高产品质量
  10. java基础-IO编程