《Redis設計與實現》[第一部分]資料結構與對象-C源碼閱讀(一)

來源:互聯網
上載者:User

標籤:

一、簡單動態字串SDS

關鍵字:空間預分配,惰性空間釋放,二進位安全

C字串不易更改,所以Redis中把C字串用在一些無須對字串值進行修改的地方,作為字串字面量(String literal),比如列印日誌:
redisLog(REDIS_WARING, “Redis is now ready to exit, bye bye…”);

在Redis資料庫中,包含字串的索引值對在底層都是由SDS實現的。

SDS還被用作緩衝區(buffer):AOF模組中的AOF緩衝區,以及用戶端狀態中的輸入緩衝區,都是SDS實現的。

源碼

SDS結構的定義在sds.h中:

    /*     * 儲存字串對象的結構     */    struct sdshdr {             // buf 中已佔用空間的長度        int len;        // buf 中剩餘可用空間的長度,即未使用空間        int free;        // 資料空間        char buf[];    };

擷取一個SDS長度的複雜度為O(1),由SDS的API在執行時自動化佈建和更新SDS長度,使用SDS無須進行任何手動修改長度的工作。

空間分配

SDS的空間分配策略是:當SDS API需要對SDS進行修改時,API會先檢查SDS的空間是否滿足修改所需的要求,若不滿足,API會自動將SDS的空間擴充至執行修改所需的大小,然後才執行實際的修改操作,杜絕了發生緩衝區溢位的可能性。

通過未使用空間,SDS實現了空間預分配和惰性空間釋放兩種最佳化策略:

  • 空間預分配

空間預分配用於減少連續執行字串增長操作所需的記憶體配置次數。
通過這種預分配策略,SDS將連續增長N次字串所需的記憶體重分配次數從必定N次降低為最多N次。
其中額外分配的未使用空間數量由以下公式決定:

1. 如果對SDS進行修改後,SDS的長度(即len屬性的值)小於1MB,就分配和len屬性同樣大小的未使用空間,即len屬性的值和free屬性的值相同   2. 如果對SDS進行修改之後,SDS的長度大於等於1MB,就分配1MB的未使用空間。  
  • 惰性空間釋放

惰性空間釋放用於最佳化SDS字串縮短操作的記憶體重分配操作:當SDS的API需要縮短SDS儲存的字串時,程式並不立即使用記憶體重分配來回收縮短後多出來的位元組,而是使用free屬性將這些位元組的數量記錄起來,並等待將來使用。

SDS的API都是二進位安全的(binary-safe),所有SDS API都會以二進位的方式處理SDS存放在buf數組裡的資料,程式不會對其中的資料做任何限制、過濾、或者假設,資料在寫入時是什麼樣的,被讀取時就是什麼樣。

Redis用SDS的buf數組儲存位元據而不是字元。

SDS可以相容部分C字串函數。

二、鏈表

關鍵字:多態

當一個列表鍵包含了數量比較多的元素,或是列表中包含的元素都是比較長的字串時,Redis就會使用鏈表作為列表鍵的底層實現。

integers列表鍵的底層實現就是一個鏈表,鏈表中的每個結點都儲存了一個整數值。

除了鏈表之外,發布與訂閱、慢查詢、監視器等功能也用到了鏈表,Redis伺服器本身還使用鏈表儲存多個用戶端的狀態資訊,以及使用鏈表來構建用戶端輸出緩衝區(output buffer)。

源碼

鏈表結構的定義在adlist.h中:

    /*     - 雙端鏈表節點     */    typedef struct listNode {        // 前置節點        struct listNode *prev;        // 後置節點        struct listNode *next;        // 節點的值        void *value;    } listNode;    /*     *雙端鏈表迭代器     */    typedef struct listIter {        // 當前迭代到的節點        listNode *next;        // 迭代的方向        int direction;    } listIter;    /*     - 雙端鏈表結構     */    typedef struct list {        // 表前端節點        listNode *head;        // 表尾節點        listNode *tail;        // 節點值複製函數        void *(*dup)(void *ptr);        // 節點值釋放函數        void (*free)(void *ptr);        // 節點值對比函數        int (*match)(void *ptr, void *key);        // 鏈表所包含的節點數量        unsigned long len;    } list;  

list結構為鏈表提供了表頭指標head、表尾指標tail,以及鏈表長度計數器len,dup、free和match成員則是用於實現多態鏈表所需的類型特定函數:

  • dup函數用於複製鏈表結點所儲存的值
  • free函數用於釋放鏈表結點所儲存的值;
  • match函數則用於對比鏈表結點所儲存的值和另一個輸入值是否相等。

Redis的鏈表實現的特性如下:

  • 雙端、無環、帶表頭指標和表尾指標、帶鏈表長度計數器、多態
三、字典

關鍵字:多態,漸進式rehash,murmurhash2

Redis的資料庫就是使用字典來作為底層實現的,對資料庫的增、刪、改、查也是構建在對字典的操作之上的。

字典還是雜湊鍵的底層實現之一,當一個雜湊鍵包含的索引值對比較多,或是索引值對中的元素都是比較長的字串時,Redis就使用字典作為雜湊鍵的底層實現。

Redis的字典使用雜湊表作為底層實現,一個雜湊表裡可以有多個雜湊表結點,每個雜湊表結點就儲存了字典中的一個索引值對。

源碼

字典所使用的雜湊表在dict.h中定義:

    /*     * 雜湊表     * 每個字典都使用兩個雜湊表,從而實現漸進式 rehash 。     */    typedef struct dictht {            // 雜湊表數組,數組中的每個元素都是一個指向dictEntry結構的指標        dictEntry **table;        // 雜湊表大小        unsigned long size;              // 雜湊表大小掩碼,用於計算索引值        // 總是等於 size - 1        unsigned long sizemask;        // 該雜湊表已有節點的數量        unsigned long used;    } dictht;
  • table屬性是一個數組,數組中的每個元素都是一個指向dictEntry結構的指標,每個dictEntry結構儲存著一個索引值對。
  • size屬性記錄了雜湊表的大小,即是table數組的大小。
  • used屬性則記錄了雜湊表目前已有結點(索引值對的數量)
  • sizemask屬性和雜湊值一起決定一個鍵應該被放到table數組的哪個索引上面
    /*     * 雜湊表節點     */    typedef struct dictEntry {              // 鍵        void *key;        // 值        union {            void *val;            uint64_t u64;            int64_t s64;        } v;        // 指向下個雜湊表節點,形成鏈表        struct dictEntry *next;    } dictEntry;
  • key屬性儲存著索引值對中的鍵
  • v屬性儲存索引值對中的值,其中索引值對中的值可以是一個指標,或是一個uint64_t整數,或是一個int64_t整數
  • next屬性指向另一個雜湊表結點的指標,使用鏈地址法解決鍵衝突問題。
    /*     * 字典     */    typedef struct dict {        // 類型特定函數        dictType *type;        // 私人資料        void *privdata;        // 雜湊表        dictht ht[2];        // rehash 索引        // 當 rehash 不在進行時,值為 -1        int rehashidx; /* rehashing not in progress if rehashidx == -1 */        // 目前正在啟動並執行安全迭代器的數量        int iterators; /* number of iterators currently running */    } dict;

type屬性和privdata屬性是針對不同類型的索引值對,為建立多態字典而設定的:

  • type屬性是一個指向dictType結構的指標,每個dictType結構儲存了一簇用於操作特定類型索引值對的函數,Redis會為用途不同的字典設定不同的類型特定函數。
  • privdata屬性儲存了需要傳給那些類型特定函數的選擇性參數。
/* * 字典類型特定函數 */typedef struct dictType {    // 計算雜湊值的函數    unsigned int (*hashFunction)(const void *key);    // 複製鍵的函數    void *(*keyDup)(void *privdata, const void *key);    // 複製值的函數    void *(*valDup)(void *privdata, const void *obj);    // 對比鍵的函數    int (*keyCompare)(void *privdata, const void *key1, const void *key2);    // 銷毀鍵的函數    void (*keyDestructor)(void *privdata, void *key);    // 銷毀值的函數    void (*valDestructor)(void *privdata, void *obj);} dictType;
  • ht屬性是一個包含兩個項的數組,數組中的每個項都是一個dictht雜湊表,一般,字典只使用ht[0]雜湊表,ht[1]雜湊表只會在對ht[0]雜湊表進行rehash時使用。
  • rehashidx屬性記錄了rehash目前的進度,如果目前沒有在進行rehash,那麼它的值為-1.
    /*     * 字典迭代器     *     - 如果 safe 屬性的值為 1 ,那麼在迭代進行的過程中,     - 程式仍然可以執行 dictAdd 、 dictFind 和其他函數,對字典進行修改。     *     - 如果 safe 不為 1 ,那麼程式只會調用 dictNext 對字典進行迭代,     - 而不對字典進行修改。     */    typedef struct dictIterator {                    // 被迭代的字典        dict *d;        // table :正在被迭代的雜湊表號碼,值可以是 0 或 1 。        // index :迭代器當前所指向的雜湊表索引位置。        // safe :標識這個迭代器是否安全        int table, index, safe;        // entry :當前迭代到的節點的指標        // nextEntry :當前迭代節點的下一個節點        //             因為在安全迭代器運作時, entry 所指向的節點可能會被修改,        //             所以需要一個額外的指標來儲存下一節點的位置,        //             從而防止指標丟失        dictEntry *entry, *nextEntry;        long long fingerprint; /* unsafe iterator fingerprint for misuse detection */    } dictIterator;
雜湊

Redis計算雜湊值和索引值的方法如下:

    // 使用字典設定的雜湊函數,計算鍵key的雜湊值    hash = dict->type->hashFunction(key);    // 使用雜湊表的sizemask屬性和雜湊值,計算出索引值    // 根據情況不同,ht[x]可以是ht[0]或ht[1]    index = hash & dict->ht[x].sizemask;
/* ------------------------- hash functions ------------------------------ *//* Thomas Wang‘s 32 bit Mix Function */unsigned int dictIntHashFunction(unsigned int key){    key += ~(key << 15);    key ^=  (key >> 10);    key +=  (key << 3);    key ^=  (key >> 6);    key += ~(key << 11);    key ^=  (key >> 16);    return key;}/* Identity hash function for integer keys */unsigned int dictIdentityHashFunction(unsigned int key){    return key;}static uint32_t dict_hash_function_seed = 5381;void dictSetHashFunctionSeed(uint32_t seed) {    dict_hash_function_seed = seed;}uint32_t dictGetHashFunctionSeed(void) {    return dict_hash_function_seed;}/* MurmurHash2, by Austin Appleby * Note - This code makes a few assumptions about how your machine behaves - * 1. We can read a 4-byte value from any address without crashing * 2. sizeof(int) == 4 * * And it has a few limitations - * * 1. It will not work incrementally. * 2. It will not produce the same results on little-endian and big-endian *    machines. */unsigned int dictGenHashFunction(const void *key, int len) {    /* ‘m‘ and ‘r‘ are mixing constants generated offline.     They‘re not really ‘magic‘, they just happen to work well.  */    uint32_t seed = dict_hash_function_seed;    const uint32_t m = 0x5bd1e995;    const int r = 24;    /* Initialize the hash to a ‘random‘ value */    uint32_t h = seed ^ len;    /* Mix 4 bytes at a time into the hash */    const unsigned char *data = (const unsigned char *)key;    while(len >= 4) {        uint32_t k = *(uint32_t*)data;        k *= m;        k ^= k >> r;        k *= m;        h *= m;        h ^= k;        data += 4;        len -= 4;    }    /* Handle the last few bytes of the input array  */    switch(len) {    case 3: h ^= data[2] << 16;    case 2: h ^= data[1] << 8;    case 1: h ^= data[0]; h *= m;    };    /* Do a few final mixes of the hash to ensure the last few     * bytes are well-incorporated. */    h ^= h >> 13;    h *= m;    h ^= h >> 15;    return (unsigned int)h;}/* And a case insensitive hash function (based on djb hash) */unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) {    unsigned int hash = (unsigned int)dict_hash_function_seed;    while (len--)        hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */    return hash;}

當字典被用作資料庫的底層實現,或是雜湊鍵的底層實現時,Redis使用MurmurHash2演算法計算鍵的雜湊值:

  • 該演算法的優點在於,即使輸入的鍵是有規律的,演算法仍能給出一個很好的隨機分布性,並且演算法的計算速度也非常快。

為了讓雜湊表的負載因子(load factor)維持在一個合理的範圍之內,當雜湊表儲存的索引值對數量太多或太少時,程式需要對雜湊表的大小進行相應的擴充或收縮。

  • 雜湊表的負載因子計算公式:load_factor = ht[0].used/ht[0].size
rehash

擴充和收縮雜湊表的工作可以通過執行rehash(重新散列)操作來完成,Redis對字典的雜湊表執行rehash的步驟如下:

  • 為字典的ht[1]雜湊表分配空間,這個雜湊表的空間大小取決於要執行的操作,以及ht[0]當前包含的索引值對數量(即ht[0].used屬性的值)

    1. 如果執行的是擴充操作,那麼ht[1]的大小為第一個大於等於ht[0].used*2的2^n(2的n次方冪);
    2. 如果執行的是收縮操作,那麼ht[1]的大小為第一個大於等於ht[0].used的2^n。
  • 將儲存在ht[0]中的所有索引值對rehash到ht[1]上面:rehash指的是重新計算鍵的雜湊值和索引值,然後將索引值對放置到ht[1]雜湊表的指定位置上。

  • 當ht[0]包含的所有索引值對都遷移到ht[1]之後(ht[0]變為空白表),釋放ht[0],將ht[1]設定為ht[0],並在ht[1]新建立一個空白雜湊表,為下一次rehash做準備。

當以下條件中的任意一個被滿足時,程式會自動開始對雜湊表執行擴充操作:

  • 伺服器目前沒有在執行BGSAVE命令或BGREWRITEAOF命令,並且雜湊表的負載因子大於等於1
  • 伺服器目前正在執行BGSAVE命令或BGREWRITEAOF命令,並且雜湊表的負載因子大於等於5

在執行BGSAVE命令或BGREWRITEAOF命令的過程中,Redis需要建立當前伺服器處理序的子進程,而大多數作業系統都採用寫時複製(copy-on-write)技術來最佳化子進程的使用效率,所以在子進程存在期間,伺服器會提高執行擴充操作所需的負載因子,從而儘可能地避免在子進程存在期間進行雜湊表擴充操作,這避免了不必要的記憶體寫入操作,最大限度地節約記憶體。

當雜湊表的負載因子小於0.1時,程式自動開始對雜湊表執行收縮操作。

漸進式rehash

為了避免rehash對伺服器效能造成影響,伺服器不是一次性將ht[0]裡面的所有索引值對全部rehash到ht[1],而是分多次、漸進式地將ht[0]裡面的索引值對慢慢rehash到ht[1]。

以下是雜湊表漸進式rehash的詳細步驟:

  1. 為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個雜湊表。

  2. 在字典中維持一個索引計數器變數rehashidx,值設定為0,表示rehash工作正式開始

  3. 在rehash進行期間,每次對字典執行添加、刪除、尋找或者更新操作時,程式除了執行指定的操作以為,還會順帶將ht[0]雜湊表在rehashidx索引上的所有索引值對rehash到ht[1],當rehash工作完成之後,程式將rehashidx屬性的值增一。

  4. 隨著字典操作的不斷執行,最終在某個時間點上,ht[0]的所有索引值對都會被rehash到ht[1]上,這是程式將rehashidx屬性的值設為-1,表示rehash操作已完成

漸進式rehash採取分而治之的方式,將rehash索引值對所需的計算工作均攤到對字典的每個添加、刪除、尋找和更新操作上,從而避免了集中式rehash而帶來的龐大計算量。

在進行漸進式rehash的過程中,字典會同時使用ht[0]和ht[1]兩個雜湊表,所以在漸進式rehash進行期間,字典的刪除、尋找、更新會在兩個雜湊表上進行,比如現在ht[0]中尋找,沒找到再去ht[1]尋找

在漸進式rehash執行期間,新添加到字典的索引值對一律會被儲存到ht[1]裡面,而ht[0]則不再進行任何添加操作,這樣保證了ht[0]包含的索引值對數量只減不增,隨著rehash操作的執行最終變成空表。

《Redis設計與實現》[第一部分]資料結構與對象-C源碼閱讀(一)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.