鍍金池/ 教程/ 大數(shù)據(jù)/ 從入門到精通(中)
使用 Redis 實(shí)現(xiàn) Twitter(上)
集群(下)
使用 Redis 實(shí)現(xiàn) Twitter(下)
使用 Redis 作為 LRU 緩存
高可用(上)
高可用客戶端指引
集群(中)
高可用(下)
持久化
Redis 介紹
集中插入
集群(上)
從入門到精通(上)
從入門到精通(下)
從入門到精通(中)
分片
數(shù)據(jù)類型初探
復(fù)制

從入門到精通(中)

Redis 列表(Lists)

為了解釋列表類型,最好先開始來點(diǎn)理論,因?yàn)榱斜磉@個術(shù)語在信息技術(shù)領(lǐng)域常常使用不當(dāng)。例如,”Python Lists”,并不是字面意思(鏈表),實(shí)際是表示數(shù)組 (和 Ruby 中的 Array 是同一種類型)。

通常列表表示有序元素的序列:10,20,1,2,3 是一個列表。但是數(shù)組實(shí)現(xiàn)的列表和鏈表實(shí)現(xiàn)的列表,他們的屬性非常不同。

Redis 的列表是使用鏈表實(shí)現(xiàn)的。這意味著,及時你的列表中有上百萬個元素,增加一個元素到列表的頭部或者尾部的操作都是在常量時間完成。使用 LPUSH 命令增加一個新元素到擁有 10 個元素的列表的頭部的速度,與增加到擁有 1000 萬個元素的列表的頭部是一樣的。

缺點(diǎn)又是什么呢?使用索引下標(biāo)來訪問一個數(shù)組實(shí)現(xiàn)的列表非???常量時間),但是訪問鏈表實(shí)現(xiàn)列表就沒那么快了(與元素索引下標(biāo)成正比的大量工作)。

Redis 采用鏈表來實(shí)現(xiàn)列表是因?yàn)?,對于?shù)據(jù)庫系統(tǒng)來說,快速插入一個元素到一個很長的列表非常重要。另外一個即將描述的優(yōu)勢是,Redis 列表能在常數(shù)時間內(nèi)獲得常數(shù)長度。

如果需要快速訪問一個擁有大量元素的集合的中間數(shù)據(jù),可以用另一個稱為有序集合的數(shù)據(jù)結(jié)構(gòu)。稍后將會介紹有序集合。

Redis 列表起步

LPUSH 命令從左邊 (頭部) 添加一個元素到列表,RPUSH 命令從右邊(尾部)添加一個元素的列表。LRANGE 命令從列表中提取一個范圍內(nèi)的元素。

> rpush mylist A  
(integer) 1  
> rpush mylist B  
(integer) 2  
> lpush mylist first  
(integer) 3  
> lrange mylist 0 -1  
1) "first"  
2) "A"  
3) "B"  

注意 LRANGE 命令使用兩個索引下標(biāo),分別是返回的范圍的開始和結(jié)束元素。兩個索引坐標(biāo)可以是負(fù)數(shù),表示從后往前數(shù),所以 - 1 表示最后一個元素,-2 表示倒數(shù)第二個元素,等等。

如你所見,RPUSH 添加元素到列表的右邊,LPUSH 添加元素到列表的左邊。

兩個命令都是可變參數(shù)命令,也就是說,你可以在一個命令調(diào)用中自由的添加多個元素到列表中:

> rpush mylist 1 2 3 4 5 "foo bar"  
(integer) 9  
> lrange mylist 0 -1  
1) "first"  
2) "A"  
3) "B"  
4) "1"  
5) "2"  
6) "3"  
7) "4"  
8) "5"  
9) "foo bar"  

定義在 Redis 列表上的一個重要操作是彈出元素。彈出元素指的是從列表中檢索元素,并同時將其從列表中清楚的操作。你可以從左邊或者右邊彈出元素,類似于你可以從列表的兩端添加元素。

> rpush mylist a b c  
(integer) 3  
> rpop mylist  
"c"  
> rpop mylist  
"b"  
> rpop mylist  
"a"  

我們添加了三個元素并且又彈出了三個元素,所以這一串命令執(zhí)行完以后列表是空的,沒有元素可以彈出了。如果我們試圖再彈出一個元素,就會得到如下結(jié)果:

> rpop mylist  
(nil)  

Redis 返回一個 NULL 值來表明列表中沒有元素了。

列表的通用場景(Common use cases)

