目錄
前言
參考資料:《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為字典包含的鍵值對數量 |
最后
