共计 3669 个字符,预计需要花费 10 分钟才能阅读完成。
这篇“Redis 如何实现排行榜及相同积分按时间排序功能”文章的知识点大部分人都不太理解,所以丸趣 TV 小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Redis 如何实现排行榜及相同积分按时间排序功能”文章吧。
在日常的开发中,经常会碰到需要对用户的分值等进行排序,比如在游戏里面需要对战斗力进行排行,在组队活动中需要对各个队伍的贡献值进行排行,在微信中需要对各个好友的步数进行排行,此时一般会选择 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 小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注丸趣 TV 行业资讯频道。