使用Redis的有序集合實現排行榜功能

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

一個典型的遊戲排行榜包括如下常見功能:數據庫

  1. 可以記錄每一個玩家的分數;
  2. 可以對玩家的分數進行更新;
  3. 可以查詢每一個玩家的分數和名次;
  4. 可以按名次查詢排名前N名的玩家;
  5. 可以查詢排在指定玩家先後M名的玩家。

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

因爲一個玩家名次上升x位將會引發x+1位玩家的名次發生變化(包括該玩家),若是採用傳統數據庫(好比MySQL)來實現排行榜,當玩家人數較多時,將會致使對數據庫的頻繁修改,性能得不到知足,因此咱們只能另想它法。服務器

Redis做爲NoSQL中的一員,近年來獲得普遍應用。與Memcached相比,Redis擁有更多的數據類型和操做接口,具備更大的適用範圍,其中的有序集合(sorted set,也稱爲zset)就很是適合於排行榜的構建。下面簡要總結一下。微信

1. Redis的安裝

Ubuntu下安裝Redis很是簡單,執行以下命令便可:數據結構

$ sudo apt-get install redis-server函數

安裝完畢,運行命令行客戶端redis-cli就能夠訪問本地redis服務器。性能

$ redis-cli redis 127.0.0.1:6379>優化

若是要使用最新版本,須要到Redis官網(redis.io)下載最新的代碼自行編譯,步驟略。ui

2. ZSet的經常使用命令

有序集合首先是集合,其成員(member)具備惟一性,其次,每一個成員關聯了一個分數(score),使得成員能夠按照分數排序。關於有序集合的介紹見redis.io/topics/data…,其命令見redis.io/commands#so…

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

假設lb爲排行榜名稱,user一、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
> 1) 「user3」
> 2) 「95」
> 3) 「user2」
> 4) 「95」
> 5) 「user4」
> 6) 「90」
> 7) 「user1」
> 8) 「89」
複製代碼

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

> redis 127.0.0.1:6379> zrevrange lb 0 2 withscores
> 1) 「user3」
> 2) 「95」
> 3) 「user2」
> 4) 「95」
> 5) 「user4」
> 6) 「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
> 1) 「user4」
> 2) 「96」
> 3) 「user3」
> 4) 「95」
> 5) 「user2」
> 6) 「95」
> 7) 「user1」
> 8) 「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
> 1) 「user3」
> 2) 「95」
> 3) 「user2」
> 4) 「95」
> 5) 「user1」
> 6) 「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)
複製代碼

3. 相同分數問題

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

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

要解決這個問題,咱們能夠考慮在分數中加入時間戳,計算公式爲:

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

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

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

第一個問題是小問題,採用秒爲時間戳可能區分度還不夠,若是同一秒出現兩個分數相同的仍然會出現前面的問題,固然咱們能夠選擇精度更高的時間戳,但在實際場景中,同一秒誰排前面已經可有可無。

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

若是Redis的分數類型爲int64,咱們就沒有上面的煩惱。說到這裏,其實Redis真應該再額外提供一個int64類型的ZSet,但目前只能是幻想,除非本身改其源碼。

既然Redis也不能完美解決排行榜問題,那最終是否是有必要本身實現一個專門的排行榜數據結構呢?畢竟實際應用中的排行榜有不少能夠優化的地方,比玩家呈金字塔分佈,越是低分段玩家數量越多,同一分數擁有大量玩家,玩家增長一分均可能超越不少玩家,這就爲優化提供了可能。

本文亦在微信公衆號【小道資訊】發佈,歡迎掃碼關注!

相關文章
相關標籤/搜索