Redis 實現實時排行榜

2021-09-21 17:57:24 字數 4567 閱讀 7206

面試:你懂什麼是分布式系統嗎?redis分布式鎖都不會?>>>

遊戲中存在各種各樣的排行榜,比如玩家的等級排名、分數排名等。玩家在排行榜中的名次是其實力的象徵,位於榜單前列的玩家在虛擬世界中擁有無尚榮耀,所以名次也就成了核心玩家的追求目標。

乙個典型的遊戲排行榜包括以下常見功能:

能夠記錄每個玩家的分數;

能夠對玩家的分數進行更新;

能夠查詢每個玩家的分數和名次;

能夠按名次查詢排名前n名的玩家;

能夠查詢排在指定玩家前後m名的玩家。

更進一步,上面的操作都需要在短時間內實時完成,這樣才能最大程度發揮排行榜的效用。

由於乙個玩家名次上公升x位將會引起x+1位玩家的名次發生變化(包括該玩家),如果採用傳統資料庫(比如mysql)來實現排行榜,當玩家人數較多時,將會導致對資料庫的頻繁修改,效能得不到滿足,所以我們只能另想它法。

redis作為nosql中的一員,近年來得到廣泛應用。與memcached相比,redis擁有更多的資料型別和操作介面,具有更大的適用範圍,其中的有序集合(sorted set,也稱為zset)就非常適合於排行榜的構建。下面簡要總結一下。

ubuntu下安裝redis非常簡單,執行如下命令即可:

有序集合首先是集合,其成員(member)具有唯一性,其次,每個成員關聯了乙個分數(score),使得成員可以按照分數排序。關於有序集合的介紹見其命令見

下面介紹幾個能用於排行榜的命令。

假設lb為排行榜名稱,user1、user2等為玩家唯一標識。

1) zadd——設定玩家分數

命令格式:zadd 排行榜名稱 分數 玩家標識時間複雜度:o(log(n))

下面設定了4個玩家的分數,如果玩家分數已經存在,則會覆蓋之前的分數。

redis 127.0.0.1:6379> zadd lb 89 user1 (integer) 1 redis 127.0.0.1:6379> zadd lb 95 user2 (integer) 1 redis 127.0.0.1:6379> zadd lb 95 user3 (integer) 1 redis 127.0.0.1:6379> zadd lb 90 user4 (integer) 1

2) zscore——檢視玩家分數命令格式:zscore 排行榜名稱 玩家標識時間複雜度:o(1) 下面是檢視user2這個玩家在lb排行榜中的分數。

redis 127.0.0.1:6379> zscore lb user2 「95」

3) zrevrange——按名次檢視排行榜命令格式:zrevrange 排行榜名稱 起始位置 結束位置 [withscores]時間複雜度:o(log(n)+m)

由於排行榜一般是按照分數由高到低排序的,所以我們使用zrevrange,而命令zrange是按照分數由低到高排序。

起始位置和結束位置都是以0開始的索引,且都包含在內。如果結束位置為-1則檢視範圍為整個排行榜。

帶上withscores則會返回玩家分數。

下面為檢視所有玩家分數。

redis 127.0.0.1:6379> zrevrange lb 0 -1 withscores

「user3」

「95」

「user2」

「95」

「user4」

「90」

「user1」

「89」

下面為查詢前三名玩家分數。

redis 127.0.0.1:6379> zrevrange lb 0 2 withscores

「user3」

「95」

「user2」

「95」

「user4」

「90」

4) zrevrank——檢視玩家的排名命令格式:zrevrank 排行榜名稱 玩家標識時間複雜度:o(log(n))

與zrevrange類似,zrevrank是以分數由高到低的排序返回玩家排名(實際返回的是以0開始的索引),對應的zrank則是以分數由低到高的排序返回排名。

下面是查詢玩家user3和user4的排名。

redis 127.0.0.1:6379> zrevrank lb user3 (integer) 0 redis 127.0.0.1:6379> zrevrank lb user1 (integer) 3

5) zincrby——增減玩家分數命令格式:zincrby 排行榜名稱 分數增量 玩家標識時間複雜度:o(log(n))

有的排行榜是在變更時重新設定玩家的分數,而還有的排行榜則是以增量方式修改玩家分數,增量可正可負。如果執行zincrby時玩家尚不在排行榜中,則認為其原始分數為0,相當於執行zdd。

下面將user4的分數增加6,使其名次上公升到第一位。

redis 127.0.0.1:6379> zincrby lb 6 user4 「96」 redis 127.0.0.1:6379> zrevrange lb 0 -1 withscores

「user4」

