Redis | 第一部分:資料結構與物件 上篇《Redis設計與實現》

多氯環己烷發表於2021-11-17


前言

參考資料:《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[];
    }
    

SDS邏輯圖

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);

雜湊表邏輯圖8

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]
  • 雜湊表擴充套件與收縮的時機:
    • 負載因子的計算:load_factor = ht[0].used / ht[0].size
    • 擴充套件:伺服器沒有執行BGSAVEBGREWRITEAOF命令,並且負載因子大於等於1;
    • 擴充套件:伺服器正在執行BGSAVEBGREWRITEAOF命令,並且負載因子大於等於5;
      • 避免在執行該命令(子程式存在期間)時進行擴充套件操作,避免不必要的記憶體寫入操作;
    • 收縮:負載因子小於0.1;

rehash

3.6 漸進式rehash

  • 當鍵值對成萬上億時,需要分多次、漸進式完成rehash;
  • 漸進式rehash的步驟:
    • 1)為ht[1]分配空間;
    • 2)將字典的索引計數器變數rehashidx設定為0,表示rehash正式開始;
    • 3)rehash期間,每個對字典操作完成後,將rehashidx++
    • 4)當ht[0]中的所有鍵值對rehash到ht[1]後,rehashidx設定為 -1;
  • 漸進式hash期間:
    • 查詢操作先查ht[0],再查ht[1]
    • 新增操作只在ht[1]新增,保證ht[0]只減不增;

漸進式rehash

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為字典包含的鍵值對數量


最後

新人制作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標註出處!
Redis | 第一部分:資料結構與物件 上篇《Redis設計與實現》

相關文章