Redis的數據結構之 list

書接上回

上一篇文章 Redis的數據結構 string我們一起學習了這種類型的常用命令,並且還學習了 Redis中的字符串的結構表示以及好處,這裏我們接着學習另外一種數據結構 list

list 簡介

list, 一般都會稱爲列表。在Redis中,這種數據結構是一種比較靈活的結構,由於其元素的是有序的,所以可以充當棧和隊列這兩種數據結構。實際在開發總也有很多應用場景。

一個List最多可以包含 2^32-1個元素。

很多人都會以爲list是用數組來實現的,非也,非也。它內部是quicklist這種數據結構. 想要先睹爲快的,那麼坐電梯直達吧。

list的相關命令

LPUSH命令

  • 語法

LPUSH key value [value …]

  • 解釋

lpush : left push

將一個或者多個值插入到列表key的表頭,返回列表的長度。元素可以是重複的。

如果key不存在,那麼會先穿件一個列表,然後再執行push操作.

如果key值存在,但是value類型不是列表類型時,會返回一個錯誤。

  • 演示
# 設置一個列表
127.0.0.1:6379> LPUSH k22 v22
(integer) 1
# 查詢指定區間內的數據,使用lrange命令
127.0.0.1:6379> LRANGE k22 0 10
1) "v22"
# 一次插入多個值
127.0.0.1:6379> LPUSH k22 v22_1 v22_2 v22_3 v22_4
(integer) 5
127.0.0.1:6379> LRANGE k22 0 10
1) "v22_4"
2) "v22_3"
3) "v22_2"
4) "v22_1"
5) "v22"

lpushx 命令

  • 語法

LPUSHX key value

  • 解釋

僅當 key 存在的時候,纔將 value 插入列表的表頭。返回列表中元素的個數。

  • 演示
# 當key值不存在的時候,不會放入列表中
127.0.0.1:6379> LPUSHX k23 v23
(integer) 0
# 再次嘗試放入,也不可以。
127.0.0.1:6379> LPUSHX k23 v23
(integer) 0
# 先往數組放入一個元素
127.0.0.1:6379> lpush k23 v23
(integer) 1
# 再次嘗試使用lpushx放入數據
127.0.0.1:6379> LPUSHX k23 v23_1
(integer) 2
# 再次嘗試使用lpushx放入數據
127.0.0.1:6379> LPUSHX k23 v23_2
(integer) 3
# 查看列表 k23 中的數據。注意:和插入的順序是相反的。
127.0.0.1:6379> Lrange k23 0 -1
1) "v23_2"
2) "v23_1"
3) "v23"

rpush 命令

  • 語法

RPUSH key value [value ...]

  • 解釋

rpush 就是right push。將一個或多個值 value 插入到列表 key 的表尾(最右邊)。返回列表的長度。

如果 key 不存在的時候,會創建一個空列表,然後在執行 rpush 操作。

如果 key 存在,但是不是一個列表類型時,返回一個錯誤。

  • 演示
# 往列表中加入數據
127.0.0.1:6379> RPUSH k24 v24
(integer) 1
127.0.0.1:6379> RPUSH k24 v24_1 v25_2 v25_3
(integer) 4
127.0.0.1:6379> lrange k24 0 -1
1) "v24"
2) "v24_1"
3) "v25_2"
4) "v25_3"
# 演示 key 存在,但是不是一個列表類型
127.0.0.1:6379> set k24_1 v24_1
OK
127.0.0.1:6379> rpush k24_1 v24_1
(error) WRONGTYPE Operation against a key holding the wrong kind of value

rpushx 命令

  • 語法

rpushx key value

  • 解釋

lpushx 類似,如果key不存在時,什麼都不會操作。如果key存在,纔會將元素添加到表尾。

  • 演示
