Redis研究-3.2 數據結構之關聯數組(字典)

這個章節要學習到的源碼都是在dict.h和dict.c兩個文件中 java

在java語言或者其餘支持關聯數組的的語言中,咱們最早知道的就是關聯數組(字典)就是key-value的「數組」,那麼,在Redis中又是如何一步一步來實現的呢?咱們先分解一下,關聯數組(字典)就是key-value的「數組」,這句話,首先必需要有key-value這個結構 數組

//key-value結構
typedef struct dictEntry {
    
    // 鍵
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 爲何須要這個呢?這是用來解決鍵衝突的問題的
    struct dictEntry *next;

} dictEntry;


上面定義的這個結構,key表明鍵,值能夠是一個指針,也能夠是一個uint64_t的整數,也能夠是一個int64_t的整數。那麼,next的具體做用是什麼呢?這個指針的做用是能夠將多個哈希值相同的鍵值對鏈接在一塊兒,能夠用來解決鍵衝突的問題。 安全

接下來的問題就是,如何構建一個「數組」?在Redis中的定義見下面的代碼: 函數

typedef struct dictht {
    
    // 數組
    dictEntry **table;

    // 大小
    unsigned long size;
    unsigned long sizemask;

    //已有節點的數量
    unsigned long used;

} dictht;



上面的table就是一個數組,每一個數組的元素就是一個指向dictEntry的指針。而size屬性則記錄了table中的大小,爲何會有這個玩意兒呢?咱們平時常常聽到有叫「哈希桶」,這個的做用就是「哈希桶」的做用,用來標明這個哈希表有多少個桶,那麼,used又是什麼呢?他表明了table中如今的元素個數(不過,我以爲更應該叫作已經佔用了多少個索引了)。如今還差一個sizemask,他是神馬呢?他是和哈希是密切相關的,sizemark的大小始終等於size-1,至於和哈希有關的東西,後面用到再來講。 性能

下一步,就應該是咱們的終極實現目標-關聯數組(字典),在Redis中,他是這樣來定義的: 學習

typedef struct dict {

    dictType *type;

    void *privdata;

    dictht ht[2];

    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    int iterators; /* number of iterators currently running */

} dict;

咱們知道,要實現一個通用的字典,你定義的時候,是不能使用具體類型的,於是,也就不能指定特定的操做,所以,在在Redis的字典裏,針對不一樣的類型,你是能夠本身配置本身的操做的,type屬性就是起到這個做用,他的定義以下: ui

//針對不一樣的字典類型,綁定不一樣的操做函數
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;

那麼,privdata屬性用來幹什麼呢?咱們從針對不一樣的類型能夠綁定的不一樣的函數來看,咱們先把這個屬性看作一個存儲通常數據的屬性了。 this

真正用來存儲數據的就是ht數組,他有兩個dictht類型的元素,爲何須要兩個?其中的一個用來存儲真實的key-value,另一個是用來rehash用的。 spa

rehashidx這個整數用來幹嗎呢?用來標明rehash的進度,若是這個字典沒有rehash,那麼他的值就是-1. 線程

iterators整數用來記錄正在使用在當前字典上的迭代器。

從key-value結構定義到key-value的數組(table)定義,再到字典定義,實現路線已經很清楚了。根據上面的定義咱們能夠看到,到目前爲止,咱們還有三個關鍵的實現或者概念沒有講清楚,分別是哈希和衝突、重哈希。

什麼是哈希?

   舉個簡單的例子,當咱們要把一個鍵值對k1-v1加入到一個字典dict中,從上面咱們知道,真正存儲數據的是這個dict中的ht數組,而這個ht素組的元素是dictht,也是一個數組,對於數組的話,最經常使用的一個屬性就是數組的索引,所以,你要把這個鍵值對加入到這個字典的數組中,就須要計算出來這個鍵值對應該放在字典的數組的哪個索引上。

