標籤:
這個章節要學習到的源碼都是在dict.h和dict.c兩個檔案中
在java語言或者其他支援關聯陣列的的語言中,我們最Crowdsourced Security Testing道的就是關聯陣列(字典)就是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屬性就是起到這個作用,他的定義如下:
//針對不同的字典類型,綁定不同的操作函數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屬性用來幹什麼呢?我們從針對不同的類型可以綁定的不同的函數來看,我們先把這個屬性看做一個儲存一般資料的屬性了。
真正用來儲存資料的就是ht數組,他有兩個dictht類型的元素,為什麼需要兩個?其中的一個用來儲存真實的key-value,另外一個是用來rehash用的。
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對應的valuevoid *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
Redis研究-3.2 資料結構之關聯陣列(字典)