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


參照鏈接 https://juejin.cn/post/6844903795131056135

import redis
r=redis.Redis(host='localhost',port=6379)
# r.set(1,'W5c0-qkAb-e2Xw-U7wS')   # 存鍵值
# r.expire(1,30)    # 設置30s時間
# print(r.ttl(1))   # 查看到期時間

r.zadd('myrank','uid1',1)
r.zincrby('myrank','uid1',200)  # 依次增加
r.zadd('myrank','uid2',2)
r.zadd('myrank','uid3',3)
aaa=r.zrange('myrank',-2,1,False,True) 

  

 

 

 

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

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

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

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

由於一個玩家名次上升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)下載最新的代碼自行編譯,步驟略。

2. ZSet的常用命令

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

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

假設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
> 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位即可。

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




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM