天天看點

Redis開發與運維. 2.4 清單

<b>2.4 清單</b>

清單(list)類型是用來存儲多個有序的字元串,如圖2-18所示,a、b、c、d、e五個元素從左到右組成了一個有序的清單,清單中的每個字元串稱為元素(element),一個清單最多可以存儲232-1個元素。在redis中,可以對清單兩端插入(push)和彈出(pop),還可以擷取指定範圍的元素清單、擷取指定索引下标的元素等(如圖2-18和圖2-19所示)。清單是一種比較靈活的資料結構,它可以充當棧和隊列的角色,在實際開發上有很多應用場景。

圖2-18 清單兩端插入和彈出操作

圖2-19 子清單擷取、删除等操作

清單類型有兩個特點:第一、清單中的元素是有序的,這就意味着可以通過索引下标擷取某個元素或者某個範圍内的元素清單,例如要擷取圖2-19的第5個元素,可以執行lindex user:1:message 4(索引從0算起)就可以得到元素e。第二、清單中的元素可以是重複的,例如圖2-20所示清單中包含了兩個字元串a。

圖2-20 清單中可以包含重複元素

這兩個特點在後面介紹集合和有序集合後,會顯得更加突出,是以在考慮是否使用該資料結構前,首先需要弄清楚清單資料結構的特點。

<b>2.4.1 指令</b>

下面将按照對清單的5種操作類型對指令進行介紹,指令如表2-4所示。

1.?添加操作

(1)從右邊插入元素

rpush key value [value ...]

下面代碼從右向左插入元素c、b、a:

127.0.0. 1:6379&gt; rpush listkey c b a

(integer) 3

lrange 0 -1指令可以從左到右擷取清單的所有元素:

127.0.0.1:6379&gt; lrange listkey 0 -1

1) "c"

2) "b"

3) "a"

(2)從左邊插入元素

lpush key value [value ...]

使用方法和rpush相同,隻不過從左側插入,這裡不再贅述。

(3)向某個元素前或者後插入元素

linsert key before|after pivot value

linsert指令會從清單中找到等于pivot的元素,在其前(before)或者後(after)插入一個新的元素value,例如下面操作會在清單的元素b前插入java:

127.0.0.1:6379&gt; linsert listkey before b

java

(integer) 4

傳回結果為4,代表目前指令的長度,目前清單變為:

2) "java"

3) "b"

4) "a"

2.?查找

(1)擷取指定範圍内的元素清單

lrange key start end

lrange操作會擷取清單指定索引範圍所有的元素。索引下标有兩個特點:第一,索引下标從左到右分别是0到n-1,但是從右到左分别是-1到-n。第二,lrange中的end選項包含了自身,這個和很多程式設計語言不包含end不太相同,例如想擷取清單的第2到第4個元素,可以執行如下操作:

127.0.0.1:6379&gt; lrange listkey 1 3

1) "java"

(2)擷取清單指定索引下标的元素

lindex key index

例如目前清單最後一個元素為a:

127.0.0.1:6379&gt; lindex listkey -1

"a"

(3)擷取清單長度

llen key

例如,下面示例目前清單長度為4:

127.0.0.1:6379&gt; llen listkey

3.?删除

(1)從清單左側彈出元素

lpop key

如下操作将清單最左側的元素c會被彈出,彈出後清單變為java、b、a:

127.0.0.1:6379&gt;t lpop listkey

"c"

(2)

從清單右側彈出

rpop key

它的使用方法和lpop是一樣的,隻不過從清單右側彈出,這裡不再贅述。

(3)删除指定元素

lrem key count value

lrem指令會從清單中找到等于value的元素進行删除,根據count的不同分為三種情況:

count&gt;0,從左到右,删除最多count個元素。

count&lt;0,從右到左,删除最多count絕對值個元素。

count=0,删除所有。

例如向清單從左向右插入5個a,那麼目前清單變為“a a a a a java b a”,下面操作将從清單左邊開始删除4個為a的元素:

127.0.0.1:6379&gt; lrem listkey 4 a

1) "a"

(4)按照索引範圍修剪清單

ltrim key start end

例如,下面操作會隻保留清單listkey第2個到第4個元素:

127.0.0.1:6379&gt; ltrim listkey 1 3

ok

4.?修改

修改指定索引下标的元素:

lset key index newvalue

下面操作會将清單listkey中的第3個元素設定為python:

127.0.0.1:6379&gt; lset listkey 2 python

3) "python"

5.?阻塞操作

阻塞式彈出如下:

blpop key [key ...] timeout

brpop key [key ...] timeout

blpop和brpop是lpop和rpop的阻塞版本,它們除了彈出方向不同,使用方法基本相同,是以下面以brpop指令進行說明,brpop指令包含兩個參數:

key [key ...]:多個清單的鍵。

timeout:阻塞時間(機關:秒)。

1)清單為空:如果timeout=3,那麼用戶端要等到3秒後傳回,如果timeout=0,那麼用戶端一直阻塞等下去:

127.0.0.1:6379&gt; brpop list:test 3

(nil)

(3.10s)

127.0.0.1:6379&gt; brpop list:test 0

...阻塞...

如果此期間添加了資料element1,用戶端立即傳回:

1) "list:test"

2) "element1"

(2.06s)

2)清單不為空:用戶端會立即傳回。

在使用brpop時,有兩點需要注意。

第一點,如果是多個鍵,那麼brpop會從左至右周遊鍵,一旦有一個鍵能彈出元素,用戶端立即傳回:

