redis实现积分排行榜

在项目开发中常常遇到一些积分排行的问题。
一个典型的积分行榜包括以下常见功能:

  1. 能够记录每个用户的分数;
  2. 能够对用户的分数进行更新;
  3. 能够查询每个用户的分数和名次;
  4. 能够按名次查询排名前N名的用户;
  5. 能够查询排在指定用户前后M名的用户;

因为排行榜的实时性,所以这个需要在第一时间进行查询并展示。由于一个用户的名次上升x位将会引起x+1位用户的名次发生变化(包括该用户),如果采用传统数据库(比如MySQL)来实现排行榜,当用户人数较多时,将会导致对数据库的频繁修改,从而降低数据库的性能。所以只能另辟蹊径,来解决排名的问题。

redis作为NoSQL中的一员,近年来得到广泛应用,Redis拥有更多的数据类型和操作接口,具有更大的适用范围,其中的有序集合(sorted set,也称为zset)就非常适合于排行榜的构建。

有序集合首先是集合,其成员(member)具有唯一性,其次,每个成员关联了一个分数(score),使得成员可以按照分数排序。
关于有序集合的介绍见http://redis.io/topics/data-types#sorted-sets
其命令见http://redis.io/commands#sorted_set

下面介绍几个能用于排行榜的命令。

假设TeamRank为排行榜名称,user1、user2等为用户唯一标识(现实中可能是用户 ID)。

1) zadd——设置用户分数

  • 命令格式:zadd 排行榜名称 分数 用户标识 时间复杂度:O(log(N))
设置4个用户的分数,如果用户分数已经存在,则会覆盖之前的分数
127.0.0.1:6379> zadd TeamRank 89 user1
(integer) 1
127.0.0.1:6379> zadd TeamRank 95 user2
(integer) 1
127.0.0.1:6379> zadd TeamRank 95 user3
(integer) 1
127.0.0.1:6379> zadd TeamRank 90 user4
(integer) 1

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2) zscore——查看用户分数

  • 命令格式:zscore 排行榜名称 用户标识 时间复杂度:O(1)
	查看user2这个用户在TeamRank排行榜中的分数
	127.0.0.1:6379> zscore TeamRank user3
	"95"

  
  
  • 1
  • 2
  • 3

3) zrevrange——按名次查看排行榜

  • 命令格式:zrevrange 排行榜名称 起始位置 结束位置 [withscores] 时间复杂度:O(log(N)+M)

  • 由于排行榜一般是按照分数由高到低排序的,所以我们使用zrevrange, 而命令zrange是按照分数由低到高排序。

  • 起始位置和结束位置都是以0开始的索引,且都包含在内。如果结束位置为 -1则查看范围为整个排行榜。

  • 带上withscores则会返回用户分数。

	查看所有用户分数
	127.0.0.1:6379> zrevrange TeamRank 0 -1 withscores 
	1) "user3"
	2) "95"
	3) "user2"
	4) "95"
	5) "user4"
	6) "90"
	7) "user1"
	8) "89"

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
查询前三名用户分数。
127.0.0.1:6379> zrevrange TeamRank 0 2 withscores 
1) "user3"
2) "95"
3) "user2"
4) "95"
5) "user4"
6) "90"

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4) zrevrank——查看用户的排名

  • 命令格式:zrevrank 排行榜名称 用户标识 时间复杂度:O(log(N))

  • 与zrevrange类似,zrevrank是以分数由高到低的排序返回用户排名(实际返回的是以0开始的索引),对应的zrank则是以分数由低到高的排序返回排名。

	查询用户user3和user4的排名。
	127.0.0.1:6379> zrevrank TeamRank user3
	(integer) 0
	127.0.0.1:6379> zrevrank TeamRank user4
	(integer) 2

  
  
  • 1
  • 2
  • 3
  • 4
  • 5

5) zincrby——增减用户分数

  • 命令格式:zincrby 排行榜名称 分数增量 用户标识 时间复杂度:O(log(N))

  • 有的排行榜是在变更时重新设置用户的分数,而还有的排行榜则是以增量方式修改用户分数,增量可正可负。如果执行zincrby时用户尚不在排行榜中,则认为其原始分数为0,相当于执行zdd。

	将user4的分数增加6,使其名次上升到第一位。
	127.0.0.1:6379> zincrby TeamRank 6 user4
	"96"
	127.0.0.1:6379> zrevrank TeamRank user4
	(integer) 0

  
  
  • 1
  • 2
  • 3
  • 4
  • 5

6) zrem——移除某个用户

  • 命令格式:zrem 排行榜名称 用户标识
    时间复杂度:O(log(N))
	移除用户4
	127.0.0.1:6379> zrem TeamRank user4
	(integer) 1
	127.0.0.1:6379> zrevrange TeamRank 0 -1 withscores
	1) "user3"
	2) "95"
	3) "user2"
	4) "95"
	5) "user1"
	6) "89"

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

