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

列表是一種比較 靈活 的 資料結構 ,它可以充當 棧 和 佇列 的角色,在實際開發上有很多應用場景。
如圖所示, a
、 b
、 c
、 d
、 e
五個元素 從左到右 組成了一個 有序的列表 ,列表中的每個字串稱為 元素 ( element
),一個列表最多可以儲存 2 ^ 32 - 1
個元素。
- 列表的 插入 和 彈出 操作

- 列表的 獲取 、 擷取 和 刪除 操作

正文
1. 相關命令
下面將按照對 列表 的 5
種 操作型別 對命令進行介紹:

1.1. 新增命令
1.1.1. 從右邊插入元素
rpush key value [value ...]
下面程式碼 從右向左 插入元素 c
、 b
、 a
:
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
操作會獲取列表 指定索引 範圍所有的元素。
索引下標有兩個特點:
-
其一,索引下標 從左到右 分別是
0
到N-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
彈出,彈出後 列表 變為 redis
、 b
、 a
。
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, 刪除所有 。
例如向列表 從左向右 插入 5
個 a
,那麼當前 列表 變為 “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
blpop
和 brpop
是 lpop
和 rpop
的 阻塞版本 ,它們除了 彈出方向 不同, 使用方法 基本相同,所以下面以 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:2
和 list: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" 複製程式碼
有關 列表 的 基礎命令 已經介紹完了,下表是相關命令的 時間複雜度 :

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
,它結合了 ziplist
和 linkedlist
兩者的優勢,為 列表型別 提供了一種更為優秀的 內部編碼 實現,它的設計原理可以參考 Redis
的另一個作者 Matt Stancliff
的部落格redis-quicklist。
3. 應用場景
3.1. 訊息佇列
通過 Redis
的 lpush + brpop
命令組合 ,即可實現 阻塞佇列 。如圖所示:

生產者客戶端使用 lrpush
從列表 左側插入元素 , 多個消費者客戶端 使用 brpop
命令 阻塞式 的 “搶” 列表 尾部 的元素, 多個客戶端 保證了消費的 負載均衡 和 高可用性 。
3.2. 文章列表
每個 使用者 有屬於自己的 文章列表 ,現需要 分頁 展示文章列表。此時可以考慮使用 列表 ,因為列表不但是 有序的 ,同時支援 按照索引範圍 獲取元素。
- 每篇文章使用 雜湊結構 儲存,例如每篇文章有
3
個屬性title
、timestamp
、content
:
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.2
的quicklist
內部編碼實現,它結合ziplist
和linkedlist
的特點,獲取列表 中間範圍 的元素時也可以 高效完成 。
3.3. 其他場景
實際上列表的使用場景很多,具體可以參考如下:
命令組合 | 對應資料結構 |
---|---|
lpush + lpop | Stack(棧) |
lpush + rpop | Queue(佇列) |
lpush + ltrim | Capped Collection(有限集合) |
lpush + brpop | Message Queue(訊息佇列) |
小結
本文介紹了 Redis
中的 列表 的 一些 基本命令 、 內部編碼 和 適用場景 。通過組合不同 命令 ,可以把 列表 轉換為不同的 資料結構 使用。