針對上面的描述,當咱們要把一個鍵值對加入到字典中的時候,須要經歷下面的步驟:

1.用這個dict(字典)的type中的hashFunction來計算這個鍵值的哈希值: 

keyHashValue=dict->type->hashFunction(k1);

2.咱們前面說過,哈希表中有兩個很重要的屬性,一個是size(用來標明有多少個哈希桶),另一個就是sizemark屬性(他的值等於size-1),用sizemark和上面獲得的hash值,就能夠獲得數組的索引:

index=keyHashValue&ht[0].sizemark;//咱們指定存儲數據的是ht的第一個哈希表

從上面的兩個步驟來看,這裏的性能和數據分佈狀況主要取決於你綁定的哈希函數。

 

什麼是哈希衝突?
    爲何會存在哈希衝突?咱們從上面添加新的鍵值對的步驟來看,咱們極有可能會遇到不一樣的鍵計算出來的數組的索引是相同的,這個時候咱們就說存在了哈希衝突。那麼,在Redis中,他是怎麼來解決這個問題的呢?答案就是咱們提到的在dictEntry中定義的next指針啦。使用這個指針,有相同的哈希值的不一樣的鍵值對會造成一個鏈表。而咱們看到,造成的這種鏈表是沒有head和tail的,所以爲了性能考慮,新增的具備相同的哈希值的不一樣的鍵值對會放在這個鏈表的首部,從而下降複雜度。


什麼是重哈希(rehash)?

    在說重哈希以前,咱們應該先明白什麼是負載因子。所謂負載因子就是你的散列表中已經存儲的節點的數量(N)除以散列表所能容納的能力(M),這裏的M>=N,那麼負載因子就是N/M,這個比值說明了,你的散列表的裝滿程度。

   明白了負載因子後咱們更容易明白,爲何會存在重哈希了。在咱們對字典的操做中,會致使字典存儲的鍵值對愈來愈多或者愈來愈少,進而會致使負載因子出現大範圍波動,爲了保證這個負載因子是在咱們的範圍內,咱們須要進行重哈希。怎麼作呢?

  在知足必定狀況下(這種狀況在之後的章節學習中再來說),程序會觸動衝哈希操做,操做的步驟是:

1.爲字典的ht[1]分配空間,這個空間的大小是第一個大於ht[0].used*2的2的n次方。(好比used=4,那麼4*2=8,而8正好是2的3次方。若是used=5,5*2=10,而大於10的2的n次方中的n應該取4,故ht[1]的大小應該是2^4=16,以此類推)。

2.將ht[0]中的鍵值對從新計算hash放到ht[1]上。

3.當ht[0]中的鍵值對所有已經轉移到了ht[1]上後,釋放ht[0],並將ht[1]設置爲ht[0],並在ht[1]上新建一個空白的哈希表,供下一次使用。

可是,這裏就會存在一個問題,當ht[0]上的鍵值對超級多的時候,是否是中止響應,只作rehash了?那這樣子的話,Redis就沒有必要存在了,所以,在Redis中就採用一種漸進式的Rehash。怎麼玩呢?關鍵就是dict->rehashidx這個計數器起到的做用。

1.爲ht[1]分配空間,這個dict同時擁有ht[0]和ht[1]兩個哈希表;

2.在進行衝哈希的時候,將rehashidx設定爲正在衝哈希的索引;

3.將ht[0]上的鍵值對衝哈希到ht[1]上,重哈希完成後,rehashidx設置爲-1;

所以,在衝哈希期間,全部的操做都是針對兩個哈希表的。

大致已經說清楚了,下面就是經常使用的API啦

//建立一個新的字典
dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type,privDataPtr);

    return d;
}

上面的函數用到了一個私有函數_dictInit。定義以下:

