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