Redis底層資料結構筆記
阿新 • • 發佈:2022-05-08
1、SDS(簡單動態字串)
-
SDS在資料庫中被用來儲存字串。
-
資料定義及結構
struct sdshdr{ int len; //已使用的位元組長度 int free; //未使用的位元組長度 char buf[]; //儲存的位元組 };
-
特性
SDS 的特性都來源於它的資料結構(是與 C 的字串相比較而獲得的優點) (1)、以常數複雜度獲得長度。保留了 len 變數 (2)、杜絕緩衝區溢位。擴充套件之前先根據 len 值判斷是否會溢位。C 中的字元擴充套件需要手動判斷 (3)、減少修改帶來的記憶體重分配次數。 空間預分配:對SDS擴充套件之後,空間小於 1M ,分配給 free 同樣大小的值。總空間為 len*2+1(加的1是結束 符'\0' 的大小)。修改後空間大於 1M ,分配給free 1M 空間。總空間 len+ 1MB +1。不需要擴充套件就不動。 惰性空間釋放:SDS 縮短時,不會立即回收空間。而是用 free 屬性記錄下來。使用API可以手動釋放。 (4)、二進位制安全:C字串遇到空字元分割的字串會停下來,不在讀取後面的內容。buf陣列不是用來儲存字元的,而是二進位制資料,所有SDS的API都以處理二進位制的方式處理buf 裡面的資料。使用len 判斷字串是否結束而不是結束符。 (5)、相容C的部分字串函式:任然保留了 '\0'結束標誌,雖然自己不用這個判斷字元的結束,但是這使得它可以使用C的部分字串庫函式。
2、連結串列(列表鍵的底層實現)
-
當一個列表鍵包含了比較多的元素,或者元素時比較長的字串時,Redis 使用連結串列作為列表鍵的實現
-
連結串列節點
typedef struct listNode{ struct listNode *prve; //前一個節點 struct listNode *next; //後一個節點 void *value; //節點的值 }listNode;
-
連結串列
typedef struct list{ listNede *head; //頭節點 listNode *tail; //尾節點 unsigned long len; //連結串列長度 void *(*dup) (void *ptr); //節點複製函式 void *(*free) (void *ptr); //節點釋放函式 void *(*match) (void *ptr,void *key); //節點值對比函式 } list;
-
特性
特性來自資料結構 (1)、雙端:每個節點都有前後指標,獲取某個節點的前後置節點複雜度為 O(1) (2)、無環:頭尾指向 NULL,連結串列的訪問以 NULL 結束 (3)、有頭尾指標:找到頭尾節點的複雜度為 O(1) (4)、有長度計數器:已有節點的個數。獲取長度複雜度為 O(1) (5)、多型:使用 void* 儲存節點值,可以存放不同的資料型別
3、字典(雜湊鍵的底層實現)
-
當一個雜湊鍵包含的元素比較多,又或者鍵值對中的元素都是比較長的字串時,會使用字典作為雜湊鍵的底層實現
-
字典中的資料結構
//字典: typedef struct dict{ //型別特定函式,計算 hash 值、複製鍵值、銷燬鍵值、對比鍵值的函式 dictType *type; //私有資料,為針對不同型別的鍵值對,建立多型字典而設計的 void *privdata; //雜湊表陣列 dictht ht[2]; //rehash 索引 int rehashidx; } dict; //雜湊表 typedef struct dictht{ //hash 表陣列 dictEntry **table; //hash表的大小 unsigned long size; //hash掩碼 size-1 unsigned long sizemask; //已用節點數量 unsigned long used; }dictht; //雜湊表節點 typedef struct dictEntry{ //鍵 void *key; //值 union{ void *val; uint64_tu64; int64_tu64; }; //下一個指標,用於 hash衝突 struct dictEntry *next; }
-
雜湊演算法
//比如要儲存鍵值對<k0,v0> int hash = dict->type->hashFunction(k0); //注意是 & 不是 % ,不明白 int index = hash & ht[0].sizemask; //接著就把該鍵值對存放在 ht[0] 的table的dictEntry的index位置
-
鍵值衝突解決
使用單向連結串列,但是要注意的是,插入時不是插入在後面而是在最前面,主要是為了提升插入時的效率。沒有了遍歷連結串列找到尾節點再插入的時間浪費
-
雜湊表的擴充套件和收縮
負載因子:used/size (1)、當伺服器在執行 BGSAVE 或者 BGREWRITEAOF 命令時,如果負載因子大於等於 1,就開始 rehash (2)、如果在執行上面兩條命令,如果負載因子大於等於 5,才開始 rehash,提高閾值是為了儘量避免在子程序執行任務時進行hash,避免不必要的記憶體寫入。 (3)、當負載因子小於 0.1 時,開始收縮
-
漸進式 rehash
步驟: 1)、為 ht[1]分配空間 2)、將字典中維持的 rehashidx 值置為0,標識開始 rehash 3)、每次對字典執行 CRUD 操作時,程式除了完成指定的 CRUD 操作外還會順帶將 ht[0] 上的鍵值對 rehash 到ht[1],每 rehash 一個鍵值對,rehashidx++ 4)、完全 rehash 之後,重新將 rehashidx 置為 -1。釋放 ht[0],將ht[1]設定為 ht[0],重新建立一個ht[1],並建立一個空的 table CRUD 操作時順帶 rehash 時注意點 1)、增加新的鍵值對只會往 ht[1] 上增加 2)、刪除和修改會同時在兩張表上進行 3)、查詢時會現在 ht[0] 查詢,沒找到再去 ht[1] 找。 一個小疑點:我本來覺得時邊操作順帶 rehash ,就是找到了指定鍵值對,操作完之後,直接把這個也 rehash 到ht[1],但是刪除和修改是同時在兩張表執行的,就是說,這個資料會在兩張表同時存在(嗎?)。問題是:他是操作誰就把誰直接 rehash 還是按一定的順序來 rehash 的。希望大佬解決 PS:書上的例子時順序 rehash 的,但是他這個例子並沒有表明時在 CRUD 時順待執行的
4、跳躍表(有序集合的實現之一)
-
如果一個有序集合包含的元素比較多或者元素的成員是比較長的字串時,Redis 會使用跳躍表作為集合鍵的底層實現
-
跳躍表的資料結構
- 跳躍表
//跳躍表 typedef struct zskiplist{ //頭尾節點 structz skipListNode *header , *tail; //節點的數量 unsigned long length; //表中層數自大的節點的層數 int level; } zskiplist;
-
跳躍表屬性解析
(1)、頭尾節點:直達首尾節點 (2)、level:節點中層數最大的節點的層數,頭節點不計入 (3)、length: 節點的數量,頭節點不算
-
跳躍表節點
typedef struct zskiplistNode{ //層 struct zskiplistlevel{ //前進指標 struct zskiplistNode *forward; //跨度 unsigned int span; } level[]; //後退節點 struct zskiplistNode *backward; //分值 double score; //成員物件,是一個 SDS robj *obj; } zskiplistNode;
-
節點屬性介紹
(1)、level[] :節點的很多層 ,節點的層數是在建立的時候根據冪次定律算出來的(1~32之間的整數); forward指標:指向下一個節點的同層(不確定,是猜測);span:跨度,就是用來計算排位,將從根節點到該節點所經的全部跨度加起來就是該節點的排位。跳躍表是一個有序的資料結構。 (2)、backword 指標:指向前面一個數據,可以用於逆序遍歷。 (3)、score:節點按分值大小進行排序,由小到大。 (4)、obj: 儲存的物件,只能是 SDS,前面的連結串列啥都能儲存
-
一點注意:
1、節點的分值可以相同,但是成員物件 SDS 必須是唯一的。 2、分值相同時,按照 sds 的字典序進行排序 3、有序的資料結構,增刪改查平均時間複雜度 O(log N), PS:這裡沒講,我覺得是和 span 有關的,要想 logN,就得能直接找到中間下標,跳躍表裡面儲存了 length,可以計算出 length/2,然後應該是根據 span 來跳著找 length/2,肯定不是用 forward 來遍歷了,但是利用 span 應該也不能保證就能一次找到 len/2 的位置吧。。。。(存疑,等待大佬解決----) 4、跳躍表是有序集合的實現之一
5、整數集合(集合鍵的實現之一)
-
當一個集合只包含整數值元素,並且集合的元素數量不多時,Redis 使用整數集合作為集合鍵的實現。
-
資料結構
typedef struct intset{ //編碼方式 uint32_t encoding; //集合元素數量 uint32_t length; //儲存的元素陣列,有序的,從小到大排列 int8_t contents[]; }
-
特性
1、雖然 contents[] 被宣告為 int8_t ,但是實際的編碼型別是由 encoding 屬性來決定的。 2、當向一個 int16_t 的陣列新增 int32_t 的元素時,陣列會進行升級,所有元素都會變成 int32_t 型別。
-
升級步驟
1、根據新元素的型別,擴充套件資料底層空間大小,併為新元素分配空間 2、將原來的元素轉換為新元素的型別,並按原來的順序放到對應的位置,保持有序性不變 3、將新元素新增到數組裡面 4、更新 encoding 值和 length 值 *****要注意的是,他是從後面開始插入的,最後在插入新元素的值,另外,資料長度減小時,不會降級*****
-
一點注意
1、整數集合是集合鍵的實現之一 2、整數集合 有序,不重複地儲存資料,會進行動態升級,不會降級
6、壓縮列表(列表鍵和雜湊鍵的實現之一)
-
當一個列表鍵只包含列表項,並且每個列表選項要麼是小整數值,要麼是長度比較短的字串時。Redis 會使用壓縮列表來實現列表鍵。
-
壓縮列表
屬性 | 型別 | 長度 | 說明 |
---|---|---|---|
zlbytes | uint32_t | 4B | 整個壓縮列表佔用的記憶體位元組數(對列表進行記憶體重分配或者極計算 zlend 時會用到) |
zltail | uint32_t | 4B | 表尾節點(是節點不是 zlend 的位置)距離起始地址的位元組數 |
zllen | uint16_t | 2B | 壓縮列表的節點數量 |
entry | 列表節點(位元組陣列或者整數) | 不定 | 壓縮列表的節點 |
zlend | uint8_t | 1B | 0xFF , 用來標記壓縮列表末端 |
根據例子,zltail
是最後一個節點的起始地址的偏移值。zlbytes-zltail-1
就是最後一個節點的長度(zlend
佔一個位元組)
- 壓縮列表的節點
- 各個欄位及用途
1、previous_entry_length:記錄前面一個節點所佔用的位元組數,這個可以用於後序遍歷。上面說了可以根據zltail 計算出最後一個節點的起始位置,減去這個值就正好是前面一個結點的起始位置,依次,可以進行後序遍歷。這個屬性可以是1或5位元組長度,如果前一個entry長度小於254位元組,就使1位元組,否則5位元組。
2、encoding:記錄 content 屬性儲存的資料的型別以及長度。可以是 1、2、5 位元組長,具體規則比較繁瑣。
3、儲存節點的值,可以是一個整數或者位元組陣列。
- 連鎖更新
如果連續的多個節點(e1,e2,e3...),他們的長度都在 250~253 之間,現在在這些節點之前新增一個長度大於 254 的節點,那麼 e1 的 previous_entry_length 屬性就不得不變為 5 個位元組,那麼 e1 的長度就也會超過 254 位元組,e2 的 previous_entry_length 屬性也會變為 5 位元組,...以此類推,引起連鎖更新 。同樣,刪除節點也可能會引發連鎖更新。
實際上這種情況比較少見,很少連續很多節點的長度在這個區間之內,對少量節點的更新也不會浪費太長時間,這種資料結構可以節省記憶體