列表可以完成很多任務(wù),兩個有代表性的場景如下:

  • 記住社交網(wǎng)絡(luò)中用戶最近提交的更新。
  • 使用生產(chǎn)者消費(fèi)者模式來進(jìn)程間通信,生產(chǎn)者添加項(xiàng)(item)到列表,消費(fèi)者(通常是 worker)消費(fèi)項(xiàng)并執(zhí)行任務(wù)。Redis 有專門的列表命令更加可靠和高效的解決這種問題。

例如,兩種流行的 Ruby 庫 resque 和 sidekiq,都是使用 Redis 列表作為鉤子,來實(shí)現(xiàn)后臺作業(yè) (background jobs)。

流行的 Twitter 社交網(wǎng)絡(luò),使用 Redis 列表來存儲用戶最新的微博 (tweets)。

為了一步一步的描述通用場景,假設(shè)你想加速展現(xiàn)照片共享社交網(wǎng)絡(luò)主頁的最近發(fā)布的圖片列表。

每次用戶提交一張新的照片,我們使用 LPUSH 將其 ID 添加到列表。

當(dāng)用戶訪問主頁時,我們使用 LRANGE 0 9 獲取最新的 10 張照片。

上限列表(Capped)

很多時候我們只是想用列表存儲最近的項(xiàng),隨便這些項(xiàng)是什么:社交網(wǎng)絡(luò)更新,日志或者任何其他東西。

Redis 允許使用列表作為一個上限集合,使用 LTRIM 命令僅僅只記住最新的 N 項(xiàng),丟棄掉所有老的項(xiàng)。

LTRIM 命令類似于 LRANGE,但是不同于展示指定范圍的元素,而是將其作為列表新值存儲。所有范圍外的元素都將被刪除。

舉個例子你就更清楚了:

> rpush mylist 1 2 3 4 5  
(integer) 5  
> ltrim mylist 0 2  
OK  
> lrange mylist 0 -1  
1) "1"  
2) "2"  
3) "3"  

上面 LTRIM 命令告訴 Redis 僅僅保存第 0 到 2 個元素,其他的都被拋棄。這可以讓你實(shí)現(xiàn)一個簡單而又有用的模式,一個添加操作和一個修剪操作一起,實(shí)現(xiàn)新增一個元素拋棄超出元素。

LPUSH mylist <some element>  
LTRIM mylist 0 999  

上面的組合增加一個元素到列表中,同時只持有最新的 1000 個元素。使用 LRANGE 命令你可以訪問前幾個元素而不用記錄非常老的數(shù)據(jù)。

注意:盡管 LRANGE 是一個O(N)時間復(fù)雜度的命令,訪問列表頭尾附近的小范圍是常量時間的操作。

列表的阻塞操作 (blocking)

列表有一個特別的特性使得其適合實(shí)現(xiàn)隊列,通常作為進(jìn)程間通信系統(tǒng)的積木:阻塞操作。

假設(shè)你想往一個進(jìn)程的列表中添加項(xiàng),用另一個進(jìn)程來處理這些項(xiàng)。這就是通常的生產(chǎn)者消費(fèi)者模式,可以使用以下簡單方式實(shí)現(xiàn):

  • 生產(chǎn)者調(diào)用 LPUSH 添加項(xiàng)到列表中。
  • 消費(fèi)者調(diào)用 RPOP 從列表提取 / 處理項(xiàng)。

然而有時候列表是空的,沒有需要處理的,RPOP 就返回 NULL。所以消費(fèi)者被強(qiáng)制等待一段時間并重試 RPOP 命令。這稱為輪詢(polling),由于其具有一些缺點(diǎn),所以不合適在這種情況下:

  1. 強(qiáng)制 Redis 和客戶端處理無用的命令 (當(dāng)列表為空時的所有請求都沒有執(zhí)行實(shí)際的工作,只會返回 NULL)。
  2. 由于工作者受到一個 NULL 后會等待一段時間,這會延遲對項(xiàng)的處理。

于是 Redis 實(shí)現(xiàn)了 BRPOP 和 BLPOP 兩個命令,它們是當(dāng)列表為空時 RPOP 和 LPOP 的會阻塞版本:僅當(dāng)一個新元素被添加到列表時,或者到達(dá)了用戶的指定超時時間,才返回給調(diào)用者。 這個是我們在工作者中調(diào)用 BRPOP 的例子:

> brpop tasks 5  
1) "tasks"  
2) "do_something"  

上面的意思是” 等待 tasks 列表中的元素,如果 5 秒后還沒有可用元素就返回”。

注意,你可以使用 0 作為超時讓其一直等待元素,你也可以指定多個列表而不僅僅只是一個,同時等待多個列表,當(dāng)?shù)谝粋€列表收到元素后就能得到通知。

