<b>2.6 有序集合</b>
有序集合相對于哈希、清單、集合來說會有一點點陌生,但既然叫有序集合,那麼它和集合必然有着聯系,它保留了集合不能有重複成員的特性,但不同的是,有序集合中的元素可以排序。但是它和清單使用索引下标作為排序依據不同的是,它給每個元素設定一個分數(score)作為排序的依據。如圖2-24所示,該有序集合包含kris、mike、frank、tim、martin、tom,它們的分數分别是1、91、200、220、250、251,有序集合提供了擷取指定分數和元素範圍查詢、計算成員排名等功能,合理的利用有序集合,能幫助我們在實際開發中解決很多問題。
有序集合中的元素不能重複,但是score可以重複,就和一個班裡的同學學号不能重複,但是考試成績可以相同。
表2-7給出了清單、集合、有序集合三者的異同點。
表2-7 給出了清單、集合和有序集合三者的異同點
資料結構 是否允許重複元素 是否有序 有序實作方式 應用場景
清單 是 是 索引下标 時間軸、消息隊列等
集合 否 否 無 标簽、社交等
有序集合 否 是 分值 排行榜系統、社交等
<b> </b>
<b>2.6.1 指令</b>
本節依舊按照集合内和集合外兩個次元對有序集合的指令進行介紹。
1.?集合内
(1)添加成員
zadd key score member [score member ...]
下面操作向有序集合user:ranking添加使用者tom和他的分數251:
127.0.0.1:6379> zadd user:ranking 251
tom
(integer) 1
傳回結果代表成功添加成員的個數:
127.0.0.1:6379> zadd user:ranking 1 kris
91 mike 200 frank 220 tim 250 martin
(integer) 5
有關zadd指令有兩點需要注意:
redis 3.2為zadd指令添加了nx、xx、ch、incr四個選項:
nx:member必須不存在,才可以設定成功,用于添加。
xx:member必須存在,才可以設定成功,用于更新。
ch:傳回此次操作後,有序集合元素和分數發生變化的個數
incr:對score做增加,相當于後面介紹的zincrby。
有序集合相比集合提供了排序字段,但是也産生了代價,zadd的時間複雜度為o(log(n)),sadd的時間複雜度為o(1)。
(2)計算成員個數
zcard key
例如下面操作傳回有序集合user:ranking的成員數為5,和集合類型的scard指令一樣,zcard的時間複雜度為o(1)。
127.0.0.1:6379> zcard user:ranking
(3)計算某個成員的分數
zscore key member
tom的分數為251,如果成員不存在則傳回nil:
127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zscore user:ranking test
(nil)
(4)計算成員的排名
zrank key member
zrevrank key member
zrank是從分數從低到高傳回排名,zrevrank反之。例如下面操作中,tom在zrank和zrevrank分别排名第5和第0(排名從0開始計算)。
127.0.0.1:6379> zrank user:ranking tom
127.0.0.1:6379> zrevrank user:ranking
(integer) 0
(5)删除成員
zrem key member [member ...]
下面操作将成員mike從有序集合user:ranking中删除。
127.0.0.1:6379> zrem user:ranking mike
傳回結果為成功删除的個數。
(6)增加成員的分數
zincrby key increment member
下面操作給tom增加了9分,分數變為了260分:
127.0.0.1:6379> zincrby user:ranking 9
"260"
(7)傳回指定排名範圍的成員
zrange
key start end [withscores]
zrevrange key start end [withscores]
有序集合是按照分值排名的,zrange是從低到高傳回,zrevrange反之。下面代碼傳回排名最低的是三個成員,如果加上withscores選項,同時會傳回成員的分數:
127.0.0.1:6379> zrange user:ranking 0 2
withscores
1) "kris"
2) "1"
3) "frank"
4) "200"
5) "tim"
6) "220"
127.0.0.1:6379> zrevrange user:ranking 0
2 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
(8)傳回指定分數範圍的成員
zrangebyscore key min max [withscores] [limit offset
count]
zrevrangebyscore key max min [withscores]
[limit offset count]
其中zrangebyscore按照分數從低到高傳回,zrevrangebyscore反之。例如下面操作從低到高傳回200到221分的成員,withscores選項會同時傳回每個成員的分數。[limit offset count]選項可以限制輸出的起始位置和個數:
127.0.0.1:6379> zrangebyscore
user:ranking 200 tinf withscores
1) "frank"
2) "200"
3) "tim"
4) "220"
127.0.0.1:6379> zrevrangebyscore
user:ranking 221 200 withscores
1) "tim"
2) "220"
同時min和max還支援開區間(小括号)和閉區間(中括号),-inf和+inf分别代表無限小和無限大:
user:ranking (200 +inf withscores
5) "tom"
6) "260"
(9)傳回指定分數範圍成員個數
zcount key min max
下面操作傳回200到221分的成員的個數:
127.0.0.1:6379> zcount user:ranking 200
221
(integer) 2
(10)删除指定排名内的升序元素
zremrangebyrank key start end
下面操作删除第start到第end名的成員:
127.0.0.1:6379> zremrangebyrank
user:ranking 0 2
(integer) 3?
(11)删除指定分數範圍的成員
zremrangebyscore key min max
下面操作将250分以上的成員全部删除,傳回結果為成功删除的個數:
127.0.0.1:6379> zremrangebyscore
user:ranking (250 +inf
(integer) 2?
2.?集合間的操作
将圖2-25的兩個有序集合導入到redis中。
圖2-25 有序集合user:ranking:1和user:ranking:2
127.0.0.1:6379> zadd user:ranking:1 1
kris 91 mike 200 frank 220 tim 250 martin
251 tom
(integer) 6
127.0.0.1:6379> zadd user:ranking:2 8
james 77 mike 625 martin 888 tom
(integer) 4
(1)交集
zinterstore destination numkeys key [key
...] [weights weight [weight ...]]
[aggregate sum|min|max]
這個指令參數較多,下面分别進行說明:
destination:交集計算結果儲存到這個鍵。
numkeys:需要做交集計算鍵的個數。
key [key ...]:需要做交集計算的鍵。
weights weight [weight ...]:每個鍵的權重,在做交集計算時,每個鍵中的每個member會将自己分數乘以這個權重,每個鍵的權重預設是1。
aggregate sum|min|max:計算成員交集後,分值可以按照sum(和)、min(最小值)、max(最大值)做彙總,預設值是sum。
下面操作對user:ranking:1和user:ranking:2做交集,weights和aggregate使用了預設配置,可以看到目标鍵user:ranking:1_inter_2對分值做了sum操作:
127.0.0.1:6379> zinterstore
user:ranking:1_inter_2 2 user:ranking:1
user:ranking:2
(integer) 3
127.0.0.1:6379> zrange
user:ranking:1_inter_2 0 -1 withscores
1) "mike"
2) "168"
4) "875"
6) "1139"
如果想讓user:ranking:2的權重變為0.5,并且聚合效果使用max,可以執行如下操作:
user:ranking:2 weights 1 0.5 aggregate max
2) "91"
4) "312.5"
6) "444"
(2)并集
zunionstore destination numkeys key [key
該指令的所有參數和zinterstore是一緻的,隻不過是做并集計算,例如下面操作是計算user:ranking:1和user:ranking:2的并集,weights和aggregate使用了預設配置,可以看到目标鍵user:ranking:1_union_2對分值做了sum操作:
127.0.0.1:6379> zunionstore
user:ranking:1_union_2 2 user:ranking:1
(integer) 7
user:ranking:1_union_2 0 -1 withscores
1)
"kris"
2)
"1"
3)
"james"
4)
"8"
5)
"mike"
6)
"168"
7)
"frank"
8)
"200"
9)
"tim"
10) "220"
11) "martin"
12) "875"
13) "tom"
14) "1139"
至此有序集合的指令基本介紹完了,表2-8是這些指令的時間複雜度,開發人員在使用對應的指令進行開發時,不僅要考慮功能性,還要了解相應的時間複雜度,防止由于使用不當造成應用方效率下降以及redis阻塞。
表2-8 有序集合指令的時間複雜度
指令 時間複雜度
zadd key score member [score member ...] o(k×log(n)),k是添加成員的個數,n是目前有序集合成員個數
zcard key o(1)
zscore key member o(1)
zrevrank key member o(log(n)),n是目前有序集合成員個數
zrem key member [member ...] o(k*log(n)),k是删除成員的個數,n是目前有序集合成員個數
zincrby key increment member o(log(n)),n是目前有序集合成員個數
zrevrange key start end [withscores] o(log(n) + k),k是要擷取的成員個數,n是目前有序集合成員個數
zrangebyscore key min max [withscores]
zrevrangebyscore key max min [withscores] o(log(n) + k),k是要擷取的成員個數,n是目前有序集合成員個數
zcount o(log(n)),n是目前有序集合成員個數
zremrangebyrank key start end o(log(n) + k),k是要删除的成員個數,n是目前有序集合成員個數
zremrangebyscore key min max o(log(n) + k),k是要删除的成員個數,n是目前有序集合成員個數
...] o(n*k)+o(m*log(m)),n是成員數最小的有序集合成員個數,k是有序集合的個數,m是結果集中成員個數
...] o(n)+o(m*log(m)),n是所有有序集合成員個數和,m是結果集中成員個數
<b>2.6.2 内部編碼</b>
有序集合類型的内部編碼有兩種:
ziplist(壓縮清單):當有序集合的元素個數小于zset-max-ziplist-entries配置(預設128個),同時每個元素的值都小于zset-max-ziplist-value配置(預設64位元組)時,redis會用ziplist來作為有序集合的内部實作,ziplist可以有效減少記憶體的使用。
skiplist(跳躍表):當ziplist條件不滿足時,有序集合會使用skiplist作為内部實作,因為此時ziplist的讀寫效率會下降。
下面用示例來說明:
1)當元素個數較少且每個元素較小時,内部編碼為skiplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2
30 e3
127.0.0.1:6379> object encoding zsetkey
"ziplist"
2.1)當元素個數超過128個,内部編碼變為ziplist:
30 e3 12 e4 ...忽略... 84 e129
(integer) 129
"skiplist"
2.2)當某個元素大于64位元組時,内部編碼也會變為hashtable:
127.0.0.1:6379> zadd zsetkey 20
"one string is bigger than 64 byte.............
..................."
<b>2.6.3 使用場景</b>
有序集合比較典型的使用場景就是排行榜系統。例如視訊網站需要對使用者上傳的視訊做排行榜,榜單的次元可能是多個方面的:按照時間、按照播放數量、按照獲得的贊數。本節使用贊數這個次元,記錄每天使用者上傳視訊的排行榜。主要需要實作以下4個功能。
(1)添加使用者贊數
例如使用者mike上傳了一個視訊,并獲得了3個贊,可以使用有序集合的zadd和zincrby功能:
zadd user:ranking:2016_03_15 mike 3
如果之後再獲得一個贊,可以使用zincrby:
zincrby user:ranking:2016_03_15 mike 1
(2)取消使用者贊數
由于各種原因(例如使用者登出、使用者作弊)需要将使用者删除,此時需要将使用者從榜單中删除掉,可以使用zrem。例如删除成員tom:
zrem user:ranking:2016_03_15 mike
(3)展示擷取贊數最多的十個使用者
此功能使用zrevrange指令實作:
zrevrangebyrank user:ranking:2016_03_15 0 9
(4)展示使用者資訊以及使用者分數
此功能将使用者名作為鍵字尾,将使用者資訊儲存在哈希類型中,至于使用者的分數和排名可以使用zscore和zrank兩個功能:
hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike