1. 程式人生 > >Redis底層資料結構dict

Redis底層資料結構dict

dict是Redis底層資料結構中實現最為複雜的一個數據結構, 其功能類似於C++標準庫中的std::unordered_map, 其實現位於 src/dict.h 與 src/dict.c中, 其關鍵定義如下:

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

typedef struct dictType {
    uint64_t (*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;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

/* If safe is set to 1 this is a safe iterator, that means, you can call
 * dictAdd, dictFind, and other functions against the dictionary even while
 * iterating. Otherwise it is a non safe iterator, and only dictNext()
 * should be called while iterating. */
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint;
} dictIterator;

其記憶體佈局如下所示:

dict

  1. dict中儲存的鍵值對, 是通過dictEntry這個結構間接持有的, k通過指標間接持有鍵, v通過指標間接持有值. 注意, 若值是整數值的話, 是直接儲存在v欄位中的, 而不是間接持有. 同時next指標用於指向, 在bucket索引值衝突時, 以鏈式方式解決衝突, 指向同索引的下一個dictEntry結構.
  2. 傳統的雜湊表實現, 是一塊連續空間的順序表, 表中元素即是結點. 在dictht.table中, 結點本身是散佈在記憶體中的, 順序表中儲存的是dictEntry的指標
  3. 雜湊表即是dictht結構, 其通過table
    欄位間接的持有順序表形式的bucket, bucket的容量儲存在size欄位中, 為了加速將雜湊值轉化為bucket中的陣列索引, 引入了sizemask欄位, 計算指定鍵在雜湊表中的索引時, 執行的操作類似於dict->type->hashFunction(鍵) & dict->ht[x].sizemask. 從這裡也可以看出來, bucket的容量適宜於為2的冪次, 這樣計算出的索引值能覆蓋到所有bucket索引位.
  4. dict即為字典. 其中type欄位中儲存的是本字典使用到的各種函式指標, 包括雜湊函式, 鍵與值的複製函式, 釋放函式, 以及鍵的比較函式. privdata
    是用於儲存使用者自定義資料. 這樣, 字典的使用者可以最大化的自定義字典的實現, 通過自定義各種函式實現, 以及可以附帶私有資料, 保證了字典有很大的調優空間.
  5. 字典為了支援平滑擴容, 定義了ht[2]這個陣列欄位. 其用意是這樣的:
    1. 一般情況下, 字典dict僅持有一個雜湊表dictht的例項, 即整個字典由一個bucket實現.
    2. 隨著插入操作, bucket中出現衝突的概率會越來越大, 當字典中儲存的結點數目, 與bucket陣列長度的比值達到一個閾值(1:1)時, 字典為了緩解效能下降, 就需要擴容
    3. 擴容的操作是平滑的, 即在擴容時, 字典會持有兩個dictht的例項, ht[0]指向舊雜湊表, ht[1]指向擴容後的新雜湊表. 平滑擴容的重點在於兩個策略:
      1. 後續每一次的插入, 替換, 查詢操作, 都插入到ht[1]指向的雜湊表中
      2. 每一次插入, 替換, 查詢操作執行時, 會將舊錶ht[0]中的一個bucket索引位持有的結點連結串列, 遷移到ht[1]中去. 遷移的進度儲存在rehashidx這個欄位中.在舊錶中由於衝突而被連結在同一索引位上的結點, 遷移到新表後, 可能會散佈在多個新表索引中去.
      3. 當遷移完成後, ht[0]指向的舊錶會被釋放, 之後會將新表的持有權轉交給ht[0], 再重置ht[1]指向NULL
  6. 這種平滑擴容的優點有兩個:
    1. 平滑擴容過程中, 所有結點的實際資料, 即dict->ht[0]->table[rehashindex]->kdict->ht[0]->table[rehashindex]->v分別指向的實際資料, 記憶體地址都不會變化. 沒有發生鍵資料與值資料的拷貝或移動, 擴容整個過程僅是各種指標的操作. 速度非常快
    2. 擴容操作是步進式的, 這保證任何一次插入操作都是順暢的, dict的使用者是無感知的. 若擴容是一次性的, 當新舊bucket容量特別大時, 遷移所有結點必然會導致耗時陡增.

除了字典本身的實現外, 其中還順帶實現了一個迭代器, 這個迭代器中有欄位safe以標示該迭代器是"安全迭代器"還是"非安全迭代器", 所謂的安全與否, 指是的這種場景:
設想在執行迭代器的過程中, 字典正處於平滑擴容的過程中. 在平滑擴容的過程中時, 舊錶一個索引位上的, 由衝突而鏈起來的多個結點, 遷移到新表後, 可能會散佈到新表的多個索引位上. 且新的索引位的值可能比舊的索引位要低.

遍歷操作的重點是, 保證在迭代器遍歷操作開始時, 字典中持有的所有結點, 都會被遍歷到. 而若在遍歷過程中, 一個未遍歷的結點, 從舊錶遷移到新表後, 索引值減小了, 那麼就可能會導致這個結點在遍歷過程中被遺漏.

所以, 所謂的"安全"迭代器, 其在內部實現時: 在迭代過程中, 若字典正處於平滑擴容過程, 則暫停結點遷移, 直至迭代器執行結束. 這樣雖然不能保證在迭代過程中插入的結點會被遍歷到, 但至少保證在迭代起始時, 字典中持有的所有結點都會被遍歷到.

這也是為什麼dict結構中有一個iterators欄位的原因: 該欄位記錄了運行於該字典上的安全迭代器的數目. 若該數目不為0, 字典是不會繼續進行結點遷移平滑擴容的.

下面是字典的擴容操作中的核心程式碼, 我們以插入操作引起的擴容為例:

先是插入操作的外部邏輯:

  1. 如果插入時, 字典正處於平滑擴容過程中, 那麼無論本次插入是否成功, 先遷移一個bucket索引中的結點至新表
  2. 在計算新插入結點鍵的bucket索引值時, 內部會探測雜湊表是否需要擴容(若當前不在平滑擴容過程中)
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);          // 呼叫dictAddRaw

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d); // 若在平滑擴容過程中, 先步進遷移一個bucket索引

    /* Get the index of the new element, or -1 if
     * the element already exists. */

    // 在計算鍵在bucket中的索引值時, 內部會檢查是否需要擴容
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);
    return entry;
}

下面是計算bucket索引值的函式, 內部會探測該雜湊表是否需要擴容, 如果需要擴容(結點數目與bucket陣列長度比例達到1:1), 就使字典進入平滑擴容過程:

static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR) // 探測是否需要擴容, 如果需要, 則開始擴容
        return -1;
    for (table = 0; table <= 1; table++) {
        idx = hash & d->ht[table].sizemask;
        /* Search if this slot does not already contain the given key */
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK; // 如果正在擴容過程中, 則什麼也不做

    /* If the hash table is empty expand it to the initial size. */
    // 若字典中本無元素, 則初始化字典, 初始化時的bucket陣列長度為4
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    // 若字典中元素的個數與bucket陣列長度比值大於1:1時, 則呼叫dictExpand進入平滑擴容狀態
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

int dictExpand(dict *d, unsigned long size)
{
    dictht n; /* the new hash table */  // 新建一個dictht結構
    unsigned long realsize = _dictNextPower(size);  

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));// 初始化dictht下的table, 即bucket陣列
    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    // 若是新字典初始化, 直接把dictht結構掛在ht[0]中
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    // 否則, 把新dictht結構掛在ht[1]中, 並開啟平滑擴容(置rehashidx為0, 字典處於非擴容狀態時, 該欄位值為-1)
    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

下面是平滑擴容的實現:

static void _dictRehashStep(dict *d) {
    // 若字典上還執行著安全迭代器, 則不遷移結點
    // 否則每次遷移一箇舊bucket索引上的所有結點
    if (d->iterators == 0) dictRehash(d,1); 
}

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        // 在舊bucket中, 找到下一個非空的索引位
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        // 取出該索引位上的結點連結串列
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        // 把所有結點遷移到新bucket中去
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    // 檢查是否舊錶中的所有結點都被遷移到了新表
    // 如果是, 則置先釋放原舊bucket陣列, 再置ht[1]為ht[0]
    // 最後再置rehashidx=-1, 以示字典不處於平滑擴容狀態
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

總結:

  1. 字典的實現很複雜, 主要是實現了平滑擴容邏輯
  2. 使用者資料均是以指標形式間接由dictEntry結構持有, 故在平滑擴容過程中, 不涉及使用者資料的拷貝
  3. 有安全迭代器可用, 安全迭代器保證, 在迭代起始時, 字典中的所有結點, 都會被迭代到, 即使在迭代過程中對字典有插入操作
  4. 字典內部使用的預設雜湊函式其實也非常有講究, 不過限於篇幅, 這裡不展開講. 並且字典的實現給了使用者非常大的靈活性(dictType結構與dict.privdata欄位), 對於一些特定場合使用的鍵資料, 使用者可以自行選擇更高效更特定化的雜湊函式