Redis | 第1章 SDS、鏈表與字典《Redis設計與實現》



前言

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

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. 鏈表

  • 鏈表 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);

哈希表邏輯圖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為字典包含的鍵值對數量


最后

新人制作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標注出處!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM