深入剖析Redis系列(七) - Redis資料結構之列表

零壹技術棧發表於2018-10-20

前言

列表list)型別是用來儲存多個 有序字串。在 Redis 中,可以對列表的 兩端 進行 插入push)和 彈出pop)操作,還可以獲取 指定範圍元素列表、獲取 指定索引下標元素 等。

深入剖析Redis系列(七) - Redis資料結構之列表

列表 是一種比較 靈活資料結構,它可以充當 佇列 的角色,在實際開發上有很多應用場景。

如圖所示,abcde 五個元素 從左到右 組成了一個 有序的列表,列表中的每個字串稱為 元素element),一個列表最多可以儲存 2 ^ 32 - 1 個元素。

  • 列表的 插入彈出 操作

深入剖析Redis系列(七) - Redis資料結構之列表

  • 列表的 獲取擷取刪除 操作

深入剖析Redis系列(七) - Redis資料結構之列表

其他文章

正文

1. 相關命令

下面將按照對 列表5操作型別 對命令進行介紹:

深入剖析Redis系列(七) - Redis資料結構之列表

1.1. 新增命令

1.1.1. 從右邊插入元素

rpush key value [value ...]

下面程式碼 從右向左 插入元素 cba

127.0.0.1:6379> rpush listkey c b a
(integer) 3
複製程式碼

lrange 0 -1 命令可以 從左到右 獲取列表的 所有元素

127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
複製程式碼

1.1.2. 從左邊插入元素

lpush key value [value ...]

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

1.1.3. 向某個元素前或者後插入元素

linsert key before|after pivot value

linsert 命令會從 列表 中找到 第一個 等於 pivot 的元素,在其 before)或者 after)插入一個新的元素 value,例如下面操作會在列表的 元素 b 前插入 redis

127.0.0.1:6379> linsert listkey before b redis
(integer) 4
複製程式碼

返回結果為 4,代表當前 列表長度,當前列表變為:

127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "redis"
3) "b"
4) "a"
複製程式碼

1.2. 查詢命令

1.2.1. 獲取指定範圍內的元素列表

lrange key start stop

lrange 操作會獲取列表 指定索引 範圍所有的元素。

索引下標 有兩個特點:

  • 其一,索引下標 從左到右 分別是 0N-1,但是 從右到左 分別是 -1-N

  • 其二,lrange 中的 end 選項包含了 自身,這個和很多程式語言不包含 end 不太相同。

從左到右 獲取列表的第 2 到第 4 個元素,可以執行如下操作:

127.0.0.1:6379> lrange listkey 1 3
1) "redis"
2) "b"
3) "a"
複製程式碼

從右到左 獲取列表的第 1 到第 3 個元素,可以執行如下操作:

127.0.0.1:6379> lrange listkey -3 -1
1) "redis"
2) "b"
3) "a"
複製程式碼

1.2.2. 獲取列表指定索引下標的元素

lindex key index

例如當前列表 最後一個 元素為 a

127.0.0.1:6379> lindex listkey -1
"a"
複製程式碼

1.2.3. 獲取列表長度

llen key

例如,下面示例 當前列表長度4

127.0.0.1:6379> llen listkey
(integer) 4
複製程式碼

1.3. 刪除命令

1.3.1. 從列表左側彈出元素

lpop key

如下操作將 列表 最左側的元素 c 彈出,彈出後 列表 變為 redisba

127.0.0.1:6379> lpop listkey
"c"
127.0.0.1:6379> lrange listkey 0 -1
1) "redis"
2) "b"
3) "a"
複製程式碼

1.3.2. 從列表右側彈出元素

rpop key

它的使用方法和 lpop 是一樣的,只不過從列表 右側 彈出元元素。

127.0.0.1:6379> lpop listkey
"a"
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "redis"
3) "b"
複製程式碼

1.3.3. 刪除指定元素

lrem key count value

lrem 命令會從 列表 中找到 等於 value 的元素進行 刪除,根據 count 的不同分為三種情況:

  • count > 0從左到右,刪除最多 count 個元素。

  • count < 0從右到左,刪除最多 count 絕對值 個元素。

  • count = 0刪除所有

例如向列表 從左向右 插入 5a,那麼當前 列表 變為 “a a a a a redis b a”,下面操作將從列表 左邊 開始刪除 4 個為 a 的元素:

127.0.0.1:6379> lrem listkey 4 a
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "redis"
3) "b"
4) "a"
複製程式碼

1.3.4. 按照索引範圍修剪列表

127.0.0.1:6379> ltrim listkey 1 3
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "redis"
2) "b"
3) "a"
複製程式碼

1.4. 修改命令

1.4.1. 修改指定索引下標的元素

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

lset key index newValue

下面操作會將列表 listkey 中的第 3 個元素設定為 mysql

127.0.0.1:6379> lset listkey 2 mysql
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "redis"
2) "b"
3) "mysql"
複製程式碼

1.5. 阻塞操作命令

阻塞式彈出 操作的命令如下:

blpop key [key ...] timeout brpop key [key ...] timeout

blpopbrpoplpoprpop阻塞版本,它們除了 彈出方向 不同,使用方法 基本相同,所以下面以 brpop 命令進行說明, brpop 命令包含兩個引數:

  • key[key...]:一個列表的 多個鍵

  • timeout阻塞 時間(單位:)。

對於 timeout 引數,要氛圍 列表為空不為空 兩種情況:

  • 列表為空

如果 timeout = 3,那麼 客戶端 要等到 3 秒後返回,如果 timeout = 0,那麼 客戶端 一直 阻塞 等下去:

127.0.0.1:6379> brpop list:test 3
(nil)
(3.10s)
127.0.0.1:6379> brpop list:test 0
...阻塞...
複製程式碼

如果此期間新增了資料 element1,客戶端 立即返回

127.0.0.1:6379> brpop list:test 3
1) "list:test"
2) "element1"
(2.06s)
複製程式碼
  • 列表不為空:客戶端會 立即返回
127.0.0.1:6379> brpop list:test 0
1) "list:test"
2) "element1"
複製程式碼

在使用 brpop 時,有以下兩點需要注意:

  • 其一,如果是 多個鍵,那麼 brpop從左至右 遍歷鍵,一旦有 一個鍵彈出元素,客戶端 立即返回
127.0.0.1:6379> brpop list:1 list:2 list:3 0
..阻塞..
複製程式碼

此時另一個 客戶端 分別向 list:2list:3 插入元素:

client-lpush> lpush list:2 element2
(integer) 1
client-lpush> lpush list:3 element3
(integer) 1
複製程式碼

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

127.0.0.1:6379> brpop list:1 list:2 list:3 0
1) "list:2"
2) "element2"
複製程式碼
  • 其二,如果 多個客戶端同一個鍵 執行 brpop,那麼 最先執行 brpop 命令的 客戶端 可以 獲取 到彈出的值。

按先後順序在 3 個客戶端執行 brpop 命令:

  • 客戶端1:
client-1> brpop list:test 0
...阻塞...
複製程式碼
  • 客戶端2:
client-2> brpop list:test 0
...阻塞...
複製程式碼
  • 客戶端3:
client-3> brpop list:test 0
...阻塞...
複製程式碼

此時另一個 客戶端 lpush 一個元素到 list:test 列表中:

client-lpush> lpush list:test element
(integer) 1
複製程式碼

那麼 客戶端 1 會獲取到元素,因為 客戶端 1 最先執行 brpop 命令,而 客戶端 2客戶端 3 會繼續 阻塞

client> brpop list:test 0
1) "list:test"
2) "element"
複製程式碼

有關 列表基礎命令 已經介紹完了,下表是相關命令的 時間複雜度

深入剖析Redis系列(七) - Redis資料結構之列表

2. 內部編碼

列表型別的 內部編碼 有兩種:

2.1. ziplist(壓縮列表)

