Redis資料結構:List型別全面解析

BJRA發表於2024-11-03

文章目錄

一、List資料型別

  • 1.1 簡介
  • 1.2 應用場景
  • 1.3 底層結構

二、資料結構

  • 2.1 壓縮列表ZipList
  • 2.2 雙向連結串列LinkedList(後續已廢棄)
  • 2.3 快速連結串列QuickList

三、List常見命令

一、List資料型別

1.1 簡介

詳細介紹:Redis五種資料型別、String、List、Set、Hash、ZSet

Redis中的List型別與Java中的LinkedList類似,可以看做是一個雙向連結串列結構,按插入順序排序,你可以新增一個元素到頭部(左邊)或尾部(右邊)。在 Redis 中,列表最多可以包含 2^32 - 1 個元素。既可以支援正向檢索和也可以支援反向檢索。特徵也與LinkedList類似:

  • 有序
  • 元素可重複
  • 插入和刪除快
  • 查詢速度一般。

1.2 應用場景

根據 Redis 雙向列表的特性,因此其也被用於非同步佇列的使用。實際開發中將需要延後處理的任務結構體序列化成字串,放入 Redis 的佇列中,另一個執行緒從這個列表中獲取資料進行後續處理。

  • 訊息佇列:可以利用 List 的 push 和 pop 操作,實現生產者消費者模型。
  • 時間線、動態訊息:比如微博的時間線,可以將最新的內容放在 List 的最前面。
  • 常用來儲存一個有序資料,例如:朋友圈點贊列表,評論列表等

1.3 底層結構

  • 在3.2版本之前,Redis List底層採用壓縮連結串列ZipList雙向連結串列LinkedList來實現List。當元素數量小於512並且元素大小小於64位元組時採用ZipList編碼,超過則將自動採用LinkedList編碼
  • 在3.2版本之後,Redis統一採用快速連結串列QuickList來實現List

二、資料結構

Redis的List結構類似一個雙端連結串列,可以從首、尾操作列表中的元素:

  • 在Redis 3.2版本之前,Redis List底層採用壓縮連結串列ZipList雙向連結串列LinkedList來實現List。當元素數量小於512個並且元素大小小於64位元組時採用ZipList編碼,超過則將自動採用LinkedList編碼。
  • 在3.2版本之後,Redis統一採用快速連結串列QuickList結構來實現List

QuickList結構如下:

在 Redis3.2 版本前,Redis 列表 List 使用兩種資料結構作為底層實現:

  • 壓縮列表 ZipList:插入元素過多或字串太大,就需要呼叫 Realloc 擴充套件記憶體;
  • 雙向連結串列 LinkedList:需附加指標 Prev 和 Next,較浪費空間,加重記憶體的碎片化

2.1 壓縮列表ZipList

在 Redis3.2 版本前 壓縮列表不僅是 List 的底層實現之一,同時也是 Hash、 ZSet 兩種資料型別底層實現之一。

ZipList是一種特殊的“雙端連結串列”(並非連結串列),由一系列特殊編碼的連續記憶體塊組成,像記憶體連續的陣列。可以在任意一端進行壓入/彈出操作,並且該操作的時間複雜度為O(1)。

壓縮列表 底層資料結構:本質是一個陣列,增加了列表長度、尾部偏移量、列表元素個數、以及列表結束標識,有利於快速尋找列表的首尾節點;但對於其他正常的元素,如元素2、元素3,只能一個個遍歷,效率仍沒有很高效。

當我們的 List 列表資料量比較少的時候,且儲存的資料輕量的(如小整數值、短字串)時候, Redis 就會透過壓縮列表來進行底層實現。

屬性 型別 長度 說明
zlbytes uint32_t 4位元組 一個 4 位元組的整數,表示整個壓縮列表佔用的位元組數量,包括 <zlbytes> 自身的大小
zltail uint32_t 4位元組 一個 4 位元組的整數,記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少位元組,透過這個偏移量,可以確定表尾節點的地址
zllen uint16_t 2位元組 一個 2 位元組的整數,表示壓縮列表中的節點數量。最大值為UINT16_MAX(65534),如果超過這個數,此處會記錄為65535,但節點的真實數量需要遍歷整個壓縮列表才能計算出
entry 列表節點 不定 壓縮列表中的元素,每個元素都由一個或多個位元組組成,節點的長度由節點儲存的內容決定。每個元素的第一個位元組(又稱為"entry header")用於表示這個元素的長度以及編碼方式
zlend uint8_t 1位元組 一個位元組,特殊值0xFF(十進位制255),表示壓縮列表的結束

注意:

  • 如果查詢定位首個元素或最後1個元素,可以透過表頭 “zlbytes”、“zltail_offset” 元素快速獲取,複雜度是 O(1)。但是查詢其他元素時,就沒有這麼高效了,只能逐個查詢下去,比如 entryN 的複雜度就是 O(N)

  • ZipList雖然節省記憶體,但申請記憶體必須是連續空間,如果記憶體佔用較多,申請效率較低。

2.2 雙向連結串列LinkedList(後續已廢棄)

LinkedList 是標準的雙向連結串列,Node 節點包含 prev 和 next 指標,分別指向後繼與前驅節點,因此從雙向連結串列中的任意一個節點開始都可以很方便地訪問其前驅與後繼節點。

LinkedList 可以進行雙向遍歷;新增刪除元素快 O(1),查詢元素慢 O(n),高效實現了 LPUSH 、RPOP、RPOPLPUSH,但由於需要為每個節點分配額外的記憶體空間,所以會浪費一定的記憶體空間。這種編碼方式適用於元素數量較多或者元素較大的場景。

