現在開始將探究Redis的5種數據結構,我們會解釋每種數據結構都是什么,包含了什么有效的方法(Method),以及你能用這些數據結構處理哪些類型的特性和數據。
目前為止,我們所知道的Redis構成僅包括命令、關鍵字和值,還沒有接觸到關于數據結構的具體概念。當我們使用`set`命令時,Redis是怎么知道我們是在使用哪個數據結構?其解決方法是,每個命令都相對應于一種特定的數據結構。例如,當你使用`set`命令,你就是將值存儲到一個字符串數據結構里。而當你使用`hset`命令,你就是將值存儲到一個散列數據結構里。考慮到Redis的關鍵字集很小,這樣的機制具有相當的可管理性。
[Redis的網站](http://redis.io/commands)里有著非常優秀的參考文檔,沒有任何理由去重造輪子。但為了搞清楚這些數據結構的作用,我們將會覆蓋那些必須知道的重要命令。
沒有什么事情比高興的玩和試驗有趣的東西來得更重要的了。在任何時候,你都能通過鍵入`flushdb`命令將你數據庫里的所有值清除掉,因此,不要再那么害羞了,去嘗試做些瘋狂的事情吧!
## 字符串(Strings)
在Redis里,字符串是最基本的數據結構。當你在思索著關鍵字-值對時,你就是在思索著字符串數據結構。不要被名字給搞混了,如之前說過的,你的值可以是任何東西。我更喜歡將他們稱作“標量”(Scalars),但也許只有我才這樣想。
我們已經看到了一個常見的字符串使用案例,即通過關鍵字存儲對象的實例。有時候,你會頻繁地用到這類操作:
~~~
set users:leto "{name: leto, planet: dune, likes: [spice]}"
~~~
除了這些外,Redis還有一些常用的操作。例如,`strlen `能用來獲取一個關鍵字對應值的長度;`getrange `將返回指定范圍內的關鍵字對應值;`append `會將value附加到已存在的關鍵字對應值中(如果該關鍵字并不存在,則會創建一個新的關鍵字-值對)。不要猶豫,去試試看這些命令吧。下面是我得到的:
~~~
> strlen users:leto
(integer) 42
> getrange users:leto 27 40
"likes: [spice]"
> append users:leto " OVER 9000!!"
(integer) 54
~~~
現在你可能會想,這很好,但似乎沒有什么意義。你不能有效地提取出一段范圍內的JSON文件,或者為其附加一些值。你是對的,這里的經驗是,一些命令,尤其是關于字符串數據結構的,只有在給定了明確的數據類型后,才會有實際意義。
之前我們知道了,Redis不會去關注你的值是什么東西。通常情況下,這沒有錯。然而,一些字符串命令是專門為一些類型或值的結構而設計的。作為一個有些含糊的用例,我們可以看到,對于一些自定義的空間效率很高的(space-efficient)串行化對象,`append`和`getrange`命令將會很有用。對于一個更為具體的用例,我們可以再看一下`incr`、`incrby`、`decr`和`decrby`命令。這些命令會增長或者縮減一個字符串數據結構的值:
~~~
> incr stats:page:about
(integer) 1
> incr stats:page:about
(integer) 2
> incrby ratings:video:12333 5
(integer) 5
> incrby ratings:video:12333 3
(integer) 8
~~~
由此你可以想象到,Redis的字符串數據結構能很好地用于分析用途。你還可以去嘗試增長`users:leto`(一個不是整數的值),然后看看會發生什么(應該會得到一個錯誤)。
更為進階的用例是`setbit`和`getbit`命令。“今天我們有多少個獨立用戶訪問”是個在Web應用里常見的問題,有一篇[精彩的博文](http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/),在里面可以看到Spool是如何使用這兩個命令有效地解決此問題。對于1.28億個用戶,一部筆記本電腦在不到50毫秒的時間里就給出了答復,而且只用了16MB的存儲空間。
最重要的事情不是在于你是否明白位圖(Bitmaps)的工作原理,或者Spool是如何去使用這些命令,而是應該要清楚Redis的字符串數據結構比你當初所想的要有用許多。然而,最常見的應用案例還是上面我們給出的:存儲對象(簡單或復雜)和計數。同時,由于通過關鍵字來獲取一個值是如此之快,字符串數據結構很常被用來緩存數據。
## 散列(Hashes)
我們已經知道把Redis稱為一種關鍵字-值型存儲是不太準確的,散列數據結構是一個很好的例證。你會看到,在很多方面里,散列數據結構很像字符串數據結構。兩者顯著的區別在于,散列數據結構提供了一個額外的間接層:一個域(Field)。因此,散列數據結構中的`set`和`get`是:
~~~
hset users:goku powerlevel 9000
hget users:goku powerlevel
~~~
相關的操作還包括在同一時間設置多個域、同一時間獲取多個域、獲取所有的域和值、列出所有的域或者刪除指定的一個域:
~~~
hmset users:goku race saiyan age 737
hmget users:goku race powerlevel
hgetall users:goku
hkeys users:goku
hdel users:goku age
~~~
如你所見,散列數據結構比普通的字符串數據結構具有更多的可操作性。我們可以使用一個散列數據結構去獲得更精確的描述,是存儲一個用戶,而不是一個序列化對象。從而得到的好處是能夠提取、更新和刪除具體的數據片段,而不必去獲取或寫入整個值。
對于散列數據結構,可以從一個經過明確定義的對象的角度來考慮,例如一個用戶,關鍵之處在于要理解他們是如何工作的。從性能上的原因來看,這是正確的,更具粒度化的控制可能會相當有用。在下一章我們將會看到,如何用散列數據結構去組織你的數據,使查詢變得更為實效。在我看來,這是散列真正耀眼的地方。
## 列表(Lists)
對于一個給定的關鍵字,列表數據結構讓你可以存儲和處理一組值。你可以添加一個值到列表里、獲取列表的第一個值或最后一個值以及用給定的索引來處理值。列表數據結構維護了值的順序,提供了基于索引的高效操作。為了跟蹤在網站里注冊的最新用戶,我們可以維護一個`newusers`的列表:
~~~
lpush newusers goku
ltrim newusers 0 50
~~~
(譯注:`ltrim`命令的具體構成是`LTRIM Key start stop`。要理解`ltrim`命令,首先要明白Key所存儲的值是一個列表,理論上列表可以存放任意個值。對于指定的列表,根據所提供的兩個范圍參數start和stop,`ltrim`命令會將指定范圍外的值都刪除掉,只留下范圍內的值。)
首先,我們將一個新用戶推入到列表的前端,然后對列表進行調整,使得該列表只包含50個最近被推入的用戶。這是一種常見的模式。`ltrim`是一個具有O(N)時間復雜度的操作,N是被刪除的值的數量。從上面的例子來看,我們總是在插入了一個用戶后再進行列表調整,實際上,其將具有O(1)的時間復雜度(因為N將永遠等于1)的常數性能。
這是我們第一次看到一個關鍵字的對應值索引另一個值。如果我們想要獲取最近的10個用戶的詳細資料,我們可以運行下面的組合操作:
~~~
keys = redis.lrange('newusers', 0, 10)
redis.mget(*keys.map {|u| "users:#{u}"})
~~~
我們之前談論過關于多次往返數據的模式,上面的兩行Ruby代碼為我們進行了很好的演示。
當然,對于存儲和索引關鍵字的功能,并不是只有列表數據結構這種方式。值可以是任意的東西,你可以使用列表數據結構去存儲日志,也可以用來跟蹤用戶瀏覽網站時的路徑。如果你過往曾構建過游戲,你可能會使用列表數據結構去跟蹤用戶的排隊活動。
## 集合(Sets)
集合數據結構常常被用來存儲只能唯一存在的值,并提供了許多的基于集合的操作,例如并集。集合數據結構沒有對值進行排序,但是其提供了高效的基于值的操作。使用集合數據結構的典型用例是朋友名單的實現:
~~~
sadd friends:leto ghanima paul chani jessica
sadd friends:duncan paul jessica alia
~~~
不管一個用戶有多少個朋友,我們都能高效地(O(1)時間復雜度)識別出用戶X是不是用戶Y的朋友:
~~~
sismember friends:leto jessica
sismember friends:leto vladimir
~~~
而且,我們可以查看兩個或更多的人是不是有共同的朋友:
~~~
sinter friends:leto friends:duncan
~~~
甚至可以在一個新的關鍵字里存儲結果:
~~~
sinterstore friends:leto_duncan friends:leto friends:duncan
~~~
有時候需要對值的屬性進行標記和跟蹤處理,但不能通過簡單的復制操作完成,集合數據結構是解決此類問題的最好方法之一。當然,對于那些需要運用集合操作的地方(例如交集和并集),集合數據結構就是最好的選擇。
## 分類集合(Sorted Sets)
最后也是最強大的數據結構是分類集合數據結構。如果說散列數據結構類似于字符串數據結構,主要區分是域(field)的概念;那么分類集合數據結構就類似于集合數據結構,主要區分是標記(score)的概念。標記提供了排序(sorting)和秩劃分(ranking)的功能。如果我們想要一個秩分類的朋友名單,可以這樣做:
~~~
zadd friends:duncan 70 ghanima 95 paul 95 chani 75 jessica 1 vladimir
~~~
對于`duncan`的朋友,要怎樣計算出標記(score)為90或更高的人數?
~~~
zcount friends:duncan 90 100
~~~
如何獲取`chani`在名單里的秩(rank)?
~~~
zrevrank friends:duncan chani
~~~
(譯注:`zrank`命令的具體構成是`ZRANK Key menber`,要知道Key存儲的Sorted Set默認是根據Score對各個menber進行升序的排列,該命令就是用來獲取menber在該排列里的次序,這就是所謂的秩。)
我們使用了`zrevrank`命令而不是`zrank`命令,這是因為Redis的默認排序是從低到高,但是在這個例子里我們的秩劃分是從高到低。對于分類集合數據結構,最常見的應用案例是用來實現排行榜系統。事實上,對于一些基于整數排序,且能以標記(score)來進行有效操作的東西,使用分類集合數據結構來處理應該都是不錯的選擇。
## 小結
對于Redis的5種數據結構,我們進行了高層次的概述。一件有趣的事情是,相對于最初構建時的想法,你經常能用Redis創造出一些更具實效的事情。對于字符串數據結構和分類集合數據結構的使用,很有可能存在一些構建方法是還沒有人想到的。當你理解了那些常用的應用案例后,你將發現Redis對于許多類型的問題,都是很理想的選擇。還有,不要因為Redis展示了5種數據結構和相應的各種方法,就認為你必須要把所有的東西都用上。只使用一些命令去構建一個特性是很常見的。