你也許已經知道Redis並不是簡單的 key-value 存儲,實際上他是一個數據結構服務器,支持不同類型的值。也就是說,你不必僅僅把字符串當作鍵值。下列這些數據類型都可作為值類型。
Redis key值是二進制安全的,這意味著可以用任何二進制序列作為key值,從形如”foo”的簡單字符串到一個JPEG文件的內容都可以。空字符串也是有效key值。
關於key的幾條規則:
這是最簡單Redis類型。如果你只用這種類型,Redis就像一個可以持久化的memcached服務器(注:memcache的數據僅保存在內存中,服務器重啟後,數據將丟失)。
我們來玩兒一下字符串類型:
$ redis-cli set mykey "my binary safe value" OK $ redis-cli get mykey my binary safe value
$ redis-cli set counter 100 OK $ redis-cli incr counter (integer) 101 $ redis-cli incr counter (integer) 102 $ redis-cli incrby counter 10 (integer) 112
要說清楚列表數據類型,最好先講一點兒理論背景,在信息技術界List這個詞常常被使用不當。例如”Python Lists”就名不副實(名為Linked Lists),但他們實際上是數組(同樣的數據類型在Ruby中叫數組)
一般意義上講,列表就是有序元素的序列:10,20,1,2,3就是一個列表。但用數組實現的List和用Linked List實現的List,在屬性方面大不相同。
Redis lists基於Linked Lists實現。這意味著即使在一個list中有數百萬個元素,在頭部或尾部添加一個元素的操作,其時間復雜度也是常數級別的。用LPUSH 命令在十個元素的list頭部添加新元素,和在千萬元素list頭部添加新元素的速度相同。
那麼,壞消息是什麼?在數組實現的list中利用索引訪問元素的速度極快,而同樣的操作在linked list實現的list上沒有那麼快。
Redis Lists are implemented with linked lists because for a database system it is crucial to be able to add elements to a very long list in a very fast way. Another strong advantage is, as you’ll see in a moment, that Redis Lists can be taken at constant length in constant time.
Redis Lists用linked list實現的原因是:對於數據庫系統來說,至關重要的特性是:能非常快的在很大的列表上添加元素。另一個重要因素是,正如你將要看到的:Redis lists能在常數時間取得常數長度。
$ redis-cli rpush messages "Hello how are you?" OK $ redis-cli rpush messages "Fine thanks. I‘m having fun with Redis" OK $ redis-cli rpush messages "I should look into this NOSQL thing ASAP" OK $ redis-cli lrange messages 0 2 1. Hello how are you? 2. Fine thanks. I‘m having fun with Redis 3. I should look into this NOSQL thing ASAP注意LRANGE 帶有兩個索引,一定范圍的第一個和最後一個元素。這兩個索引都可以為負來告知Redis從尾部開始計數,因此-1表示最後一個元素,-2表示list中的倒數第二個元素,以此類推。
As you can guess from the example above, lists can be used, for instance, in order to implement a chat system. Another use is as queues in order to route messages between different processes. But the key point is that you can use Redis lists every time you require to access data in the same order they are added. This will not require any SQL ORDER BY operation, will be very fast, and will scale to millions of elements even with a toy Linux box.
在博客引擎實現中,你可為每篇日志設置一個list,在該list中推入進博客評論,等等。
在上面的例子裡 ,我們將“對象”(此例中是簡單消息)直接壓入Redis list,但通常不應這麼做,由於對象可能被多次引用:例如在一個list中維護其時間順序,在一個集合中保存它的類別,只要有必要,它還會出現在其他list中,等等。
讓我們回到reddit.com的例子,將用戶提交的鏈接(新聞)添加到list中,有更可靠的方法如下所示:
$ redis-cli incr next.news.id (integer) 1 $ redis-cli set news:1:title "Redis is simple" OK $ redis-cli set news:1:url "http://code.google.com/p/redis" OK $ redis-cli lpush submitted.news 1 OK我們自增一個key,很容易得到一個獨一無二的自增ID,然後通過此ID創建對象–為對象的每個字段設置一個key。最後將新對象的ID壓入submitted.news list。
$ redis-cli sadd myset 1 (integer) 1 $ redis-cli sadd myset 2 (integer) 1 $ redis-cli sadd myset 3 (integer) 1 $ redis-cli smembers myset 1. 3 2. 1 3. 2
我向集合中添加了三個元素,並讓Redis返回所有元素。如你所見它們是無序的。
現在讓我們檢查某個元素是否存在:
$ redis-cli sismember myset 3 (integer) 1 $ redis-cli sismember myset 30 (integer) 0
“3″是這個集合的成員,而“30”不是。集合特別適合表現對象之間的關系。例如用Redis集合可以很容易實現標簽功能。
下面是一個簡單的方案:對每個想加標簽的對象,用一個標簽ID集合與之關聯,並且對每個已有的標簽,一組對象ID與之關聯。
例如假設我們的新聞ID 1000被加了三個標簽tag 1,2,5和77,就可以設置下面兩個集合:
$ redis-cli sadd news:1000:tags 1 (integer) 1 $ redis-cli sadd news:1000:tags 2 (integer) 1 $ redis-cli sadd news:1000:tags 5 (integer) 1 $ redis-cli sadd news:1000:tags 77 (integer) 1 $ redis-cli sadd tag:1:objects 1000 (integer) 1 $ redis-cli sadd tag:2:objects 1000 (integer) 1 $ redis-cli sadd tag:5:objects 1000 (integer) 1 $ redis-cli sadd tag:77:objects 1000 (integer) 1
要獲取一個對象的所有標簽,如此簡單:
$ redis-cli smembers news:1000:tags 1. 5 2. 1 3. 77 4. 2而有些看上去並不簡單的操作仍然能使用相應的Redis命令輕松實現。例如我們也許想獲得一份同時擁有標簽1, 2, 10和27的對象列表。這可以用SINTER命令來做,他可以在不同集合之間取出交集。因此為達目的我們只需:
$ redis-cli sinter tag:1:objects tag:2:objects tag:10:objects tag:27:objects ... no result in our dataset composed of just one object ;) ...在命令參考文檔中可以找到和集合相關的其他命令,令人感興趣的一抓一大把。一定要留意SORT命令,Redis集合和list都是可排序的。
在 標簽的例子裡,我們用到了標簽ID,卻沒有提到ID從何而來。基本上你得為每個加入系統的標簽分配一個唯一標識。你也希望在多個客戶端同時試著添加同樣的 標簽時不要出現競爭的情況。此外,如果標簽已存在,你希望返回他的ID,否則創建一個新的唯一標識並將其與此標簽關聯。
Redis 1.4將增加Hash類型。有了它,字符串和唯一ID關聯的事兒將不值一提,但如今我們如何用現有Redis命令可靠的解決它呢?
我們首先的嘗試(以失敗告終)可能如下。假設想為標簽“redis”獲取一個唯一ID:
多美妙,或許更好…等等!當兩個客戶端同時使用這組指令嘗試為標簽“redis”獲取唯一ID時會發生什麼呢?如果時間湊巧,他們倆都會從GET操作獲得nil,都將對next.tag.id key做自增操作,這個key會被自增兩次。其中一個客戶端會將錯誤的ID返回給調用者。幸運的是修復這個算法並不難,這是明智的版本:
集 合是使用頻率很高的數據類型,但是…對許多問題來說他們也有點兒太不講順序了;)因此Redis1.2引入了有序集合。他和集合非常相似,也是二進制安全 的字符串集合,但是這次帶有關聯的score,以及一個類似LRANGE的操作可以返回有序元素,此操作只能作用於有序集合,它就是,ZRANGE 命令。
從 某種程度上說,有序集合是SQL世界的索引在Redis中的等價物。例如在剛才的reddit.com例子中,並沒有提到如何根據用戶投票和時間因素將新 聞組合生成首頁。下面我們會用有序集合解決這個問題,我們先從最簡單的事情開始,闡明這個高級數據類型是如何工作的。讓我們添加幾個黑客,並將他們的生日 作為“score”。
$ redis-cli zadd hackers 1940 "Alan Kay" (integer) 1 $ redis-cli zadd hackers 1953 "Richard Stallman" (integer) 1 $ redis-cli zadd hackers 1965 "Yukihiro Matsumoto" (integer) 1 $ redis-cli zadd hackers 1916 "Claude Shannon" (integer) 1 $ redis-cli zadd hackers 1969 "Linus Torvalds" (integer) 1 $ redis-cli zadd hackers 1912 "Alan Turing" (integer) 1
$ redis-cli zrange hackers 0 -1 1. Alan Turing 2. Claude Shannon 3. Alan Kay 4. Richard Stallman 5. Yukihiro Matsumoto 6. Linus Torvalds你知道Linus比Yukihiro年輕嗎 ;)
$ redis-cli zrevrange hackers 0 -1 1. Linus Torvalds 2. Yukihiro Matsumoto 3. Richard Stallman 4. Alan Kay 5. Claude Shannon 6. Alan Turing
一個非常重要的小貼士,ZSets只是有一個“默認的”順序,但你仍然可以用 SORT 命令對有序集合做不同的排序(但這次服務器要耗費CPU了)。要想得到多種排序,一種可選方案是同時將每個元素加入多個有序集合。
有序集合之能不止於此,他能在區間上操作。例如獲取所有1950年之前出生的人。我們用 ZRANGEBYSCORE 命令來做:
$ redis-cli zrangebyscore hackers -inf 1950 1. Alan Turing 2. Claude Shannon 3. Alan Kay
我們請求Redis返回score介於負無窮到1950年之間的元素(兩個極值也包含了)。
也可以刪除區間內的元素。例如從有序集合中刪除生日介於1940到1960年之間的黑客。
$ redis-cli zremrangebyscore hackers 1940 1960 (integer) 2ZREMRANGEBYSCORE 這個名字雖然不算好,但他卻非常有用,還會返回已刪除的元素數量。
最後,回到 Reddit的例子。現在我們有個基於有序集合的像樣方案來生成首頁。用一個有序集合來包含最近幾天的新聞(用 ZREMRANGEBYSCORE 不時的刪除舊新聞)。用一個後台任務從有序集合中獲取所有元素,根據用戶投票和新聞時間計算score,然後用新聞IDs和scores關聯生成 reddit.home.page 有序集合。要顯示首頁,我們只需閃電般的調用 ZRANGE。
不時的從 reddit.home.page 有序集合中刪除過舊的新聞也是為了讓我們的系統總是工作在有限的新聞集合之上。
更新有序集合的scores
可以在任何時候更新。只要用 ZADD 對有序集合內的元素操作就會更新它的score(和位置),時間復雜度是O(log(N)),因此即使大量更新,有序集合也是合適的。