有序集合(Sorted Set)是Redis一個(gè)很重要的數(shù)據(jù)結(jié)構(gòu),它用來(lái)保存需要排序的數(shù)據(jù)。例如排行榜,一個(gè)班的語(yǔ)文成績(jī),一個(gè)公司的員工工資,一個(gè)論壇的帖子等。有序集合中,每個(gè)元素都帶有score(權(quán)重),以此來(lái)對(duì)元素進(jìn)行排序。它有三個(gè)元素:key、member和score。以語(yǔ)文成績(jī)?yōu)槔?,key是考試名稱(期中考試、期末考試等),member是學(xué)生名字,score是成績(jī)。
有序集合有兩大基本用途:排序和聚合,以下用幾個(gè)例子分別說(shuō)明。
排序
假設(shè)老師需要處理期中考試的語(yǔ)文成績(jī),他做的第一件事是將學(xué)生成績(jī)錄入系統(tǒng)。
Li Lei成績(jī)70分
127.0.0.1:6379> ZADD mid_test 70 "Li Lei"
(integer) 1
Han Meimei成績(jī)70分
127.0.0.1:6379> ZADD mid_test 70 "Han Meimei"
(integer) 1
tom成績(jī)99.5分
127.0.0.1:6379> ZADD mid_test 99.5 "Tom"
(integer) 1
排行榜
有序集合天然就是做排行榜的利器。只需將帶score的member塞到有序集合里,就可以正序或倒序取出數(shù)據(jù)。這要用到ZREVRANGE(倒序)和ZRANGE(正序)。
分?jǐn)?shù)排行榜
127.0.0.1:6379> ZREVRANGE mid_test 0 -1 WITHSCORES
1) "Tom"
2) "99.5"
3) "Li Lei"
4) "70"
5) "Han Meimei"
6) "70"
分段統(tǒng)計(jì)
有序集合還支持按score區(qū)間來(lái)查詢:ZREVRANGEBYSCORE為倒序查詢,ZRANGEBYSCORE為正序。例如要知道90分以上的學(xué)霸:
127.0.0.1:6379> ZREVRANGEBYSCORE mid_test 100 90 WITHSCORES
1) "Tom"
2) "99.5"
聚合
有序集合,其本質(zhì)是集合,當(dāng)然會(huì)有交集(ZINTERSTORE)和并集(ZUNIONSTORE)運(yùn)算。