# key不存在的時候,不會插入數據
127.0.0.1:6379> rpushx k25 v25
(integer) 0
# 先設置一個列表
127.0.0.1:6379> rpush k25 v25_1
(integer) 1
127.0.0.1:6379> rpushx k25 v25_2
(integer) 2
127.0.0.1:6379> rpushx k25 v25_3
(integer) 3
# 查看列表中的數據。注意和插入的順序是一致的。
127.0.0.1:6379> lrange k25 0 -1
1) "v25_1"
2) "v25_2"
3) "v25_3"

lpop 命令

  • 語法

LPOP key

  • 解釋

left pop;

移除並返回列表的頭元素. 當key不存在的時候,返回nil

  • 演示
# key不存在的時候,返回nil
127.0.0.1:6379> LPOP k26
(nil)
# 設置一個列表,有三個元素
127.0.0.1:6379> lpush k26 v26_1 v26_2 v26_3
(integer) 3
# 查看列表中的元素
127.0.0.1:6379> lrange k26 0 -1
1) "v26_3"
2) "v26_2"
3) "v26_1"
# 依次pop出元素
127.0.0.1:6379> lpop k26
"v26_3"
127.0.0.1:6379> lpop k26
"v26_2"
127.0.0.1:6379> lpop k26
"v26_1"
127.0.0.1:6379> lpop k26
(nil)

tip: lpush + lpop => 棧, rpush + lpop => 隊列。

rpop 命令

  • 語法

rpop key

  • 解釋

rpopright pop;

和lpop相反。移除並返回列表的尾元素。如果key不存在返回 nil。

  • 演示
# key 不存在,返回nil
127.0.0.1:6379> rpop k27
(nil)
# 先設置一個列表
127.0.0.1:6379> lpush k27 v27_1 v27_2 v27_3
(integer) 3
127.0.0.1:6379> lrange k27 0 -1
1) "v27_3"
2) "v27_2"
3) "v27_1"
# 一次pop每個值
127.0.0.1:6379> rpop k27
"v27_1"
127.0.0.1:6379> rpop k27
"v27_2"
127.0.0.1:6379> rpop k27
"v27_3"
127.0.0.1:6379> rpop k27
(nil)

tip: lpush + rpop => 隊列, rpush + rpop => 棧。

lrange 命令

  • 語法

LRANGE key start stop

  • 解釋

獲取指定區間內的元素。0表示第一個元素。如果超過了實際範圍就返回空數組。

  • 演示
127.0.0.1:6379> LRANGE k22 0 10
1) "v22_4"
2) "v22_3"
3) "v22_2"
4) "v22_1"
5) "v22"
127.0.0.1:6379> LRANGE k22 0 1
1) "v22_4"
2) "v22_3"
127.0.0.1:6379> LRANGE k22 10 100
(empty list or set)

rpoplpush 命令

  • 語法

RPOPLPUSH source destination

  • 解釋

source 的尾元素插入到destination列表的頭元素中,返回該元素。 注意,這是一個原子操作。

比如: source: a,b,c

distination: 1,2,3

使用 RPOPLPUSH source distination ,則:

source: a,b

distination: c,1,2,3

  • 演示
# 設置列表1
127.0.0.1:6379> lpush k28_1 v28_c v28_b v28_a
(integer) 3
# 設置列表2
127.0.0.1:6379> lpush k28_2 v28_3 v28_2 v28_1
(integer) 3
# 使用 rpoppush命令
127.0.0.1:6379> RPOPLPUSH k28_1 k28_2
"v28_c"
# 查看列表1
127.0.0.1:6379> lrange k28_1 0 -1
1) "v28_a"
2) "v28_b"
# 查看列表2
127.0.0.1:6379> lrange k28_2 0 -1
1) "v28_c"
2) "v28_1"
3) "v28_2"
4) "v28_3"

lrem 命令

  • 語法

LREM key count value

  • 解釋

至多移除列表中 count 個與參數 value 相等的元素。

有以下情況:

count > 0 : 從表頭開始向表尾搜索,移除與 value 相等的元素,最多移除count個 。

count < 0 : 從表尾開始向表頭搜索,移除與 value 相等的元素,最多移除|count|個。

count = 0 : 移除表中所有與 value 相等的值。

  • 演示
