1. 程式人生 > 資料庫 >Redis之資料結構

Redis之資料結構

資料結構分類

資料結構按鍵和值分類,可以分為兩類,鍵值資料結構和值的資料結構。鍵值資料結構是hash表,使用者通過鍵在hash表中找到值;值的資料結構又可以分為兩類,API層面資料結構和底層資料結構。API層面的資料結構包括String、List、Set,Sorted Set,Hash,Bitmap;底層資料結構包括SDS(簡單動態字串)、dict(雜湊表)、ziplist(壓縮列表)、quicklist(雙向連結串列)、skiplist(跳錶),intset(整數陣列)。值的底層資料結構是這篇文章介紹的重點。

值的底層資料結構

簡單動態字串

Redis 沒有直接使用 C 語言傳統的字串表示(以空字元結尾的字元陣列,以下簡稱 C 字串), 而是自己構建了一種名為 **簡單動態字串(simple dynamic string,SDS)**的抽象型別, 並將 SDS 用作 Redis 的預設字串表示。(和Golang相同,我相信也是大多數語言的作法)

C字串SDS
獲取字串長度的複雜度為 O(N)獲取字串長度的複雜度為 O(1)
API 是不安全的,可能會造成緩衝區溢位API 是安全的,不會造成緩衝區溢位
修改字串長度 N 次必然需要執行 N 次記憶體重分配修改字串長度 N 次最多需要執行 N 次記憶體重分配
只能儲存文字資料可以儲存文字或者二進位制資料
可以使用所有 <string.h> 庫中的函式可以使用一部分 <string.h> 庫中的函式

與 C 字串不同, SDS 的空間分配策略完全杜絕了發生緩衝區溢位的可能性: 當 SDS API 需要對 SDS 進行修改時, API 會先檢查 SDS 的空間是否滿足修改所需的要求, 如果不滿足的話, API 會自動將 SDS 的空間擴充套件至執行修改所需的大小, 然後才執行實際的修改操作, 所以使用 SDS 既不需要手動修改 SDS 的空間大小, 也不會出現前面所說的緩衝區溢位問題

雜湊表

普普通通的使用連結串列法的散列表,唯一需要注意的是漸進式rehash(這個和golang的symc.Map相同)。我們著重介紹一下漸進式rehash

漸進式rehash

dict的資料結構裡包含 兩個雜湊表。在重雜湊期間,資料從一個雜湊表向另一個雜湊表遷移。Redis 仍然正常處理客戶端請求,每處理一個請求時,從雜湊表 1 中的第一個索引位置開始,順帶著將這個索引位置上的所有 entries 拷貝到雜湊表 2 中;等處理下一個請求時,再順帶拷貝雜湊表 1 中的下一個索引位置的 entries。如下圖所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-QqY9mkes-1609427803384)(./assets/1.jpg)]

壓縮列表

ziplist 是一個經過特殊編碼的 雙向連結串列,它的設計目標就是為了提高儲存效率。 ziplist 可以用於儲存字串或整數,其中整數是按真正的二進位制表示進行編碼的,而不是編碼成字串序列。它能以 O(1)O(1) 的時間複雜度在表的兩端提供 push 和 pop 操作。

一個普通的雙向連結串列,連結串列中每一項都佔用獨立的一塊記憶體,各項之間用地址指標(或引用)連線起來。這種方式會帶來大量的記憶體碎片,而且地址指標也會佔用額外的記憶體。而 ziplist 卻是將表中每一項存放在前後 連續的地址空間 內,一個 ziplist 整體佔用一大塊記憶體。它是一個表(list),但其實不是一個連結串列(linked list)。

另外,ziplist 為了在細節上節省記憶體,對於值的儲存採用了 變長編碼方式,大概意思是說,對於大的整數,就多用一些位元組來儲存,而對於小的整數,就少用一些位元組來儲存。ziplist 的底層結構如下所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-y8hHLxho-1609427803397)(./assets/2.png)]

  • zlbytes: 32bit,表示 ziplist 佔用的位元組總數。
  • zltail: 32bit,表示 ziplist 表中最後一項(entry)在 ziplist 中的偏移位元組數。
  • zllen: 16bit, 表示 ziplist 中資料項(entry)的個數。 zllen 可以表達的最大值為 2^16-1。當 ziplist 長度超過 2^16−1 時, zllen 不表示長度,長度需要進行遍歷計算。
  • entry: 表示真正存放資料的資料項,長度不定。一個數據項(entry)也有它自己的內部結構。
  • zlend: ziplist 最後1個位元組,是一個結束標記,值固定等於 255。

quicklist(雙向連結串列)

quicklist 是由 ziplist 為節點組成的雙向連結串列。 ziplist 本身也是一個能維持資料項先後順序的列表(按插入位置),而且是一個記憶體緊縮的列表(各個資料項在記憶體上前後相鄰)。比如,一個包含 3 個節點的 quicklist ,如果每個節點的 ziplist 又包含 4 個數據項,那麼對外表現上,這個 list 就總共包含 12 個數據項。
quicklist的設計是在空間和時間取了折中:

  • 雙向連結串列便於在表的兩端進行 push 和 pop 操作,但是它的記憶體開銷比較大。首先,它在每個節點上除了要儲存資料之外,還要額外儲存兩個指標;其次,雙向連結串列的各個節點是單獨的記憶體塊,地址不連續,節點多了容易產生記憶體碎片。
  • ziplist 由於是一整塊連續記憶體,所以儲存效率很高。但是 不利於修改操作,每次資料變動都會引發一次記憶體的 realloc (擴容)。特別是當 ziplist 長度很長的時候,一次 realloc 可能會導致大批量的資料拷貝,進一步降低效能。

skiplist(跳錶)

跳躍表(skiplist) 是一種有序資料結構,它通過在每個節點中維持多個指向其他節點的指標,從而達到快速訪問節點的目的。

intset(整數陣列)

intset 是一個由整陣列成的 有序集合,從而便於在上面進行二分查詢,用於快速地判斷一個元素是否屬於這個集合。它在記憶體分配上與 ziplist 有些類似,是連續的一整塊記憶體空間,而且對於大整數和小整數(按絕對值)採取了不同的編碼,儘量對記憶體的使用進行了優化。

對於小集合使用 intset 來儲存,主要的原因是節省記憶體。特別是當儲存的元素個數較少的時候, dict 所帶來的記憶體開銷要大得多(包含兩個雜湊表、連結串列指標以及大量的其它元資料)。所以,當儲存大量的小集合而且集合元素都是數字的時候,用 intset 能節省下一筆可觀的記憶體空間。

API結構和底層結構對應關係

API資料結構限制底層資料結構(量小)底層資料結構(量大)
string512 MBSDSSDS
list最大長度 2^32-1quicklistquicklist
set最大長度 2^32-1intsetdict
zset最大長度 2^32-1ziplistdict+skiplist
hash最大長度 2^32-1ziplistdict
bitmap512 MBSDSSDS