LinkedList 結構為連結串列提供了表頭指標 head、表尾指標 tail,以及節點數量計算 len。下圖展示一個由 list 結構和三個 listNode 節點組成的連結串列:

Redis 的連結串列實現的特性可以總結如下:

  • 雙端:連結串列節點帶有 prev 和 next 指標,獲取某個節點的前一節點和後一節點的複雜度都是 O(1);
  • 無環:表頭節點的 prev 指標和表尾節點的 next 指標都指向 NULL,對連結串列的訪問以 NULL 為終點;
  • 表頭指標/表尾指標:透過 list 結構的 head 指標和 tail 指標,獲取連結串列的表頭節點和表尾節點的複雜度為 O(1);
  • 連結串列長度計數器:透過 list 結構的 len 屬性來對 list 的連結串列節點進行計數,獲取節點數量的複雜度為O(1);
  • 多型:連結串列節點使用 void* 指標來儲存節點值,並透過 list 結構的 dup、free、match 三個屬性為節點值設定型別特定函式,所以連結串列可以用於儲存各種不同型別的值。
  • 使用連結串列的附加空間相對太高,因為 64bit 系統中指標是 8 個位元組,所以 prev 和 next 指標需要佔據 16 個位元組,且連結串列節點在記憶體中單獨分配,會加劇記憶體的碎片化,影響記憶體管理效率

2.3 快速連結串列QuickList

QuickList底層 LinkedList + ZipList,可以從雙端訪問,記憶體佔用較低,保含多個ZipList,儲存上限高。其特點:

  • 是一個節點為ZipList的雙端連結串列
  • 節點採用ZipList,解決了傳統連結串列的記憶體佔用問題
  • 控制了ZipList大小,解決連續記憶體空間申請效率問題
  • 中間節點可以壓縮,進一步節省了記憶體

ZipList雖然節省記憶體,但申請記憶體必須是連續空間,如果記憶體佔用較多,申請效率較低。

三、List常見命令

List的常見命令有

  • LPUSH key value [value ...] :向列表左側插入一個或多個元素
  • LPOP key :移除並返回列表左側的第一個元素,沒有則返回nil
  • RPUSH key value [value ...] :向列表右側插入一個或多個元素
  • RPOP key :移除並返回列表右側的第一個元素
  • LRANGE key start stop:返回一段角標範圍內的所有元素
  • BLPOP和BRPOP:與LPOP和RPOP類似,只不過在沒有元素時等待指定時間,而不是直接返回nil
  • lindex key index:透過下標獲得list當中的某一個值
  • llen key:獲取list的長度

  • 如何利用List結構模擬一個棧?

    • 入口和出口在同一邊
  • 如何利用List結構模擬一個佇列?

    • 入口和出口在不同邊
  • 如何利用List結構模擬一個阻塞佇列?

    • 入口和出口在不同邊
    • 出隊時採用BLPOP或BRPOP
127.0.0.1:6379> lpush list1 18 20 Jenny       #向列表左側插入一個或多個元素
(integer) 3
127.0.0.1:6379> lpush list1 Happy
(integer) 4
127.0.0.1:6379> lrange list1 0 2              #返回一段角標範圍內的所有元素
1) "Happy"
2) "Jenny"
3) "20"
127.0.0.1:6379> rpush list1 Jack Health 16    #向列表右側插入一個或多個元素
(integer) 7
127.0.0.1:6379> lrange list1 0 -1             #透過區間獲取具體的值
1) "Happy"
2) "Jenny"
3) "20"
4) "18"
5) "Jack"
6) "Health"
7) "16"
127.0.0.1:6379> lpop list1                   #移除list的第一個元素:Happy
"Happy"
127.0.0.1:6379> rpop list1                   #移除list的最後一個元素:16
"16"
127.0.0.1:6379> lindex list1 2               #透過下標獲得list當中的某一個值
"18"
127.0.0.1:6379> llen list1                   #獲取list的長度
(integer) 5
127.0.0.1:6379> rpush list1 18 Jack
(integer) 7
127.0.0.1:6379> lrange list1 0 -1
1) "Jenny"
2) "20"
3) "18"
4) "Jack"
5) "Health"
6) "18"
7) "Jack"
127.0.0.1:6379> lrem list1 2 Jack           #移除list集合指定個數的value,移除2個值為Jack的,精確匹配
(integer) 2
127.0.0.1:6379> lrange list1 0 -1
1) "Jenny"
2) "20"
3) "18"
4) "Health"
5) "18"
127.0.0.1:6379> ltrim list1 3 4            #擷取list集合中下標為3到下標為4之間的元素集合,並覆蓋原來的list集合
OK
127.0.0.1:6379> lrange list1 0 -1
1) "Health"
2) "18"
127.0.0.1:6379> lset list 1 17
(error) ERR no such key
127.0.0.1:6379> lset list1 1 17           #更新list集合當中下標為1的值為17,如果下標1的值不存在,則報錯
OK
127.0.0.1:6379> linsert list1 AFTER Health 20    #將某個具體的值插入到某一個具體元素(預設第一個)的前面或者後面
(integer) 3
127.0.0.1:6379> linsert list1 BEFORE Health Jenny
(integer) 4
127.0.0.1:6379> lrange list1 0 -1
1) "Jenny"
2) "Health"
3) "20"
4) "17"

參考 Redis基礎(超詳解)一 :Redis定義、SQL與NoSQL區別、Redis常用命令、Redis五種資料型別、String、List、Set、Hash、ZSet;Redis的Java客戶端Redis資料結構:List型別全面解析

相關文章