# 演示 count>0 時
# 設置一個列表
127.0.0.1:6379> lpush k29_1 v29_1  v29  v29_2 v29 v29_3 v29
(integer) 6
# 從表頭開始,移除2個 v29
127.0.0.1:6379> lrem k29_1 2 v29
(integer) 2
127.0.0.1:6379> lrange k29_1 0 -1
1) "v29_3"
2) "v29_2"
3) "v29"
4) "v29_1"
# 演示count<0 時
127.0.0.1:6379> lpush k29_2 v29_1 v29  v29_2 v29 v29_3 v29
(integer) 6
127.0.0.1:6379> lrem k29_2 -2 v29
(integer) 2
127.0.0.1:6379> LRANGE k29_2 0 -1
1) "v29"
2) "v29_3"
3) "v29_2"
4) "v29_1"

# 演示count=0時
127.0.0.1:6379> lpush k29_3 v29_1 v29  v29_2 v29 v29_3 v29
(integer) 6
127.0.0.1:6379> lrem k29_3 0 v29
(integer) 3
127.0.0.1:6379> LRANGE k29_3 0 -1
1) "v29_3"
2) "v29_2"
3) "v29_1"

llen 命令

  • 語法

LLEN key

  • 解釋

獲取列表的長度。

如果 key 不存在的時候,返回0.

如果 key 對應類型不是 list ,則返回一個錯誤。

  • 演示
127.0.0.1:6379> llen k30
(integer) 0
127.0.0.1:6379> lpush k30 v30_1 v30_2
(integer) 2
127.0.0.1:6379> llen k30
(integer) 2

# 刪掉k30,演示,類型不是list的時候,報錯
127.0.0.1:6379> del k30
(integer) 1
127.0.0.1:6379> set k30 v30
OK
127.0.0.1:6379> llen k30
(error) WRONGTYPE Operation against a key holding the wrong kind of value

lindex 命令

  • 語法

lindex key index

  • 解釋

返回列表中,下標爲 index 的元素. -1 表示列表的最後一個元素, 如果key不存在,或者index超出範圍,返回nil, 如果key不是一個列表類型, 返回一個錯誤。

  • 演示
127.0.0.1:6379> lpush k31 v31_3 v31_2 v31_1
(integer) 3
127.0.0.1:6379> LINDEX k31 2
"v31_3"
127.0.0.1:6379> LINDEX k31 1
"v31_2"
127.0.0.1:6379> LINDEX k31 0
"v31_1"

linsert 命令

  • 語法

linsert key BEFORE|AFTER pivot value

  • 解釋

value插入到key隊列pivot值之前或者之後. 返回插入完成之後列表的長度。

如果 pivot 不存在 或者 key 不存在, 不執行任何操作。

如果 key 對應的不是一個列表類型, 返回一個錯誤。

  • 演示
127.0.0.1:6379> linsert k32 BEFORE k31_1 k31_0
(integer) 0
127.0.0.1:6379> lpush k32 v32_1
(integer) 1
# k32_3 => pivot不存在
127.0.0.1:6379> linsert k32 BEFORE v32_3 v31_2
(integer) -1
# pivot之前插入
127.0.0.1:6379> linsert k32 BEFORE v32_1 v31_0
(integer) 2
# pivot之後插入
127.0.0.1:6379> linsert k32 AFTER v32_1 v31_2
(integer) 3

lset 命令

  • 語法

lset key index value

  • 解釋

將列表中的 索引爲index的值設置爲value。 如果index超出範圍,則返回一個錯誤

  • 演示
127.0.0.1:6379> lpush k33 v33_3 v33_1
(integer) 2
127.0.0.1:6379> lrange k33 0 -1
1) "v33_1"
2) "v33_3"
## 將第二個值,索引爲1,設置爲v33_2
127.0.0.1:6379> lset k33 1 v33_2
OK
127.0.0.1:6379> lrange k33 0 -1
1) "v33_1"
2) "v33_2"

# 超出範圍返回錯誤
127.0.0.1:6379> lset k33 2 v33_2
(error) ERR index out of range

