PHP+Redis 有序集合实现 24 小时排行榜实时更新

基本介绍

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 2^32 - 1^ (4294967295, 每个集合可存储40多亿个成员)。
有序集合首先是集合,其成员(member)具有唯一性,其次,每个成员关联了一个分数(score),使得成员可以按照分数排序。

需求描述

设想在一个游戏中,有上百万的玩家数据,如果现在需要你根据玩家的经验值整理一个前10名的排行榜,你会怎么做呢?一般的做法是写一条类似下面这条sql语句的方式来获取:

    select * from game_socre order by score desc limit 0,20

这种方式在数据量较小的情况下可行,但是在数据量大的情况下查询速度将变慢,特别是还需要联表查询时,速度下降的就更明显了。

实现

这时你可以考虑使用redis来实现这个功能。
实现这个功能主要用到的redis数据类型是redis的有序集合zset。zset 是set 类型的一个扩展,比原有的类型多了一个顺序属性.此属性在每次插入数据时会自动调整顺序值,保证value值按照一定顺序连续排列。
主要的实现思路是:
1、在一个新的玩家参与到游戏中时,在redis中的zset中新增一条记录(记录内容看具体的需求)score为0
2、当玩家的经验值发生变化时,修改该玩家的score值
3、使用redis的ZREVRANGE方法获取排行榜
返回有序集key中,指定区间内的成员。其中成员的位置按score值递减(从大到小)来排列。具有相同score值的成员按字典序的反序排列。 除了成员按score值递减的次序排列这一点外,ZREVRANGE命令的其他方面和ZRANGE命令一样。
redis 127.0.0.1:6379> ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN

  • 1、数据准备

php+redis有序集合实现24小时排行榜实时更新

  • 2、获取score高分top10排名(ZREVRANGE为降序,ZRANGE为升序)

php+redis有序集合实现24小时排行榜实时更新

  • 3、查看用户ee的实际排名(ZREVRANK为降序,ZRANK为升序)、实时分数

php+redis有序集合实现24小时排行榜实时更新

进一步需求

需要实现最近的24小时用户积分排行榜,并统计前10名的玩家和积分

实现

主要的实现思路是:
利用ZADD按小时划分添加用户的积分信息,然后用ZUNIONSTORE并集实现24小时的游戏积分总和,实现“24小时排行榜”;(如果有更好的思路,能够在下方留言不吝赐教一下就更好了)

    ZUNIONSTORE destination numkeys key [key ...]
    Redis Zunionstore 命令计算给定的一个或多个有序集的并集,其中给定 key 的数量必须以 numkeys 参数指定,并     将该并集(结果集)储存到 destination 。
    默认情况下,结果集中某个成员的分数值是所有给定集下该成员分数值之和 。

可能碰到的问题

  • 1、相同分数问题
    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,但目前只能是幻想,除非自己改其源码。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 4年前 自动加精
xugege
讨论数量: 3

利用 ZADD 按小时划分添加用户的积分信息,然后用 ZUNIONSTORE 并集实现 24 小时的游戏积分总和

博主你好~,这句有点没太看懂,能给个例子看一看吗?

4年前 评论
小烦

哇,讲道理,豁然开朗啊真的~
万分感谢~

4年前 评论

有序集合,常用于需要排名的地方,相当好用。当然你也可以用数据库,实现只不过麻烦点。 常用于的场景,比如

  1. 游戏中消费排行榜
  2. 游戏中冲值排行榜
  3. 游戏币获取排行榜

等等

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!