交集
ZINTERSTORE取所有集合的交集。以兩個(gè)集合A和B為例,要取交集C,是這樣的邏輯:
- A和B中共有的member,會(huì)加入到C中,其score等于A、B中score之和。
- 不同時(shí)在A和B的member,不會(huì)加到C中。
某班又進(jìn)行了期末考試,同時(shí)來(lái)了個(gè)新同學(xué)Jerry。
127.0.0.1:6379> ZADD fin_test 88 "Li Lei"
(integer) 1
127.0.0.1:6379> ZADD fin_test 75 "Han Meimei"
(integer) 1
127.0.0.1:6379> ZADD fin_test 99.5 "Tom"
(integer) 1
127.0.0.1:6379> ZADD fin_test 100 "Jerry"
(integer) 1
老師要按期中考試和期末考試的總成績(jī)來(lái)排座位,就對(duì)mid_test和fin_test做了個(gè)交集。
127.0.0.1:6379> ZINTERSTORE sum_point 2 mid_test fin_test
(integer) 3
127.0.0.1:6379> ZREVRANGE sum_point 0 -1 WITHSCORES
1) "Tom"
2) "199"
3) "Li Lei"
4) "158"
5) "Han Meimei"
6) "145"
結(jié)果顯示了學(xué)生的總成績(jī)。
但結(jié)果中沒(méi)有新來(lái)的Jerry同學(xué)(盡管TA考了100分)。這是坑一。
并集
ZUNIONSTORE計(jì)算所有集合的并集。以兩個(gè)集合A和B為例,要取并集C,是這樣的邏輯:
- A的所有member會(huì)加到C中,其score與A中相等
- B的所有member會(huì)加到C中,其score與B中相等
- A和B中共有的member,其score等于A、B中score之和。
假設(shè)某公司要核算工資總支出,先由各部門(mén)獨(dú)自核算,再由財(cái)務(wù)統(tǒng)一處理。
程序員工資
127.0.0.1:6379> zadd programmer 2000 peter
(integer) 1
127.0.0.1:6379> zadd programmer 3500 jack
(integer) 1
127.0.0.1:6379> zadd programmer 5000 tom
(integer) 1
經(jīng)理工資
127.0.0.1:6379> zadd manager 2000 herry
(integer) 1
127.0.0.1:6379> zadd manager 3500 mary
(integer) 1
127.0.0.1:6379> zadd manager 4000 tom
(integer) 1
財(cái)務(wù)統(tǒng)一處理。
127.0.0.1:6379> zunionstore salary 2 programmer manager
(integer) 5
127.0.0.1:6379> zrange salary 0 -1 withscores
1) "herry"
2) "2000"
3) "peter"
4) "2000"
5) "jack"
6) "3500"
7) "mary"
8) "3500"
9) "tom"
10) "9000"
結(jié)果顯示了總工資支出情況。
但結(jié)果中程序員tom和經(jīng)理tom是兩個(gè)人,但工資算在了一起。這是坑二。
避免踩坑
還記得上面說(shuō)的坑一和坑二嗎?
坑一:
當(dāng)進(jìn)行ZINTERSTORE操作時(shí),如果進(jìn)行聚合操作的源集合中元素不同,則聚合后的結(jié)果集僅為交集。如果發(fā)現(xiàn)聚合后少了一些元素,請(qǐng)查看源集合元素是否相同。
坑二:
當(dāng)進(jìn)行ZUNIONSTORE操作時(shí),如果進(jìn)行聚合操作的源集合中有相同元素,則聚合后的結(jié)果集中,相同元素的score等于源集合元素的score之和。如果發(fā)現(xiàn)聚合后某些元素的score異常,請(qǐng)查看源集合是否有相同元素。
我踩過(guò)的坑:
做用戶的feed(timeline)時(shí),需要將我關(guān)注的人和我自己發(fā)表的信息聚合起來(lái)。

應(yīng)該用ZUNIONSTORE將所有信息聚合到一起。
后來(lái)有用戶反饋說(shuō)timeline排序錯(cuò)誤,自己發(fā)表發(fā)布的信息永遠(yuǎn)在最上面。后來(lái)查明原因,由于早期的bug,自己竟然可以關(guān)注自己,導(dǎo)致關(guān)注人和自己重復(fù)聚合。踩到了坑二。
為什么踩坑
以坑二為例,為什么有相同元素時(shí),score就會(huì)變成原來(lái)元素的和?
因?yàn)閆INTERSTORE和ZUNIONSTORE有個(gè)參數(shù)為AGGREGATE,表示結(jié)果集的聚合方式,可取SUM、MIN、MAX其中之一。默認(rèn)值為SUM。
所以不指定聚合方式時(shí),缺省值為SUM,即求和。
<blockquote>
默認(rèn)使用的參數(shù) SUM ,可以將所有集合中某個(gè)成員的 score 值之 和 作為結(jié)果集中該成員的 score 值;使用參數(shù) MIN ,可以將所有集合中某個(gè)成員的 最小 score 值作為結(jié)果集中該成員的 score 值;而參數(shù) MAX 則是將所有集合中某個(gè)成員的 最大 score 值作為結(jié)果集中該成員的 score 值。
</blockquote>
文檔如上。
有序集合之總結(jié)
使用場(chǎng)景:排行榜,有序列表,聚合;
算法復(fù)雜度:
- 增刪:O(M*log(N)), N 為有序集的基數(shù), M 為被成功操作(新增、移除)的成員的數(shù)量。
- 查詢:O(log(N)+M), N 為有序集的基數(shù),而 M 為結(jié)果集的基數(shù)。
- 聚合:O(N)+O(M log(M)), N 為給定有序集基數(shù)的總和, M 為結(jié)果集的基數(shù)。
- 總數(shù):O(1)
注意事項(xiàng):
- ZINTERSTORE操作時(shí),如果發(fā)現(xiàn)聚合后少了一些元素,請(qǐng)查看源集合元素是否相同。
- ZUNIONSTORE操作時(shí),如果發(fā)現(xiàn)聚合后某些元素的score異常,請(qǐng)查看源集合是否有相同元素。
博客鏈接:http://spetacular.github.io/2015/11/01/redis-zunionstore-tip.html