127.0.0.1:6379&gt; brpop list:1 list:2

list:3 0

..阻塞..

此時另一個用戶端分别向list:2和list:3插入元素:

client-lpush&gt; lpush list:2 element2

(integer) 1

client-lpush&gt; lpush list:3 element3

用戶端會立即傳回list:2中的element2,因為list:2最先有可以彈出的元素:

1) "list:2"

2) "element2_1"

第二點,如果多個用戶端對同一個鍵執行brpop,那麼最先執行brpop指令的用戶端可以擷取到彈出的值。

用戶端1:

client-1&gt; brpop list:test 0

用戶端2:

client-2&gt; brpop list:test 0

用戶端3:

client-3&gt; brpop list:test 0

此時另一個用戶端lpush一個元素到list:test清單中:

client-lpush&gt; lpush list:test element

那麼用戶端1最會擷取到元素,因為用戶端1最先執行brpop,而用戶端2和用戶端3繼續阻塞:

client&gt; brpop list:test 0

2) "element"

有關清單的基礎指令已經介紹完了,表2-5是這些指令的時間複雜度,開發人員可以參考此表選擇适合的指令。

表2-5 清單指令時間複雜度

操作類型         命  令         時間複雜度

添加         rpush key value [value

...]        o(k),k是元素個數

         lpush

key value [value ...]         o(k),k是元素個數

         linsert

key before|after pivot value         o(n),n是pivot距離清單頭或尾的距離

查找         lrange key start end         o(s+n),s是start偏移量,n是start到end

的範圍

         lindex

key index        o(n),n是索引的偏移量

         llen

key     o(1)

删除         lpop key   o(1)

         rpop

key   o(1)

         lrem

count value      o(n),n是清單長度

         ltrim

key start end   o(n),n是要裁剪的元素總數

修改         lset key index value o(n),n是索引的偏移量

阻塞操作         blpop brpop      o(1)

<b>2.4.2 内部編碼</b>

清單類型的内部編碼有兩種。

ziplist(壓縮清單):當清單的元素個數小于list-max-ziplist-entries配置(預設512個),同時清單中每個元素的值都小于list-max-ziplist-value配置時(預設64位元組),redis會選用ziplist來作為清單的内部實作來減少記憶體的使用。

linkedlist(連結清單):當清單類型無法滿足ziplist的條件時,redis會使用linkedlist作為清單的内部實作。

下面的示例示範了清單類型的内部編碼,以及相應的變化。

1)當元素個數較少且沒有大元素時,内部編碼為ziplist:

127.0.0.1:6379&gt; rpush listkey e1 e2 e3

127.0.0.1:6379&gt; object encoding listkey

"ziplist"

2.1)當元素個數超過512個,内部編碼變為linkedlist:

127.0.0.1:6379&gt; rpush listkey e4 e5 ...忽略... e512

e513

(integer) 513

"linkedlist"

2.2)或者當某個元素超過64位元組,内部編碼也會變為linkedlist:

127.0.0.1:6379&gt; rpush listkey "one

string is bigger than 64 byte...............

................."

redis 3.2版本提供了quicklist内部編碼,簡單地說它是以一個ziplist為節點的linkedlist,它結合了ziplist和linkedlist兩者的優勢,為清單類型提供了一種更為優秀的内部編碼實作,它的設計原理可以參考redis的另一個作者matt stancliff的部落格:https://matt.sh/redis-quicklist。

有關清單類型的記憶體優化技巧将在8.3節詳細介紹。

<b>2.4.3 使用場景</b>

1.?消息隊列

如圖2-21所示,redis的lpush+brpop指令組合即可實作阻塞隊列,生産者用戶端使用lrpush從清單左側插入元素,多個消費者用戶端使用brpop指令阻塞式的“搶”清單尾部的元素,多個用戶端保證了消費的負載均衡和高可用性。

2.?文章清單

每個使用者有屬于自己的文章清單,現需要分頁展示文章清單。此時可以考慮使用清單,因為清單不但是有序的,同時支援按照索引範圍擷取元素。

圖2-21 redis消息隊列模型

1)每篇文章使用哈希結構存儲,例如每篇文章有3個屬性title、timestamp、content:

hmset acticle:1 title xx timestamp

1476536196 content xxxx

...

hmset acticle:k title yy timestamp

1476512536 content yyyy

2)向使用者文章清單添加文章,user:{id}:articles作為使用者文章清單的鍵:

lpush user:1:acticles article:1 article3

lpush user:k:acticles article:5

3)分頁擷取使用者文章清單,例如下面僞代碼擷取使用者id=1的前10篇文章:

articles = lrange user:1:articles 0 9

for article in {articles}

hgetall {article}

使用清單類型儲存和擷取文章清單會存在兩個問題。第一,如果每次分頁擷取的文章個數較多,需要執行多次hgetall操作,此時可以考慮使用pipeline(第3章會介紹)批量擷取,或者考慮将文章資料序列化為字元串類型,使用mget批量擷取。第二,分頁擷取文章清單時,lrange指令在清單兩端性能較好,但是如果清單較大,擷取清單中間範圍的元素性能會變差,此時可以考慮将清單做二級拆分,或者使用redis 3.2的quicklist内部編碼實作,它結合ziplist和linkedlist的特點,擷取清單中間範圍的元素時也可以高效完成。

實際上清單的使用場景很多,在選擇時可以參考以下口訣:

lpush + lpop = stack(棧)

lpush + rpop = queue(隊列)

lpsh + ltrim = capped collection(有限集合)

lpush + brpop = message queue(消息隊列)