//初始化字典
int _dictInit(dict *d, dictType *type,
        void *privDataPtr)
{
    // 初始化,從下面的函數能夠看到,這裏並無分配空間
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);

    // 設置類型特定函數
    d->type = type;

    // 設置私有數據
    d->privdata = privDataPtr;

    // 設置哈希表 rehash 狀態
    d->rehashidx = -1;

    // 設置字典的安全迭代器數量
    d->iterators = 0;

    return DICT_OK;
}

裏面用到了_dictReset私有函數:

static void _dictReset(dictht *ht)
{
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}



//添加新的鍵值對
int dictAdd(dict *d, void *key, void *val)
{
    
    dictEntry *entry = dictAddRaw(d,key);

    // 鍵已存在
    if (!entry) return DICT_ERR;

    // 鍵不存在
    dictSetVal(d, entry, val);

    // 添加成功
    return DICT_OK;
}

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

    // 若是dict正在進行hash,那麼就進行單步 rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    // 計算鍵在哈希表中的索引值
    // 若是值爲 -1 ,那麼表示鍵已經存在
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry */
    // 若是字典正在 rehash ,那麼將新鍵添加到 1 號哈希表
    // 不然,將新鍵添加到 0 號哈希表
    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;
}

static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}


 

int dictRehash(dict *d, int n) {
     //並非線程安全的哦
    // dict沒有在rehash的時候就直接返回
    if (!dictIsRehashing(d)) return 0;

    // 進行 n 步遷移
    while(n--) {
        dictEntry *de, *nextde;

        /* Check if we already rehashed the whole table... */
        // 若是 0 號哈希表爲空,那麼表示 rehash 執行完畢
        if (d->ht[0].used == 0) {
            // 釋放 0 號哈希表
            zfree(d->ht[0].table);
            // 將原來的 1 號哈希表設置爲新的 0 號哈希表
            d->ht[0] = d->ht[1];
            // 重置舊的 1 號哈希表
            _dictReset(&d->ht[1]);
            // 關閉 rehash 標識
            d->rehashidx = -1;
            // rehash 已經完成
            return 0;
        }

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        // 確保 rehashidx 沒有越界
        assert(d->ht[0].size > (unsigned)d->rehashidx);

        // 略過數組中爲空的索引,找到下一個非空索引
        while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;

        // 指向該索引的鏈表表頭節點
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        // 將鏈表中的全部節點遷移到新哈希表
        while(de) {
            unsigned int 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;
        // 更新 rehash 索引
        d->rehashidx++;
    }

    return 1;
}

dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    unsigned int h, idx, table;

    // 字典爲空,直接返回NULL
    if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */

    // 若是dict正在rehash,那麼就進行rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 計算鍵的哈希值
    h = dictHashKey(d, key);
    // 在字典的哈希表中查找這個鍵,這裏的有兩個哈希表
    for (table = 0; table <= 1; table++) {

        // 計算索引值
        idx = h & d->ht[table].sizemask;

        // 遍歷給定索引上的鏈表的全部節點,查找 key
        he = d->ht[table].table[idx];
        while(he) {
			//找到就返回
            if (dictCompareKeys(d, key, he->key))
                return he;

            he = he->next;
        }
         //若是運行到這裏還沒找到,首先要判斷dict是否是在rehash,若是是,則要去另一個哈希表中找,找不到才返回NULL
        if (!dictIsRehashing(d)) return NULL;
    }

    // 進行到這裏時,說明兩個哈希表都沒找到
    return NULL;
}

//在dict中得到指定的key對應的value
void *dictFetchValue(dict *d, const void *key) {
    dictEntry *he;

    he = dictFind(d,key);

    return he ? dictGetVal(he) : NULL;
}

上面已經說了增、查,下面還有改、刪