關(guān)于 BRPOP 的一些注意事項(xiàng)。

  1. 客戶端按順序服務(wù):第一個被阻塞等待列表的客戶端,將第一個收到其他客戶端添加的元素,等等。
  2. 與 RPOP 的返回值不同:返回的是一個數(shù)組,其中包括鍵的名字,因?yàn)?BRPOP 和 BLPOP 可以阻塞等待多個列表的元素。
  3. 如果超時時間到達(dá),返回 NULL。

還有更多你需要知道的關(guān)于列表和阻塞選項(xiàng),建議你閱讀下面的頁面:

  • 使用 RPOLPUSH 構(gòu)建更安全的隊列和旋轉(zhuǎn)隊列。
  • BRPOPLPUSH 命令是其阻塞變種命令。

自動創(chuàng)建和刪除鍵

到目前為止的例子中,我們還沒有在添加元素前創(chuàng)建一個空的列表,也沒有刪除一個沒有元素的空列表。要注意,當(dāng)列表為空時 Redis 將刪除該鍵,當(dāng)向一個不存在的列表鍵(如使用 LPUSH)添加一個元素時,將創(chuàng)建一個空的列表。

這并不只是針對列表,適用于所有 Redis 多元素組成的數(shù)據(jù)類型,因此適用于集合,有序集合和哈希。

基本上我們可以概括為三條規(guī)則:

  1. 當(dāng)我們向聚合(aggregate)數(shù)據(jù)類型添加一個元素,如果目標(biāo)鍵不存在,添加元素前將創(chuàng)建一個空的聚合數(shù)據(jù)類型。
  2. 當(dāng)我們從聚合數(shù)據(jù)類型刪除一個元素,如果值為空,則鍵也會被銷毀。
  3. 調(diào)用一個像 LLEN 的只讀命令(返回列表的長度),或者一個寫命令從空鍵刪除元素,總是產(chǎn)生和操作一個持有空聚合類型值的鍵一樣的結(jié)果。

規(guī)則 1 的例子:

> del mylist  
(integer) 1  
> lpush mylist 1 2 3  
(integer) 3  

然而,我們不能執(zhí)行一個錯誤鍵類型的操作:

> set foo bar  
OK  
> lpush foo 1 2 3  
(error) WRONGTYPE Operation against a key holding the wrong kind of value  
> type foo  
string  

規(guī)則 2 的例子:

> lpush mylist 1 2 3  
(integer) 3  
> exists mylist  
(integer) 1  
> lpop mylist  
"3"  
> lpop mylist  
"2"  
> lpop mylist  
"1"  
> exists mylist  
(integer) 0  

當(dāng)所有元素彈出后,鍵就不存在了。

規(guī)則 3 的例子:

> del mylist  
(integer) 0  
> llen mylist  
(integer) 0  
> lpop mylist  
(nil)  

Redis 哈希/散列 (Hashes)

Redis 哈希看起來正如你期待的那樣:

> hmset user:1000 username antirez birthyear 1977 verified 1  
OK  
> hget user:1000 username  
"antirez"  
> hget user:1000 birthyear  
"1977"  
> hgetall user:1000  
1) "username"  
2) "antirez"  
3) "birthyear"  
4) "1977"  
5) "verified"  
6) "1"  

哈希就是字段值對(fields-values pairs)的集合。由于哈希容易表示對象,事實(shí)上哈希中的字段的數(shù)量并沒有限制,所以你可以在你的應(yīng)用程序以不同的方式來使用哈希。

HMSET 命令為哈希設(shè)置多個字段,HGET 檢索一個單獨(dú)的字段。HMGET 類似于 HGET,但是返回值的數(shù)組:

> hmget user:1000 username birthyear no-such-field  
1) "antirez"  
2) "1977"  
3) (nil)  

也有一些命令可以針對單個字段執(zhí)行操作,例如 HINCRBY:

> hincrby user:1000 birthyear 10  
(integer) 1987  
> hincrby user:1000 birthyear 10  
(integer) 1997  

你可以從命令頁找到全部哈希命令列表。

值得注意的是,小的哈希 (少量元素,不太大的值) 在內(nèi)存中以一種特殊的方式編碼以高效利用內(nèi)存。

Redis 集合 (Sets)

Redis 集合是無序的字符串集合 (collections)。SADD 命令添加元素到集合。還可以對集合執(zhí)行很多其他的操作,例如,測試元素是否存在,對多個集合執(zhí)行交集、并集和差集,等等。

> sadd myset 1 2 3  
(integer) 3  
> smembers myset  
1. 3  
2. 1  
3. 2  

