如何用Redis实现排行榜及相同积分按时间排序功能

72次阅读
没有评论

共计 3467 个字符,预计需要花费 9 分钟才能阅读完成。

本篇内容主要讲解“如何用 Redis 实现排行榜及相同积分按时间排序功能”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让丸趣 TV 小编来带大家学习“如何用 Redis 实现排行榜及相同积分按时间排序功能”吧!

需求:对组队活动中各个队伍的贡献值进行排行。

不考虑积分相同

Redis 的 Sorted Set 是 String 类型的有序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数 (score) 却可以重复。

下面先不考虑积分相同的情况,实现排行榜:

//  准备数据,其中 value 为每个队伍的 ID,score 为队伍的贡献值
  zadd z1 5 a 6 b 1 c 2 d 10 e
(integer) 5
//  分页查询排行榜所有的队伍和贡献值,要使用 zrevrange,而不是 zrange,贡献值越大越排在前面
  zrevrange z1 0 2 withscores
1)  e 
2)  10 
3)  b 
4)  6 
5)  a 
6)  5 
//  增加某个队伍的贡献值
  zincrby z1 3 d
  zincrby z1 4 c
//  查询排行榜所有的队伍
  zrevrange z1 0 -1 withscores
 1)  e 
 2)  10 
 3)  b 
 4)  6 
 5)  d 
 6)  5 
 7)  c 
 8)  5 
 9)  a 
10)  5 
//  查询某个队伍的排名
  zrevrank z1 d
(integer) 2

Redis 默认实现是相同分数的成员按字典顺序排序(09,AZ,a~z),上面使用的是 zrevrange,所以是倒序,所以相同分数排序就不能根据时间优先来排序。

积分相同按时间排序,排名唯一

在上面的实现中,如果两个队伍的贡献值相同,也就是积分值相同,无法根据时间的先后进行排行。

所以需要设计一个分数 = 贡献值 + 时间戳,谁分数大谁排前面,最后还要能根据分数能解析出来贡献值。

设计 1

使用整型存储分数值,redis 中 score 本身是一个 double 类型,能精确存储的最大整型数字为 2^53=9007199254740992(16 位)。而精确到毫秒的时间戳需要 13 位,此时留给存储贡献值只有 3 位数了,当前如果时间只要精确到秒,只需要 10 位,这样留给贡献值就有 6 位。

整体设计:高 3 位表示贡献值,低 13 位表示时间戳。

如果我们简单地把 score 结构由:贡献值 * 10^13 + 时间戳 拼凑,因为分数越大越靠前,而时间戳越小则越靠前,这样两部分的判断规则是相反的,无法简单把两者合成一起成为 score。

但是我们可以逆向思维,可以用同一个足够大的数 Integer.MAX 减去时间戳,时间戳越小,则得到的差值越大,这样我们就可以把 score 的结构改为:贡献值 * 10^13 + (Integer.MAX- 时间戳),这样就能满足我们的需求了。

设计 2

由于 redis 的 score 值是 double 类型,可以使用整数部分存储贡献值,小数部分存储时间戳,同样时间戳的部分使用一个最大值减去它。

这样,整体设计变为:分数 = 贡献值 + (Integer.MAX- 时间戳) * 10^-13

弊端:由于分数值是由两个变量来计算得出,所以在给队伍增加贡献值时,无法简单的使用之前的 zincrby 来改变 score 的值了,这样在并发情况下为队伍增加贡献值就会导致 score 值不准确。

错误情况模拟:

假设现在队伍 A 的贡献值为 10 队伍 A 中的队员 X 为队伍增加贡献值 1,在程序中算出 score 为 11.xxx 队伍 A 中的队员 Y 为队伍增加贡献值 1,在程序中算出 score 为 11.yyy 队伍 A 中的队员 X 调用 redis 的 zadd 命令设置队伍的贡献值为 11.xxx 队伍 A 中的队员 Y 调用 redis 的 zadd 命令设置队伍的贡献值为 11.yyy 最后算出队伍 A 的贡献值为 11,无法保证增加贡献值这一个操作的原子性。

此时需要借助 lua 脚本来保证计算和设置贡献值这两个操作的原子性:

//  其中 KEYS[1]为排行榜 key,KEYS[2]为队伍 ID
//  其中 ARGV[1]为增加的贡献值,ARGV[2]为 Integer.MAX- 时间戳
local score = redis.call(zscore , KEYS[1], KEYS[2]) 
if not(score) then
 score=0 
end 
score=math.floor(score) + tonumber(ARGV[1]) + tonumber(ARGV[2]) 
redis.call(zadd , KEYS[1], score, KEYS[2]) return 1

由于 redis 中无法使用时间函数,所以(Integer.MAX- 时间戳) * 10^-13 部分由脚本外程序计算好传入。

分页查询排行榜,查询队伍的排名等功能都可以继续使用上面的命令。

积分相同按时间排序,并列排名

所谓并列排行榜,就是存在相同排名情况的排行榜。

我们期望的结果如下表:

队伍 ID 贡献值排名 a1001b992c992d884e875

当然现实中也有排名不跳过的情况,我这里考虑的是排名跳过的情况。

redis 中 score 的设计还是采用上面的分数 = 贡献值 + (Integer.MAX- 时间戳) * 10^-13,只是在查询排名时需要进行计算。

比如要查上表中队伍 b 的排名,思路如下:

首先查到队伍 b 的 score

再查到跟队伍 b 的 score 的整数部分相同(也就是贡献值一样),排在第一个的队伍的 value(队伍 ID)

根据上一步得到的队伍 ID 查询此队伍的排名就是队伍 b 的排名

使用命令实现上面的步骤如下:

 zscore  排行榜 key teamId
  zrevrangebyscore(排行榜 key,  上一步得到的 score+1,  上一步得到的 score, limit, 0 , 1)
  zrevrank(排行榜 key,  上一步得到的 teamId)

为了性能考虑,可以使用下面的脚本一次查出来:

// KEYS[1]表示排行榜 key
// KEYS[2]表示要查询的队伍的 ID
local rank = 0 
local score = redis.call(zscore , KEYS[1], KEYS[2]) 
if not(score) then
 score=0 
else 
 score=math.floor(score) 
 local firstScore = redis.call(zrevrangebyscore , KEYS[1], score+1, score,  limit , 0, 1) 
 rank=redis.call(zrevrank , KEYS[1], firstScore[1]) 
end 
return {score,rank}

下面附上分页查询排行榜的脚本,假如一页 10 条,不用下面的脚本需要查询 10 次上面的脚本,如果连上面的脚本都没有使用的话就要查询 30 次 redis。

//  排行榜 key
// ARGV[1]分页起始偏移
// ARGV[2]分页结束偏移
local list = redis.call(zrevrange , KEYS[1], ARGV[1], ARGV[2],  withscores ) 
local result={} 
local i = 1 
for k,v in pairs(list) do 
 if k%2 == 0 then 
 local teamId = list[k-1] 
 local score = math.floor(v) 
 local firstScore = redis.call(zrevrangebyscore , KEYS[1], score+1, score,  limit , 0, 1) 
 local rank=redis.call(zrevrank , KEYS[1], firstScore[1]) 
 local l = {teamId=teamId, contributionValue=score, teamRank=rank+1} 
 result[i] = l i = i + 1 
 end 
end 
return cjson.encode(result)

此脚本使用了 cjson 库,返回的是一个 json。

到此,相信大家对“如何用 Redis 实现排行榜及相同积分按时间排序功能”有了更深的了解,不妨来实际操作一番吧!这里是丸趣 TV 网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-13发表,共计3467字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)