static int dictGenericDelete(dict *d, const void *key, int nofree)
{
    unsigned int h, idx;
    dictEntry *he, *prevHe;
    int table;

    // dict爲空的話,返回刪除錯誤
    if (d->ht[0].size == 0) return DICT_ERR; /* d->ht[0].table is NULL */

    // 進行單步rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 計算哈希值
    h = dictHashKey(d, key);

    // 遍歷哈希表
    for (table = 0; table <= 1; table++) {

        // 計算索引值 
        idx = h & d->ht[table].sizemask;
        // 指向該索引上的鏈表
        he = d->ht[table].table[idx];//這有可能就是一個鏈表
        prevHe = NULL;
        // 遍歷鏈表上的全部節點
        while(he) {
        
            if (dictCompareKeys(d, key, he->key)) {
                // 查找目標節點

                /* Unlink the element from the list */
                // 從鏈表中刪除
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;

                // 釋放調用鍵和值的釋放函數?
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                }
                
                // 釋放節點自己
                zfree(he);

                // 更新已使用節點數量,我的以爲這裏是有問題的,由於一個節點上可能存在一個鏈表,而此次刪除的有可能只是鏈表中的一部分,所以,節點數是不能少的
                d->ht[table].used--;

                // 返回已找到信號
                return DICT_OK;
            }

            prevHe = he;
            he = he->next;
        }

        // 若是執行到這裏,說明在 0 號哈希表中找不到給定鍵
        // 那麼根據字典是否正在進行 rehash ,決定要不要查找 1 號哈希表
        if (!dictIsRehashing(d)) break;
    }

    // 沒找到
    return DICT_ERR; /* not found */
}
int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0);//要調用釋放節點的函數
}


int dictDeleteNoFree(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,1);//不調用釋放函數
}



 

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, auxentry;

    /* Try to add the element. If the key
     * does not exists dictAdd will suceed. */
    // 嘗試直接將鍵值對添加到字典
    // 若是鍵 key 不存在的話,添加會成功
    if (dictAdd(d, key, val) == DICT_OK)
        return 1;

    /* It already exists, get the entry */
    // 運行到這裏,說明鍵 key 已經存在,那麼找出包含這個 key 的節點
    entry = dictFind(d, key);
    /* Set the new value and free the old one. Note that it is important
     * to do that in this order, as the value may just be exactly the same
     * as the previous one. In this context, think to reference counting,
     * you want to increment (set), and then decrement (free), and not the
     * reverse. */
    // 先保存原有的值的指針
    auxentry = *entry;
    // 而後設置新的值
    dictSetVal(d, entry, val);
    // 而後釋放舊值
    dictFreeVal(d, &auxentry);

    return 0;
}




在咱們學習java的集合類的時候,最經常使用的一個武器就是迭代器,在Redis的dict中,也實現了迭代器,分爲安全的和不安全的

typedef struct dictIterator {
        
    // 被迭代的字典
    dict *d;

    // table :正在被迭代的哈希表號,值能夠是 0 或 1 。
    // index :迭代器當前所指向的哈希表索引位置。
    // safe 迭代器是否安全,當爲1的時候,他是安全的,不然爲不安全的
    int table, index, safe;

    // entry :當前迭代到的節點的指針
    // nextEntry :當前迭代節點的下一個節點, 由於在安全迭代器運做時, entry所只帶的節點有可能被修改,因此須要一個額外的指針來保存下一節點的位置,從而防止指針丟失
    dictEntry *entry, *nextEntry;

    long long fingerprint; /* unsafe iterator fingerprint for misuse detection */
} dictIterator;


//生成一個不安全的迭代器
dictIterator *dictGetIterator(dict *d)
{
    dictIterator *iter = zmalloc(sizeof(*iter));

    iter->d = d;
    iter->table = 0;
    iter->index = -1;
    iter->safe = 0;
    iter->entry = NULL;
    iter->nextEntry = NULL;

    return iter;
}

 

//生成安全的迭代器
dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d);

    i->safe = 1;

    return i;
}

好啦,這一節有點多,請見諒,若是有疑問,請諮詢QQ:359311095

相關文章
相關標籤/搜索