目錄
前言
參考資料:《Redis設計與實現 第二版》;
本篇筆記按照書里的脈絡,將知識點分為四個部分。其中第一部分數據結構與對象分為上中下篇,上篇包括:SDS、鏈表和字典;中篇包括跳躍表、整數集合和壓縮列表;下篇為對象;
中篇的鏈接:https://www.cnblogs.com/dlhjw/p/15582039.html
下篇的鏈接:https://www.cnblogs.com/dlhjw/p/15594048.html
與本章相關的 Redis 命令總結在下篇文章,歡迎點擊收藏,本篇將不再重復:
《Redis常用命令及示例總結(API)》:https://www.cnblogs.com/dlhjw/p/15639773.html
1. 簡單動態字符串
- 簡單動態字符串(simple dynamic string)SDS;
- Redis構建一種名為簡單動態字符串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. 鏈表
- 鏈表 list;
- 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為字典包含的鍵值對數量 |
最后
