zoukankan      html  css  js  c++  java
  • 使用Redis的有序集合实现排行榜功能

    参照链接 https://juejin.cn/post/6844903795131056135

    import redis
    r=redis.Redis(host='localhost',port=6379)
    # r.set(1,'W5c0-qkAb-e2Xw-U7wS')   # 存键值
    # r.expire(1,30)    # 设置30s时间
    # print(r.ttl(1))   # 查看到期时间
    
    r.zadd('myrank','uid1',1)
    r.zincrby('myrank','uid1',200)  # 依次增加
    r.zadd('myrank','uid2',2)
    r.zadd('myrank','uid3',3)
    aaa=r.zrange('myrank',-2,1,False,True) 
    

      

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

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

    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官网(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位即可。

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



  • 相关阅读:
    SDUT2482二叉排序树
    POJ 3278Catch That Cow
    SDUT2140图结构练习——判断给定图是否存在合法拓扑序列
    SDUT2142数据结构实验之图论二:基于邻接表的广度优先搜索遍历
    SDUT2141数据结构实验图论一:基于邻接矩阵的广度优先搜索遍历
    SDUT1466双向队列
    搜索
    搜索
    贪心算法
    贪心算法
  • 原文地址:https://www.cnblogs.com/zhangshijiezsj/p/14203196.html
Copyright © 2011-2022 走看看