ltrim 命令

  • 語法

ltrim key start stop

  • 解釋

保留列表從startstop之間的元素。其他元素都將被刪除。 注意:包含(不刪除)startstop兩個元素.

如果key不存在,直接返回OK, 如果key對應的不是列表,直接返回錯誤。

  • 演示
127.0.0.1:6379> lpush k34 v34_1 v34_2 v34_3 v34_4 v34_5 v34_6
(integer) 6
127.0.0.1:6379> ltrim k34 1 4
OK
127.0.0.1:6379> lrange k34  0 -1
1) "v34_5"
2) "v34_4"
3) "v34_3"
4) "v34_2"

blpop 命令

  • 語法

BLPOP key [key ...] timeout

  • 解釋

lpop 的 阻塞版本。 block left pop

當給定列表內沒有任何元素可供彈出的時候,連接將被 BLPOP 命令阻塞,直到等待超時或發現可彈出元素爲止。
當給定多個 key 參數時,按參數 key 的先後順序依次檢查各個列表,彈出第一個非空列表的頭元素。

  • 演示
# push到三組列表,分別三個元素
127.0.0.1:6379> lpush k35 v35_1 v35_2 v35_3
(integer) 3
127.0.0.1:6379> lrange k35 0 -1
1) "v35_3"
2) "v35_2"
3) "v35_1"
127.0.0.1:6379> lpush k35_1 v35_1 v35_2 v35_3
(integer) 3
127.0.0.1:6379> lpush k35_2 v35_1 v35_2 v35_3
(integer) 3

# 阻塞調用lpop, 從左到右 依次pop元素,直到有一個元素可以pop。
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
1) "k35"
2) "v35_3"
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
1) "k35"
2) "v35_2"
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
1) "k35"
2) "v35_1"
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
1) "k35_1"
2) "v35_3"
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
1) "k35_1"
2) "v35_2"
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
1) "k35_1"
2) "v35_1"
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
1) "k35_2"
2) "v35_3"
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
1) "k35_2"
2) "v35_2"
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
1) "k35_2"
2) "v35_1"
127.0.0.1:6379> blpop k35 k35_1 k35_2 10
# 沒有元素的時候會阻塞一直到超時。
(nil)
(10.59s)

brpop 命令

  • 語法

BRPOP key [key ...] timeout

  • 解釋

rpop 的阻塞版本。 block right pop
當給定多個key的時候,按照key的先後順序依次檢查各個列表。直到彈出一個元素或者超時。

  • 演示
# 設置兩個列表
127.0.0.1:6379> lpush k36 v36_1 v36_2 v36_3
(integer) 3
127.0.0.1:6379> lpush k36_1 v36_1 v36_2 v36_3
(integer) 3
# 阻塞式的pop出每個值。
127.0.0.1:6379> BRPOP k36 k36_1 10
1) "k36"
2) "v36_1"
127.0.0.1:6379> BRPOP k36 k36_1 10
1) "k36"
2) "v36_2"
127.0.0.1:6379> BRPOP k36 k36_1 10
1) "k36"
2) "v36_3"
127.0.0.1:6379> BRPOP k36 k36_1 10
1) "k36_1"
2) "v36_1"
127.0.0.1:6379> BRPOP k36 k36_1 10
1) "k36_1"
2) "v36_2"
127.0.0.1:6379> BRPOP k36 k36_1 10
1) "k36_1"
2) "v36_3"
127.0.0.1:6379> BRPOP k36 k36_1 10
# 阻塞10s
(nil)
(10.61s)

Tips: lpush + brpop => 阻塞隊列。

brpoplpush 命令

  • 語法

BRPOPLPUSH source destination timeout

  • 解釋

rpoplpush 的阻塞版本。 block right left push

當列表 source 爲空的時候,該命令將阻塞,直到超時,或者source中有一個元素可以pop。

  • 演示
