有序集合類似于集合和哈希的混合體的一種數(shù)據(jù)類型。像集合一樣,有序集合由唯一的,不重復的字符串元素組成,在某種意義上,有序集合也就是集合。
集合中的每個元素是無序的,但有序集合中的每個元素都關聯(lián)了一個浮點值,稱為分數(shù)(score,這就是為什么該類型也類似于哈希,因為每一個元素都映射到一個值)。
此外,有序集合中的元素是按序存儲的(不是請求時才排序的,順序是依賴于表示有序集合的數(shù)據(jù)結(jié)構(gòu))。他們按照如下規(guī)則排序:
讓我們開始一個簡單的例子,添加一些黑客的名字作為有序集合的元素,以他們的出生年份為分數(shù)。
> zadd hackers 1940 "Alan Kay"
(integer) 1
> zadd hackers 1957 "Sophie Wilson"
(integer 1)
> zadd hackers 1953 "Richard Stallman"
(integer) 1
> zadd hackers 1949 "Anita Borg"
(integer) 1
> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
> zadd hackers 1916 "Claude Shannon"
(integer) 1
> zadd hackers 1969 "Linus Torvalds"
(integer) 1
> zadd hackers 1912 "Alan Turing"
(integer) 1
如你所見,ZADD 命令類似于 SADD,但是多一個參數(shù)(位于添加的元素之前),即分數(shù)。ZADD 命令也是可變參數(shù)的,所以你可以自由的指定多個分數(shù)值對(score-value pairs),盡管上面的例子中并沒有使用。
使用排序集合可以很容易返回按照出生年份排序的黑客列表,因為他們已經(jīng)是排序好的。
實現(xiàn)注意事項:有序集合是通過雙端(dual-ported)數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的,包括跳躍表(skiplist,后續(xù)文章會詳細介紹,譯者注)和哈希表(hashtable),所以我們每次添加元素時 Redis 執(zhí)行 O(log(N)) 的操作。這還好,但是當我們請求有序元素時,Redis 根本不需要做什么工作,因為已經(jīng)是全部有序了:
> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
6) "Richard Stallman"
7) "Sophie Wilson"
8) "Yukihiro Matsumoto"
9) "Linus Torvalds"
注意:0 和 - 1 表示從索引為 0 的元素到最后一個元素(-1 像 LRANGE 命令中一樣工作)。
如果我想按照相反的順序排序,從最年輕到最年長?使用 ZREVRANGE 代替 ZRANGE:
> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Yukihiro Matsumoto"
3) "Sophie Wilson"
4) "Richard Stallman"
5) "Anita Borg"
6) "Alan Kay"
7) "Claude Shannon"
8) "Hedy Lamarr"
9) "Alan Turing"
也可以同時返回分數(shù),使用 WITHSCORES 參數(shù):
> zrange hackers 0 -1 withscores
1) "Alan Turing"
2) "1912"
3) "Hedy Lamarr"
4) "1914"
5) "Claude Shannon"
6) "1916"
7) "Alan Kay"
8) "1940"
9) "Anita Borg"
10) "1949"
11) "Richard Stallman"
12) "1953"
13) "Sophie Wilson"
14) "1957"
15) "Yukihiro Matsumoto"
16) "1965"
17) "Linus Torvalds"
18) "1969"
有序集合遠比這些要強大。他們可以在范圍上操作。讓我們獲取 1950 年前出生的所有人。我們使用 ZRANGEBYSCORE 命令來辦到:
> zrangebyscore hackers -inf 1950
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
我們要求 Redis 返回分數(shù)在負無窮到 1950 之間的所有元素(包括兩個極端)。
也可以刪除某個范圍的元素。讓我們從有序集合中刪除出生于 1940 年到 1960 年之間的黑客:
> zremrangebyscore hackers 1940 1960
(integer) 4
ZREMRANGEBYSCORE 也許不是最合適的命令名,但是非常有用,返回刪除的元素數(shù)目。
另一個非常有用的操作是用來獲取有序集合中元素排行的操作。也就是可以詢問集合中元素的排序位置。
> zrank hackers "Anita Borg"
(integer) 4
ZREVRANK 命令用來按照降序排序返回元素的排行。
最近的 Redis2.8 版本引入了一個新的特性,假定集合中的元素都具有相同的分數(shù),允許按字典順序獲取范圍(元素按照 C 語言中的 memcmp 函數(shù)進行比較,因此可以保證沒有整理,每個 Redis 實例會有相同的輸出)。
操作字典順序范圍的主要命令是 ZRANGEBYLEX,ZREVRANGEBYLEX,ZREMRANGEBYLEX 和 ZLEXCOUNT。例如,我們再次添加我們的著名黑客清單。但是這次為每個元素使用 0 分數(shù):
> zadd hackers 0 "Alan Kay" 0 "Sophie Wilson" 0 "Richard Stallman" 0
"Anita Borg" 0 "Yukihiro Matsumoto" 0 "Hedy Lamarr" 0 "Claude Shannon"
0 "Linus Torvalds" 0 "Alan Turing"
根據(jù)有序集合的排序規(guī)則,他們已經(jīng)按照字典順序排好了:
> zrange hackers 0 -1
1) "Alan Kay"
2) "Alan Turing"
3) "Anita Borg"
4) "Claude Shannon"
5) "Hedy Lamarr"
6) "Linus Torvalds"
7) "Richard Stallman"
8) "Sophie Wilson"
9) "Yukihiro Matsumoto"
使用 ZRANGEBYLEX 我們可以查詢字典順序范圍:
> zrangebylex hackers [B [P
1) "Claude Shannon"
2) "Hedy Lamarr"
3) "Linus Torvalds"
范圍可以是包容性的或者排除性的(取決于第一個字符,即開閉區(qū)間,譯者注),+ 和 - 分別表示正無窮和負無窮。查看該命令的文檔獲取更詳細信息(該文檔后續(xù)即奉獻,譯者注)。
這個特性非常重要,因為這允許有序集合作為通用索引。例如,如果你想用一個 128 位無符號整數(shù)來索引元素,你需要做的就是使用相同的分數(shù)(例如 0)添加元素到有序集合中,元素加上由 128 位大端(big endian)數(shù)字組成的 8 字節(jié)前綴。由于數(shù)字是大端編碼,字典順序排序(原始 raw 字節(jié)順序)其實就是數(shù)字順序,你可以在 128 位空間查詢范圍,獲取元素后拋棄前綴。如果你想在一個更正式的例子中了解這個特性,可以看看 Redis 自動完成范例(后續(xù)獻上,譯者注)。
這一部分是開始新的主題前最后一個關于有序集合的內(nèi)容。有序集合的分數(shù)可以隨時更新。對一個存在于有序集合中的元素再次調(diào)用 ZADD,將會在 O(log(N))時間復雜度更新他的分數(shù) (和位置),所以有序集合適合于經(jīng)常更新的場合。
由于這個特性,通常的一個使用場景就是排行榜。最典型的應用就是 facebook 游戲,你可以組合使用按分數(shù)高低存儲用戶,以及獲取排名的操作,來展示前 N 名的用戶以及用戶在排行榜上的排行(你是第 4932 名最佳分數(shù))。
位圖不是一個真實的數(shù)據(jù)類型,而是定義在字符串類型上的面向位的操作的集合。由于字符串類型是二進制安全的二進制大對象(blobs),并且最大長度是 512MB,適合于設置 232 個不同的位。
位操作分為兩組:常量時間單個位的操作,像設置一個位為 1 或者 0,或者獲取該位的值。對一組位的操作,例如計算指定范圍位的置位數(shù)量。
位圖的最大優(yōu)勢是有時是一種非常顯著的節(jié)省空間來存儲信息的方式。例如,在一個系統(tǒng)中,不同用戶由遞增的用戶 ID 來表示,可以使用 512MB 的內(nèi)存來表示 400 萬用戶的單個位信息(例如他們是否需要接收信件)。
設置和檢索位使用 SETBIT 和 GETBIT 命令:
> setbit key 10 1
(integer) 1
> getbit key 10
(integer) 1
> getbit key 11
(integer) 0
SETBIT 命令把第一個參數(shù)作為位數(shù),第二個參數(shù)作為要給位設置的值,0 或者 。如果位的位置超過了當前字符串的長度,這個命令或自動擴充這個字符串。
GETBIT 命令只是返貨指定下標處的位的值。超出范圍的位(指定的位超出了該鍵下字符串的長度)被認為是 0。
有 3 個操作一組位的命令:
BITOPS 和 BITCOUNT 命令都可以操作字符串的字節(jié)范圍,而不僅僅是運行于整個字符串長度。下面是 BITCOUNT 調(diào)用的一個簡單例子:
> setbit key 0 1
(integer) 0
> setbit key 100 1
(integer) 0
> bitcount key
(integer) 2
位圖的通用場景:
例如,假設你想知道你的網(wǎng)站的用戶的最長日訪問曲線。你從 0 開始計算天數(shù),也就是你的網(wǎng)站可訪問的那天,并且每當用戶訪問你的網(wǎng)站的時候,就用 SETBIT 命令設置一個位。你可以使用當前 unix 時間減去初始位移,然后除以 3600*24,作為位的下標。
這種方式下每個用戶都有一個記錄每天訪問信息的一個小字符串。使用 BITCOUNT 命令以及幾次 BITPOS 調(diào)用,可以很容易獲得指定用戶訪問網(wǎng)站的天數(shù),或者只是獲取并在客戶端分析位圖,也很容易計算出最長曲線。
位圖可以很容易的拆分為多個鍵,例如為了數(shù)據(jù)集分片,因為通常要避免使用很大的鍵。為了將位圖拆分為不同的鍵,而不是將所有的位設置到一個鍵,一個簡單的策略就是每個鍵只存儲 M 位,并且使用位數(shù)除以 M 作為鍵名,在鍵中使用位數(shù)模 M 來定位第 N 位。
超重對數(shù)是用于計算唯一事物數(shù)量的概率性數(shù)據(jù)結(jié)構(gòu)(學術上指的是估算集合的基數(shù))。通常計算唯一項數(shù)量需要使用和你想計算的項成正比的大量內(nèi)存,因為你需要記住你已經(jīng)看到的元素,以避免被多次計算。然而,有一組用內(nèi)存換精度的算法:你會得到一個估算的測量,伴隨一個標準錯誤,在 Redis 的實現(xiàn)中誤差低于 1%,但是這些算法的魔力在于,你不再需要使用和你要計算的量成比例的大量內(nèi)存,你只需要使用常量內(nèi)存!最壞情況下 12K 字節(jié),或者當你的超重對數(shù)(后續(xù)稱它們?yōu)?HLL)只發(fā)現(xiàn)了少量元素時更是省內(nèi)存。
Redis 中的超重對數(shù),雖然技術上是一個不同的數(shù)據(jù)結(jié)構(gòu),但被編碼為 Redis 字符串,所以你可以調(diào)用 GET 來序列化超重對數(shù),使用 SET 反序列化回服務器。
從概念上講,超重對數(shù)的 API 像是使用集合來做同樣的事情。你會 SADD 元素到集合,使用 SCARD 來檢查集合中元素數(shù)量,這些元素都是唯一的,因為 SCARD 捕獲重復添加已經(jīng)添加的元素。
你并沒有真正添加項到超重對數(shù)中,因為這種數(shù)據(jù)結(jié)構(gòu)只是包含了狀態(tài)而沒有包含真正的元素,其 API 也是一樣:
redis> PFADD hll a b c d
(integer) 1
redis> PFCOUNT hll
(integer) 4
這種數(shù)據(jù)結(jié)構(gòu)的使用場景的一個例子是,計算每天搜索的唯一請求數(shù)。
Redis 也可以執(zhí)行超重對數(shù)的并集,更多信息請繼續(xù)關注相關命令(請關注此公眾號,逐一揭曉)。
還有一些重要的 Redis API 沒有在此文中探索,但是非常值得你關注:
你可以增量迭代鍵空間或者一個很大的集合。
你可以在服務端運行 Lua 腳本以贏得延遲和帶寬。
Redis 還是也是一個訂閱發(fā)布服務器。
這個教程并不完整,只涵蓋了基本的 API。閱讀命令參考去發(fā)現(xiàn)更多。
歡迎閱讀此教程,和 Redis 一起狂舞!