「96」

「user3」

「95」

「user2」

「95」

「user1」

「89」

6) zrem——移除某個玩家

命令格式:zrem 排行榜名稱 玩家標識時間複雜度:o(log(n)) 下面移除玩家user4。

redis 127.0.0.1:6379> zrem lb user4 (integer) 1 redis 127.0.0.1:6379> zrevrange lb 0 -1 withscores

「user3」

「95」

「user2」

「95」

「user1」

「89」

7) del——刪除排行榜

命令格式:del 排行榜名稱

排行榜物件在我們首次呼叫zadd或zincrby時被建立,當我們要刪除它時,呼叫redis通用的命令del即可。

redis 127.0.0.1:6379> del lb (integer) 1 redis 127.0.0.1:6379> get lb (nil)

免費的方案總有那麼一些不完美。從前面的例子我們可以看到,user2和user3具有相同的分數,但在按分數逆序排序時,user3排在了user2前面。而在實際應用場景中,我們更希望看到user2排在user3前面,因為user2比user3先加入排行榜,也就是說user2先到達該分數。

但redis在遇到分數相同時是按照集合成員自身的字典順序來排序,這裡即是按照」user2″和」user3″這兩個字串進行排序,以逆序排序的話user3自然排到了前面。

要解決這個問題,我們可以考慮在分數中加入時間戳,計算公式為:

帶時間戳的分數 = 實際分數*10000000000 + (9999999999 – timestamp)

timestamp我們採用系統提供的time()函式,也就是2023年1月1日以來的秒數,我們採用32位的時間戳(這能堅持到2023年),由於32位時間戳是10位十進位制整數(最大值4294967295),所以我們讓時間戳佔據低10位(十進位制整數),實際分數則擴大10^10倍,然後把兩部分相加的結果作為zset的分數。考慮到要按時間倒序排列,所以時間戳這部分需要顛倒一下,這便是用9999999999減去時間戳的原因。當我們要讀取玩家實際分數時,只需去掉後10位即可。

初步看起來這個方案還不錯,但這裡面有兩個問題。

第乙個問題是小問題,採用秒為時間戳可能區分度還不夠,如果同一秒出現兩個分數相同的仍然會出現前面的問題,當然我們可以選擇精度更高的時間戳,但在實際場景中,同一秒誰排前面已經無關緊要。

第二個問題是大問題,因為redis的分數型別採用的是double,64位雙精度浮點數只有52位有效數字,它能精確表達的整數範圍為-2^53到2^53,最高只能表示16位十進位制整數(最大值為9007199254740992,其實連16位也不能完整表示)。這就是說,如果前面時間戳佔了10位的話,分數就只剩下6位了,這對於某些排行榜分數來說是不夠用的。我們可以考慮縮減時間戳位數,比如從2023年1月1日開始計時,但這仍然增加不了幾位。或者減少區分度,以分鐘、小時來作為時間戳單位。

如果redis的分數型別為int64,我們就沒有上面的煩惱。說到這裡,其實redis真應該再額外提供乙個int64型別的zset,但目前只能是幻想,除非自己改其原始碼。

既然redis也不能完美解決排行榜問題,那最終是不是有必要自己實現乙個專門的排行榜資料結構呢?畢竟實際應用中的排行榜有很多可以優化的地方,比玩家呈金字塔分布,越是低分段玩家數量越多,同一分數擁有大量玩家,玩家增加一分都可能超越很多玩家,這就為優化提供了可能。

使用redis中的zset實現實時排行榜

redis在業務開發中會被頻繁使用,zset是其中一種特殊用法,zset具排行榜的天然特性,我前幾個月在一次開發中使用到了zset,就是因為涉及到要實現乙個排行榜,那是我第一次用到zset,雖然之前都看過redis幾種資料型別的資料結構及其使用方法,但是真正用起來的時候,還是有一些細節的東西要處理的...

redis實現排行榜

排行榜功能是乙個很普遍的需求。設想在乙個遊戲中,有上百萬的玩家資料,如果現在需要你根據玩家的經驗值整理乙個前20名的排行榜,你會怎麼做呢?你不可能 order by limit 去實現 select from game socre order by score desc limit 0,20 使用 ...

Redis 實現排行榜

不再介紹資料庫做實時排行榜的弊端,直接介紹redis的有序集合的強大作用。有序集合的資料和集合一樣,不能重複,但每個元素又可以關聯乙個分數,這個分數可以重複。需要注意的是,redis版本和命令變化較大,注意執行環境。執行環境 redis 庫版本 3.3.11 redis版本 3.2.1 生成資料 i...