首頁  >  文章  >  資料庫  >  實例詳解Redis實現排行榜及相同積分按時間排序功能的實現

實例詳解Redis實現排行榜及相同積分按時間排序功能的實現

WBOY
WBOY轉載
2022-08-26 14:09:491632瀏覽

推薦學習: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
"5"
> zincrby z1 4 c
"5"

// 查询排行榜所有的队伍
> 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部分由腳本外程式計算好傳入。

分頁查詢排行榜,查詢隊伍的排名等功能都可以繼續使用上面的指令。

積分相同是依時間排序,並列排名

所謂並列排行榜,就是存在相同排名情況的排行榜。

我們預期的結果如下表:

12#245
隊伍ID 貢獻值
##a 100
#b 99
c 99
d 88
e 87
#######

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

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视频教程

以上是實例詳解Redis實現排行榜及相同積分按時間排序功能的實現的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:jb51.net。如有侵權,請聯絡admin@php.cn刪除