7) del——删除排行榜

  • 命令格式:del 排行榜名称

  • 排行榜对象在我们首次调用zadd或zincrby时被创建,当我们要删除它时,调用redis通用的命令del即可。

	删除 TeamRank 排行榜
	127.0.0.1:6379> del TeamRank 
	(integer) 1
	127.0.0.1:6379> zrevrange TeamRank 0 -1 withscores
	(empty array)	

  
  
  • 1
  • 2
  • 3
  • 4
  • 5

分数相同的排行处理

  • 免费的方案总有那么一些不完美。从前面的例子我们可以看到,user2和user3具有相同的分数,但在按分数逆序排序时,user3排在了user2前面。而在实际应用场景中,我们更希望看到user2排在user3前面,因为user2比user3先加入排行榜,也就是说user2先到达该分数。但Redis在遇到分数相同时是按照集合成员自身的字典顺序来排序,这里即是按照”user2″和”user3″这两个字符串进行排序现(现实中可能是用户 ID),以逆序排序的话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位了,这对于某些排行榜分数来说是不够用的。我们可以考虑缩减时间戳位数,比如从2019年1月1日开始计时,推荐采用
    7位+8位 的的模式来进行排行 99999999 秒=1157.40739583
    天也就是可以存储三年左右的数据,而且积分可以达到七位数百万级别最高积分支持九百九十九万九千九百九十九,能满足大部分积分的需求。

    计算公式为:时间戳的分数=实际分数*100000000+(99999999 –(timestamp - 开始时间timestamp))

后记

通过 redis 来存储排行榜,
第一减少了数据库的修改,减少了数据库的频繁变动。
第二减少了数据库的查询,减少了数据库的查询压力
第三加快了排名的查询速度。

  
  
  • 1
  • 2
  • 3
  • 4
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,需要明确排行榜实现方式。一般而言,排行榜可以使用有序集合(sorted set)来实现,其中成员为需要排名的对象,分数为该对象的分数(或者得分、积分等),分数越高排名越靠前。 接下来,我们针对日榜、周榜和月榜分别进行实现。 ## 日榜 对于日榜,我们可以使用 Redis 的 ZINCRBY 命令来增加某个对象的分数,并使用 ZREVRANGE 命令来获取分数最高的前 N 个对象,从而得到日榜排名。 具体实现步骤如下: 1. 每当一个对象获得分数时,使用 ZINCRBY 命令将其分数增加; 2. 使用 ZREVRANGE 命令获取排序后的前 N 个对象。 例如,假设我们要维护一个日榜,其中每个用户有一个得分,我们可以使用以下代码实现: ```php // 增加用户得分 $redis->zincrby('daily_ranking', $score, $user_id); // 获取前 N 名用户 $result = $redis->zrevrange('daily_ranking', 0, $n - 1, 'WITHSCORES'); ``` 其中,$score 为用户得分,$user_id 为用户 ID,$n 为需要获取的前 N 名用户数量。 ## 周榜 对于周榜,我们可以使用 Redis 的事务(transaction)来实现。具体实现步骤如下: 1. 获取当前日期所在的周的编号; 2. 在 Redis 中创建一个键名为 "weekly_ranking:周编号" 的有序集合(sorted set); 3. 使用事务,将该周内所有用户的得分分别增加到对应的有序集合中; 4. 使用 ZREVRANGE 命令获取分数最高的前 N 个对象。 例如,假设我们要维护一个周榜,其中每个用户有一个得分,我们可以使用以下代码实现: ```php // 获取当前日期所在的周的编号 $week_number = date('W'); // 开启 Redis 事务 $redis->multi(); // 增加用户得分 $redis->zincrby("weekly_ranking:$week_number", $score, $user_id); // 获取前 N 名用户 $redis->zrevrange("weekly_ranking:$week_number", 0, $n - 1, 'WITHSCORES'); // 执行事务 $result = $redis->exec(); ``` 其中,$score 为用户得分,$user_id 为用户 ID,$n 为需要获取的前 N 名用户数量。 ## 月榜 对于月榜,与周榜类似,我们可以使用 Redis 的事务来实现。具体实现步骤如下: 1. 获取当前日期所在的月份; 2. 在 Redis 中创建一个键名为 "monthly_ranking:年份-月份" 的有序集合(sorted set); 3. 使用事务,将该月内所有用户的得分分别增加到对应的有序集合中; 4. 使用 ZREVRANGE 命令获取分数最高的前 N 个对象。 例如,假设我们要维护一个月榜,其中每个用户有一个得分,我们可以使用以下代码实现: ```php // 获取当前日期所在的月份 $month = date('Y-m'); // 开启 Redis 事务 $redis->multi(); // 增加用户得分 $redis->zincrby("monthly_ranking:$month", $score, $user_id); // 获取前 N 名用户 $redis->zrevrange("monthly_ranking:$month", 0, $n - 1, 'WITHSCORES'); // 执行事务 $result = $redis->exec(); ``` 其中,$score 为用户得分,$user_id 为用户 ID,$n 为需要获取的前 N 名用户数量。 需要注意的是,以上实现仅供参考,具体实现方式还要根据实际情况进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值