目錄
前言
參考資料:《Redis設計與實現 第二版》;
本篇筆記按照書裡的脈絡,將知識點分為四個部分。其中第一部分資料結構與物件分為上中下篇,上篇包括:SDS、連結串列和字典;中篇包括跳躍表、整數集合和壓縮列表;下篇為物件;
1. 簡單動態字串
- Redis構建一種名為簡單動態字串(SDS)的抽象型別,並將SDS作為Redis的預設字串表示;
- 除了用作字串值外,SDS還被用作緩衝區buffer,如:AOF模組中的AOF緩衝區、客戶端狀態中的輸入緩衝區;
1.1 SDS的定義
- SDS的定義在
sds.h/sdshdr
結構:struct sdshdr { //記錄buf陣列中已使用位元組數量 //等於SDS所儲存字串長度 int len; //記錄buf陣列中未使用位元組數量 int free; //位元組陣列,儲存字串 char buf[]; }
1.2 空間預分配與惰性空間釋放
- SDS相比C字串的優點:
- 在常數時間複雜度內獲取字串長度;
- 杜絕緩衝區溢位(空間預分配);
- 減少修改字串時帶來的記憶體重分配次數(空間預分配);
- 可儲存二進位制(使用len值判斷字串是否結束而不是
\0
); - 相容部分C字串函式(在字串末尾保留空字元
\0
)
- 通過未使用空間
free
,SDS實現空間預分配和惰性空間釋放:- 空間預分配:用於SDS字串增長。修改後小於1MB,則 len = free;反之分配1MB額外空間;
- 惰性空間釋放:用於SDS字串縮短。即在有需要時才回收記憶體;
1.3 SDS的API
函式 | 作用 | 時間複雜度 |
---|---|---|
sdsnew | 建立一個包含給定C字串的SDS | O(N),N為給定C字串的長度 |
sdsempty | 建立一個不包含任何內容的空SDS | O(1) |
sdsfree | 釋放給定的SDS | O(N),N為被釋放SDS的長度 |
sdslen | 返回SDS的已使用空間位元組數 | O(1),通過讀取SDS的len屬性獲得 |
sdsavail | 返回SDS的未使用空間位元組數 | O(1),通過讀取free屬性獲得 |
sdsdup | 建立一個給定SDS的副本(copy) | O(N),N為給定SDS的長度 |
sdsclear | 清空SDS儲存的字串內容 | O(1),因為惰性空間釋放策略 |
sdscat | 將給定C字串拼接到SDS字串的末尾 | O(N),N為被拼接字串的長度 |
sdscatsds | 將給定SDS字串拼接到另一個SDS字串的末尾 | O(N),N為被拼接SDS字串的長度 |
sdscpy | 將給定的C字串複製到SDS裡面,覆蓋原有字串 | O(N),N為被複制的C字串長度 |
sdsgrowzero | 用空字串將SDS擴充套件至給定長度 | O(N),N為擴充套件新增的位元組數 |
sdsrange | 保留SDS給定區間內的資料,不在區間內的資料會被覆蓋或清除 | O(N),N為擴充套件新增的位元組數 |
sdstrim | 接受一個SDS和一個C字串作為引數,從SDS中移除所有在C字串出現過的字元 | O(N2),N為給定C字串的長度 |
sdscmp | 對比兩個SDS字串是否相同 | O(N),N為兩個SDS中較短的那個SDS的長度 |
2. 連結串列
- C語言沒有內建連結串列,所以Redis構建自己的連結串列;
- 連結串列在Redis裡的應用:釋出與訂閱、慢查詢、監視器、Redis伺服器儲存多個客戶端、列表鍵底層等、構建客戶端輸出緩衝區(output buffer);
2.1 連結串列與節點的定義
-
連結串列節點的定義與實現在
adlist.h/listNode
結構裡;typedef struct listNode { //前置節點 struct listNode *prev; //後置節點 struct listNode *next; //節點的值 void *value; } listNode;
-
連結串列的定義在
adlist.h/list
中:typedef struct list { //表頭節點 listNode *head; //表尾節點 listNode *tail; //連結串列所包含的節點數量 unsigned long len; //節點值複製函式 void *(*dup)(void *ptr); //節點值釋放函式 void (*free)(void *ptr); //節點值對比函式 int (*match)(void *ptr, void *key); } list;
2.2 連結串列的API
函式 | 作用 | 時間複雜度 |
---|---|---|
listSetDupMethod | 將給定的函式設定為連結串列的節點值複製函式 | O(1),複製函式可以通過連結串列的dup屬性直接獲得 |
listGetDupMethod | 返回連結串列當前正在使用的節點值複製函式 | O(1) |
listSetFreeMethod | 將給定的函式設定為連結串列的節點值釋放函式 | O(1),釋放函式可以通過連結串列的free屬性直接獲得 |
listGetFree | 返回連結串列當前正在使用的節點值釋放函式 | O(1) |
listSetMatchMethod | 將給定的函式設定為連結串列的節點值對比函式 | O(1),對比函式可以通過連結串列的match屬性直接獲得 |
listGetMatchMethod | 返回連結串列當前正在使用的節點值對比函式 | O(1) |
listLength | 返回連結串列的長度 | O(1),連結串列長度可以通過連結串列的len屬性直接獲得 |
listFirst | 返回連結串列的表頭節點 | O(1),表頭節點可以通過連結串列的head屬性直接獲得 |
listLast | 返回連結串列的表尾節點 | O(1),表尾節點可以通過連結串列的tail屬性直接獲得 |
listPrevNode | 返回給定節點的前置節點 | O(1),前置節點可以通過節點的prev屬性直接獲得 |
listNextNode | 返回給定節點的後置節點 | O(1),前置節點可以通過節點的next屬性直接獲得 |
listNodeValue | 返回給定節點的目前正在儲存的值 | O(1),節點值可以通過節點的value屬性直接獲得 |
listCreate | 建立一個不包含任何節點的新連結串列 | O(1) |
listAddNodeHead | 將一個包含給定值的新節點新增到給定連結串列的表頭 | O(1) |
listAddNodeTail | 將一個包含給定值的新節點新增到給定連結串列的表尾 | O(1) |
listInsertNode | 將一個包含給定值的新節點新增到給定節點的之前或之後 | O(1) |
listSearchKey | 查詢並返回連結串列中包含給定值的節點 | O(N),N為連結串列長度 |
listIndex | 返回連結串列在給定索引上的節點 | O(N),N為連結串列長度 |
listDelNode | 從連結串列中刪除給定節點 | O(N),N為連結串列長度 |
listRotate | 將連結串列的表尾節點彈出,然後將被彈出的節點插入到連結串列的表頭,成為新的表頭節點 | O(1) |
listDup | 複製一個給定連結串列的副本 | O(N),N為連結串列長度 |
listRelease | 釋放給定連結串列,以及連結串列中的所有節點 | O(N),N為連結串列長度 |
3. 字典
- 字典,又稱符號表、關聯陣列、對映,用於儲存鍵值對;
- Redis自己構建字典;
- 字典在Redis裡的應用:Redis資料庫底層、雜湊鍵的底層實現等;
- Redis的字典使用雜湊表作為底層實現;
3.1 雜湊表與雜湊節點
-
字典所使用的雜湊表的定義,在
dict.h/dictht
結構中:typedef struct dictht { //雜湊表陣列 dictEntry **table; //雜湊表大小 unsigned long size; //雜湊表大小掩碼,用於計算索引值 //總是等於size-1 unsigned long sizemask; //該雜湊表已有節點的數量 unsigned long used; } dictht;
table
是一個陣列,陣列的每個元素都是指向dict.h/dictEntry
結構的指標;
-
雜湊表節點的定義,在
dict.h/dictEntry
結構;typedef struct dictEntry { //鍵 void *key; //值 union{ void *val; uint64_t u64; int64_t s64; } v; //指向下個雜湊表節點,形成連結串列 struct dictEntry *next; } dictEntry;
- next值的作用:將多個雜湊值相同的鍵值對連線,解決鍵衝突問題(collision);
3.2 字典
-
字典的定義,在
dict.h/dict
結構:typedef struct dict { //型別特定函式 dictType *type; //私有資料 void *privdata; //雜湊表 dictht ht[2]; //rehash 索引 //當 rehash 不在進行時,值為-1 int trehashidx; /* rehashing not in progress if rehashidx == -1 */ } dict;
- type和privdata屬性:針對不同型別鍵值對,為建立多型字典而設定;
- type屬性:是一個指向dictType的指標,Redis為用途不同的字典設定不同的dictType結構體,進而設定不同的型別特定函式;
- privdata屬性:儲存了需要傳給型別特定函式的可選引數;
- ht[2]屬性:每一項是dictht雜湊表,一般字典只用ht[0]雜湊表。對ht[0]進行rehash時使用ht[1];
- trehashidx屬性:記錄當前rehash的進度;
3.3 雜湊演算法
-
Redis計算雜湊值與索引值的方法:
# 使用字典設定雜湊函式,計算key的雜湊值 hash = dict -> type -> hashFunction(key) # 使用雜湊表的sizemask屬性和雜湊值,計算索引值 # 根據情況不同,ht[x]可以是ht[0]或者ht[1] index = hash & dict -> ht[x].sizemask
-
當字典被用作資料庫底層實現,或雜湊鍵底層實現時,Redis使用
MurmurHash2
演算法計算鍵的雜湊值;
3.4 解決鍵衝突
- 鍵衝突:有兩個或以上的鍵被分配到雜湊表陣列的同一個索引;
- Redis使用鏈地址法解決鍵衝突問題;
- 鏈地址法:
dictEntry
雜湊節點裡有個next屬性,可以用其將索引值相同的節點連成連結串列;- 出於速度考慮,將新節點新增到連結串列表頭,O(1);
3.5 rehash
- 通過執行rehash(重新雜湊)來擴充套件和收縮雜湊表;
- rehash的步驟:
- 1)為
ht[1]
分配空間,若擴充套件,則ht[1].size
為第一個大於等於ht[0].used*2
的 2n。若收縮,則ht[1].size
為第一個大於等於ht[0].used
的 2n; - 2)將
ht[0]
中的所有鍵值對rehash到ht[1]
上; - 3)遷移完後,釋放
ht[0]
,將ht[1]
設定為ht[0]
,建立一個空白雜湊表ht[1]
;
- 1)為
- 雜湊表擴充套件與收縮的時機:
- 負載因子的計算:
load_factor = ht[0].used / ht[0].size
; - 擴充套件:伺服器沒有執行
BGSAVE
和BGREWRITEAOF
命令,並且負載因子大於等於1; - 擴充套件:伺服器正在執行
BGSAVE
和BGREWRITEAOF
命令,並且負載因子大於等於5;- 避免在執行該命令(子程式存在期間)時進行擴充套件操作,避免不必要的記憶體寫入操作;
- 收縮:負載因子小於0.1;
- 負載因子的計算:
3.6 漸進式rehash
- 當鍵值對成萬上億時,需要分多次、漸進式完成rehash;
- 漸進式rehash的步驟:
- 1)為
ht[1]
分配空間; - 2)將字典的索引計數器變數
rehashidx
設定為0,表示rehash正式開始; - 3)rehash期間,每個對字典操作完成後,將
rehashidx++
; - 4)當
ht[0]
中的所有鍵值對rehash到ht[1]
後,rehashidx
設定為 -1;
- 1)為
- 漸進式hash期間:
- 查詢操作先查
ht[0]
,再查ht[1]
; - 新增操作只在
ht[1]
新增,保證ht[0]
只減不增;
- 查詢操作先查
3.7 字典的API
函式 | 作用 | 時間複雜度 |
---|---|---|
dictCreate | 建立一個新字典 | O(1) |
dictAdd | 將給定的鍵值對新增到字典裡 | O(1) |
dictReplace | 將給定鍵值對新增到字典裡,如果鍵已存在,則會用新值替換舊值 | O(1) |
dictFetchValue | 返回給定鍵的值 | O(1) |
dictGetRandomKey | 從字典中隨機返回一個鍵值對 | O(1) |
dictDelete | 從字典中刪除給定鍵所對應的鍵值對 | O(1) |
dictRelease | 釋放字典,以及字典包含的鍵值對 | O(N),N為字典包含的鍵值對數量 |