redis學習筆記(3)---字典dict

字典dict

  redis中的字典,即hash表,其實現與Java中的HashMap基本類似。同樣是基於數組和鏈表的,通過“拉鍊法”來處理碰撞。
  字典中的每一個key都是獨一無二的,當要加入一個key-value對時,如果key在字典中已經存在,則會直接返回,而不會重新將其加入到字典中。

字典的實現  

hash節點的定義如下: 

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
  • key:元素的key
  • v:元素的值
  • next:處理碰撞,所有分配到同一索引的元素通過next指針鏈接起來形成鏈表
  • key和v都可以保存多種類型的數據

hash表的定義如下:  

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
  • table:一個二級指針,真正存儲數據的地方。可以將table看做一個指向數組的指針,而數組就是hash表最基本的結構。通過數組和hash節點中的next指針形成完整的hash表。
  • size:數組的大小,通常爲2的整數次方
  • sizemask:hash表大小掩碼,用於計算索引,當size非0時爲(size-1)
  • used:hash表中已有節點的數量

處理函數  

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;

  redis中通過dictType這樣的一個結構用來存儲針對不同類型的鍵值對的處理函數,這樣對於不同類型的鍵值對,就可以有不同的處理了。即通過函數指針實現多態。

字典定義如下:   

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;
  • type:處理函數表,通過其中保存的函數指針對不同類型的數據進行不同的處理,實現多態
  • privdata:私有數據
  • ht[2]:hash表,可以發現一個字典中有兩個hash表
  • rehashidx:ht[0]中正在rehash的桶的索引,當rehash=-1時,表明此時沒有在進行rehash操作
  • iterator:正在運行的迭代器的數量

示意圖 

  一個字典的示意圖如下:
  這裏寫圖片描述

rehash  

  hash表利用負載因子loadfactor = used/size來表明hash表當前的存儲情況。
  當負載因子過大時操作的時間複雜度增大,負載因子過小時說明hash表的填充率很低,浪費內存。redis中的數據都是存儲在內存中的,因此我們必須儘量的節省內存。
  因此我們必須將loadfactor控制在一定的範圍內,同時保證操作的時間複雜度接近O(1)和內存儘量被佔用。即rehash操作分爲擴展和收縮兩種情況。
 
  dict中有兩個hash表,ht[0]和ht[1]。所有的數據都是存在放dict的ht[0]中,ht[1]只在rehash的時候使用。dict進行rehash的時候,將ht[0]中的所有數據rehash到ht[1]中。
  dict的rehash並不是一次性完成的,而是分多次、漸進式的完成的
  每一步的大小分爲兩種:
  1)在一步中將ht[0]的table中的一個元素,也就是一個哈希桶所對應的鏈表中的所有元素進行rehash。(在調用dict的一些操作函數時,如add,find等時進行)
  2)在一步中執行一段固定的時間,當時間到達後,暫停rehash。 (數據庫週期性執行databasesCron()時進行)
  這兩種方法對應的函數分別是_dictRehashStep和dictRehashMilliseconds。

  rehash過程的基本步驟如下:
  1) 首先調用dictExpand爲ht[1]分配空間  

static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE; 

    if (size >= LONG_MAX) return LONG_MAX;
    while(1) {
        if (i >= size) //size爲第一個大於等於size的2的冪次
            return i;
        i *= 2;
    }
}
int dictExpand(dict *d, unsigned long size)
{
    dictht n; 
    unsigned long realsize = _dictNextPower(size); //計算ht[1]所需大小

    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;
    if (realsize == d->ht[0].size) return DICT_ERR;

    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    if (d->ht[0].table == NULL) { //表明爲第一次加入元素時創建hash表
        d->ht[0] = n; //直接將新創建的hash表賦給ht[0]
        return DICT_OK;
    }

    d->ht[1] = n;  //否則爲rehash,將新創建的hash表賦給ht[1]
    d->rehashidx = 0;//表明正在處於rehash過程中
    return DICT_OK;
}

  2)調用dictRehash逐步完成rehash操作

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; //每次rehash最多可訪問的空桶的個數
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
         //rehashidx用於指示正在對ht[0]中的第幾個桶進行rehash操作,確保不會越界
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        while(d->ht[0].table[d->rehashidx] == NULL) { //跳過數組中爲空的桶
            d->rehashidx++;
            if (--empty_visits == 0) return 1; //如果訪問空桶次數超過限制,則直接返回
        }
        de = d->ht[0].table[d->rehashidx]; //ht[0]中正在rehash的桶元素的頭節點
        while(de) {
            unsigned int h;

            nextde = de->next;
            h = dictHashKey(d, de->key) & d->ht[1].sizemask; //計算ht[0]中元素進行rehash後在ht[1]中的索引
            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++; //該桶處理完成後,準備處理下一個桶
    }

    //ht[0]剩餘元素個數爲0,表明ht[0]中的元素已經全部rehash到ht[1]中,因此rehash過程已經完成。
    if (d->ht[0].used == 0) { 
        //可以釋放ht[0],並將ht[1]賦給ht[0]後重置ht[1]
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1; //表明rehash已經結束
        return 0;
    }

    return 1; //否則還處於rehash過程中
}

  將ht[0]中的所有元素全部轉移到ht[1]中,釋放原來的ht[0],將ht[1]賦給ht[0],並重置ht[1]爲下次rehash做準備  
  

創建一個字典: 

dict *dictCreate(dictType *type,void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d)); //分配內存

    _dictInit(d,type,privDataPtr); //字典初始化
    return d;
} 
int _dictInit(dict *d, dictType *type,void *privDataPtr)
{
    _dictReset(&d->ht[0]); //初始化兩個hash表
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;
    d->iterators = 0;
    return DICT_OK;
}
static void _dictReset(dictht *ht)
{  
    //hash表的初始化
    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; //表明key已存在
    dictSetVal(d, entry, val);
    return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d); //當正在rehash時,先進行rehash操作

    if ((index = _dictKeyIndex(d, key)) == -1)  ///計算key在hash表中的桶的索引,-1時表示key已存在
        return NULL;

    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];  //當正在rehash時,則加入ht[1]中,否則加入到ht[0]
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];  //將新元素加入到桶中鏈表的頭節點
    ht->table[index] = entry;
    ht->used++;

    dictSetKey(d, entry, key);
    return entry;
}

  可以發現,在一個字典中,每一個key都是獨一無二的。當加入一個key-value對時,如果字典中已存在該key,則會直接返回而不會繼續執行加入操作,既不會執行dictSetKey(),也不會執行dictSetVal()。


本文所引用的源碼全部來自Redis3.0.7版本

redis學習參考資料:
https://github.com/huangz1990/redis-3.0-annotated
Redis 設計與實現(第二版)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章