共計 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 網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!