當列表的元素個數 小於 list-max-ziplist-entries 配置(預設 512 個),同時列表中 每個元素 的值都 小於 list-max-ziplist-value 配置時(預設 64 位元組),Redis 會選用 ziplist 來作為 列表內部實現 來減少記憶體的使用。

2.2. linkedlist(連結串列)

列表型別 無法滿足 ziplist 的條件時, Redis 會使用 linkedlist 作為 列表內部實現

2.3. 編碼轉換

下面的示例演示了 列表型別內部編碼,以及相應的變化。

  • 當元素 個數較少沒有大元素 時,內部編碼ziplist
127.0.0.1:6379> rpush listkey e1 e2 e3
(integer) 3
127.0.0.1:6379> object encoding listkey
"ziplist"
複製程式碼
  • 當元素個數超過 512 個,內部編碼 變為 linkedlist
127.0.0.1:6379> rpush listkey e4 e5 ... e512 e513
(integer) 513
127.0.0.1:6379> object encoding listkey
"linkedlist"
複製程式碼
  • 當某個元素超過 64 位元組內部編碼 也會變為 linkedlist
127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte..."
(integer) 4
127.0.0.1:6379> object encoding listkey
"linkedlist"
複製程式碼

Redis3.2 版本提供了 quicklist 內部編碼,簡單地說它是以一個 ziplist節點linkedlist,它結合了 ziplistlinkedlist 兩者的優勢,為 列表型別 提供了一種更為優秀的 內部編碼 實現,它的設計原理可以參考 Redis 的另一個作者 Matt Stancliff 的部落格 redis-quicklist

3. 應用場景

3.1. 訊息佇列

通過 Redislpush + brpop 命令組合,即可實現 阻塞佇列。如圖所示:

深入剖析Redis系列(七) - Redis資料結構之列表

生產者客戶端 使用 lrpush 從列表 左側插入元素多個消費者客戶端 使用 brpop 命令 阻塞式“搶” 列表 尾部 的元素,多個客戶端 保證了消費的 負載均衡高可用性

3.2. 文章列表

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

  • 每篇文章使用 雜湊結構 儲存,例如每篇文章有 3 個屬性 titletimestampcontent
hmset acticle:1 title xx timestamp 1476536196 content xxxx
hmset acticle:2 title yy timestamp 1476536196 content yyyy
...
hmset acticle:k title kk timestamp 1476512536 content kkkk
複製程式碼
  • 向使用者文章列表 新增文章user:{id}:articles 作為使用者文章列表的
lpush user:1:acticles article:1 article:3 article:5
lpush user:2:acticles article:2 article:4 article:6
...
lpush user:k:acticles article:7 article:8
複製程式碼
  • 分頁 獲取 使用者文章列表,例如下面 虛擬碼 獲取使用者 id=1 的前 10 篇文章:
articles = lrange user:1:articles 0 9
for article in {articles}
    hgetall {article}
複製程式碼

使用 列表 型別 儲存獲取 文章列表會存在兩個問題:

  • 第一:如果每次 分頁 獲取的 文章個數較多,需要執行多次 hgetall 操作,此時可以考慮使用 Pipeline 進行 批量獲取,或者考慮將文章資料 序列化為字串 型別,使用 mget 批量獲取

  • 第二分頁 獲取 文章列表 時, lrange 命令在列表 兩端效能較好,但是如果 列表較大,獲取列表 中間範圍 的元素 效能會變差。此時可以考慮將列表做 二級拆分,或者使用 Redis 3.2quicklist 內部編碼實現,它結合 ziplistlinkedlist 的特點,獲取列表 中間範圍 的元素時也可以 高效完成

3.3. 其他場景

實際上列表的使用場景很多,具體可以參考如下:

命令組合 對應資料結構
lpush + lpop Stack(棧)
lpush + rpop Queue(佇列)
lpush + ltrim Capped Collection(有限集合)
lpush + brpop Message Queue(訊息佇列)

小結

本文介紹了 Redis 中的 列表 的 一些 基本命令內部編碼適用場景。通過組合不同 命令,可以把 列表 轉換為不同的 資料結構 使用。

參考

《Redis 開發與運維》


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章