# 設置一個列表
127.0.0.1:6379> lpush k37_source v37_1 v37_2 v37_3 v37_4
(integer) 4
# 將source移動到distination中。
127.0.0.1:6379> BRPOPLPUSH k37_source k37_distination 10
"v37_1"
# 查看下distination。
127.0.0.1:6379> lrange k37_distination 0 -1
1) "v37_1"
127.0.0.1:6379> BRPOPLPUSH k37_source k37_distination 10
"v37_2"
127.0.0.1:6379> BRPOPLPUSH k37_source k37_distination 10
"v37_3"
127.0.0.1:6379> BRPOPLPUSH k37_source k37_distination 10
"v37_4"

# 這時我們啓動兩個客戶端,演示阻塞直到另一個客戶端執行source列表中的插入操作。
# 客戶端1中繼續執行 BRPOPLPUSH, 然後馬上在客戶端2中,輸入"LPUSH k37_source v37_5".

# 客戶端1
127.0.0.1:6379> BRPOPLPUSH k37_source k37_distination 10
"v37_5"
(3.02s)
# 客戶端2
127.0.0.1:6379> LPUSH k37_source v37_5
(integer) 1

list內部結構之quicklist

quicklist

我們來看一下list的內部實現 quicklist 結構.

特別註明: quicklist 是鏈表結構。

Redis中使用如下結構體表示.

typedef struct quicklist {
    // 頭結點
    quicklistNode *head;
    // 尾結點
    quicklistNode *tail;
    // 列表的元素個數
    unsigned long count;
    // 鏈表的長度
    unsigned long len;
    // 單個節點的填充因子
    int fill : 16;
    // 不進行節點壓縮的最大深度
    // 超過這個節點就會進行節點壓縮
    unsigned int compress : 16;
} quicklist;

quicklist是回一個通用的雙向鏈接快速列表實現。它的每個節點用 quicklistNode 表示。

一起來看下 qucklistNode 是什麼吧。

quicklistNode

typedef struct quicklistNode {
    // 前一個節點
    struct quicklistNode *prev;
    // 後一個節點
    struct quicklistNode *next;
    // 數據指針。
    // 如果指向的數據沒有被壓縮,那麼會指向zipList結構。
    // 如果進行了壓縮,那麼會指向 quickLZF結構。
    unsigned char *zl;
    // 當前節點的大小
    unsigned int sz;
    // 元素的個數
    unsigned int count : 16;
    // 編碼方式,1=RAW,2=LZF
    // 1 表示未被壓縮
    // 2 表示使用LZF結構進行的壓縮
    unsigned int encoding : 2;   
    // 使用的容器是什麼?1=NONE,2=ZIPLIST
    unsigned int container : 2;
    // 前一個節點是否被壓縮
    unsigned int recompress : 1; 
    // 是否壓縮
    unsigned int attempted_compress : 1; 
    // 暫時留出來,以後使用。
    unsigned int extra : 10;
} quicklistNode;

quicklistNode是一個32byte的結構體,用於描述一個quicklist的一個節點。從代碼中可看出,使用了位圖來節約空間。在上面的代碼中我們提到還提到兩種數據結構 quicklistLZFziplist.

ziplist

ziplist這種結構比較複雜,而且在源碼中也沒有給出明確定義。那 ziplist 這麼神祕的結構到底是什麼樣的呢?

彆着急, 我們先大體熟悉下ziplist這種結構的設計意圖。

ziplist 是一個經過特殊編碼的雙向鏈表,它的設計意圖就是 提高存儲效率, ziplist可以用於存儲字符串或者整數,其中整數是按照真正的二進制進行編碼的。 它能以O(1) 的效率在表的兩端進行poppush操作。

我們都知道,普通的鏈表每項都是一塊獨立的內存空間,各項之間都是通過指針連接起來的。這種方式,會帶來大量的空間碎片,指針引用也會佔用部分空間內存。所以ziplist是將表中每項放在連續的空間內存中(類似數組),ziplist還對值採取了一個可變長度的存儲方式,大的值就用大空間,小的值就用小空間。

ziplist結構的官方定義。