我們向集合總添加了 3 個元素,然后告訴 Redis 返回所有元素。如你所見,他們沒有排序,Redis 在每次調(diào)用時按隨意順序返回元素,因?yàn)闆]有與用戶有任何元素排序協(xié)議。

我們有測試成員關(guān)系的命令。一個指定的元素存在嗎?

> sismember myset 3  
(integer) 1  
> sismember myset 30  
(integer) 0  

“3” 是集合中的成員,”30” 則不是。

集合適用于表達(dá)對象間關(guān)系。例如,我們可以很容易的實(shí)現(xiàn)標(biāo)簽。對這個問題的最簡單建模,就是有一個為每個需要標(biāo)記的對象的集合。集合中保存著與對象相關(guān)的標(biāo)記的 ID。

假設(shè),我們想標(biāo)記新聞。如果我們的 ID 為 1000 的新聞,被標(biāo)簽 1,2,5 和 77 標(biāo)記,我們可以有一個這篇新聞被關(guān)聯(lián)標(biāo)記 ID 的集合:

> sadd news:1000:tags 1 2 5 77  
(integer) 4  

然而有時候我們也想要一些反向的關(guān)系:被某個標(biāo)簽標(biāo)記的所有文章:

> sadd tag:1:news 1000  
(integer) 1  
> sadd tag:2:news 1000  
(integer) 1  
> sadd tag:5:news 1000  
(integer) 1  
> sadd tag:77:news 1000  
(integer) 1  

獲取指定對象的標(biāo)簽很簡單:

> smembers news:1000:tags  
1. 5  
2. 1  
3. 77  
4. 2  

注意:在這個例子中,我們假設(shè)你有另外一個數(shù)據(jù)結(jié)構(gòu),例如,一個 Redis 哈希,存儲標(biāo)簽 ID 到標(biāo)簽名的映射。

還有一些使用正確的 Redis 命令就很容實(shí)現(xiàn)的操作。例如,我們想獲取所有被標(biāo)簽 1,2,10 和 27 同時標(biāo)記的對象列表。我們可以使用 SINTER 命令實(shí)現(xiàn)這個,也就是對不同的集合執(zhí)行交集。我們只需要:

> sinter tag:1:news tag:2:news tag:10:news tag:27:news  
... results here ...  

并不僅僅是交集操作,你也可以執(zhí)行并集,差集,隨機(jī)抽取元素操作等等。

抽取一個元素的命令是 SPOP,就方便為很多問題建模。例如,為了實(shí)現(xiàn)一個基于 web 的撲克游戲,你可以將你的一副牌表示為集合。假設(shè)我們使用一個字符前綴表示(C)lubs 梅花, (D)iamonds 方塊,(H)earts 紅心,(S)pades 黑桃。

>  sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK  
   D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3  
   H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6  
   S7 S8 S9 S10 SJ SQ SK  
   (integer) 52  

現(xiàn)在我們?yōu)槊课贿x手提供 5 張牌。SPOP 命令刪除一個隨機(jī)元素,返回給客戶端,是這個場景下的最佳操作。

然而,如果我們直接對這副牌調(diào)用,下一局我們需要再填充一副牌,這個可能不太理想。所以我們一開始要復(fù)制一下 deck 鍵的集合到 game:1:deck 鍵。

這是通過使用 SUNIONSTORE 命令完成的,這個命令通常對多個集合執(zhí)行交集,然后把結(jié)果存儲在另一個集合中。而對單個集合求交集就是其自身,于是我可以這樣拷貝我的這副牌:

> sunionstore game:1:deck deck  
(integer) 52  

現(xiàn)在我們準(zhǔn)備好為第一個選手提供 5 張牌:

> spop game:1:deck  
"C6"  
> spop game:1:deck  
"CQ"  
> spop game:1:deck  
"D1"  
> spop game:1:deck  
"CJ"  
> spop game:1:deck  
"SJ"  

只有一對 jack,不太理想……

現(xiàn)在是時候介紹提供集合中元素數(shù)量的命令。這個在集合理論中稱為集合的基數(shù)(cardinality,也稱集合的勢),所以相應(yīng)的 Redis 命令稱為 SCARD。

> scard game:1:deck  
(integer) 47  

數(shù)學(xué)計算式為:52 - 5 = 47。

當(dāng)你只需要獲得隨機(jī)元素而不需要從集合中刪除,SRANDMEMBER 命令則適合你完成任務(wù)。它具有返回重復(fù)的和非重復(fù)的元素的能力。

上一篇:Redis 介紹下一篇:集群(中)