The general layout of the ziplist is as follows:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
<uint32_t zlbytes> is an unsigned integer to hold the number of bytes that the ziplist occupies, including the four bytes of the zlbytes field itself. This value needs to be stored to be able to resize the entire structure without the need to traverse it first.
<uint32_t zltail> is the offset to the last entry in the list. This allows a pop operation on the far side of the list without the need for full traversal.
<uint16_t zllen> is the number of entries. When there are more than 2^16-2 entries, this value is set to 2^16-1 and we need to traverse the entire list to know how many items it holds.
<uint8_t zlend> is a special entry representing the end of the ziplist. Is encoded as a single byte equal to 255. No other normal entry starts with a byte set to the value of 255.

根據上面中解釋我們可以得出以下這種模型:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IJar37ts-1591506025719)(./images/ziplist-01-Ziplist的結構.png)]

如果沒有特殊指定的話, 都是採用小尾數法存儲的。

  • zlbytes: 存儲一個無符號整數,用於存儲ziplist的所用的字節數,(包括zlbytes字段本身的四個字節),當重新分配內容的時候,不需要遍歷整個列表來計算內存大小。

  • zltail: 一個無符號整數,表示ziplist中最後一個元素的偏移字節數,這樣可以方便的找到最後一個元素,從而可以以O(1)的複雜度在尾端進行pop和push。

  • zllen:壓縮列表包含的結點的個數,即entry的個數。
    這裏的zllen是佔用16bit, 也就是說最多存儲 2^16-2 個。但是ziplist超了2^16-2個也是可以表示的。那種情況就是161的時候,只需要從頭遍歷到尾就好了。

  • entry: 真正存放數據的數據項,每個數據項都有自己的內部結構。

  • zlend: ziplist的最後一個字節,值固定等於255,就是一個結束標記。

entry 結構

entry 是由三部分構成的。

  • previous length(pre_entry_length): 表示前一個數據節點佔用的總字節數,這個字段的用處是爲了讓ziplist能夠從後向前遍歷(從後一項的位置,只需向前偏移previous length個字節,就找到了前一項)。這個字段採用變長編碼。

  • encodingencoding&cur_entry_length):表示當前數據節點content的內容類型以及長度。也採用變長編碼。

  • entry-data:表示當前節點存儲的數據,entry-data的內容類型有整數類型和字節數組類型,且某些條件下entry-data的長度可能爲0

所以我們可以得出 ziplist 是一個這樣的結構。

在這裏插入圖片描述

有時,encoding也可以代表entry本身,就像小整數一樣。
在這裏插入圖片描述

這裏就是大體的瞭解下ziplist這種數據結構。

後面我們有一篇專門對ziplist這種數據結構解讀的文章。

quicklistLZF

看完了比較神祕的ziplist 結構,我們來看一個比較簡單的quicklist的壓縮節點的結構 quicklistLZF

/**
 * quicklistLZF是一個4 + N字節的 struct。
 * sz 是 compressed 字段的字節長度。'compressed' 是長度爲 sz的 LZF數據。
 *
 * 未被壓縮的長度保存到 quicklistNode->sz中。
 *
 * 當壓縮了quicklistNode->zl時,quicklistNode->zl指向的是一個 quicklistLZF類型的數據。
 * 未壓縮的時候,指向的是ziplist.
 */
typedef struct quicklistLZF {
    ///compressed數組長度 
    unsigned int sz; 
    char compressed[];
} quicklistLZF;

總結

  • list 相關的命令。以及常見的應用場景.比如棧和隊列等等。
  • list 其實是一種鏈表結構,但是不是一個普通的鏈表結構。
  • list 是由 quicklist 這種數據結構實現的。quicklist 中的每個節點是quicklistNode, 而quicklistzl指針,指向的是 一個ziplist
  • ziplist是一個比較神祕的數據結構,有5部分構成,是連續存儲的,可以實現O(1)的尾端poppush操作。

最後

希望和你成爲朋友!我們一起學習~
最新文章盡在公衆號【方家小白】,期待和你相逢在【方家小白】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章