標籤:redis 字典 dict 迭代器 rehash
針對 上一文中提出的問題,這一次就進行解答:
由rehash過程可以看出,在rehash過程中,ht[0]和ht[1]同時具有條目,即字典中的所有條目分布在ht[0]和ht[1]中,
這時麻煩也就出來了。主要有以下問題:(現在暫不解答是如何解決的)
1.如何尋找key。
2.如何插入新的key。
3.如何刪除一個key。
4.如何確保rehash過程不斷插入、刪除條目,而rehash沒有出錯。
5.如何遍曆dict所有條目,如何確保遍曆順序。
6.如何確保迭代器有效,且正確。
1. 如何尋找key
dictEntry *dictFind(dict *d, const void *key){ dictEntry *he; unsigned int h, idx, table; if (d->ht[0].size == 0) return NULL; /* We don‘t have a table at all */ if (dictIsRehashing(d)) _dictRehashStep(d);//如果進行中rehash,則進行一次rehash操作 h = dictHashKey(d, key);//計算key的雜湊值 //先在ht[0]表上尋找 for (table = 0; table <= 1; table++) { idx = h & d->ht[table].sizemask; he = d->ht[table].table[idx]; while(he) { if (dictCompareKeys(d, key, he->key)) return he; he = he->next; } //在ht[0]上找不到時,如果現在正進行rehash,key有可能在ht[1]上,需要在ht[1]上尋找 if (!dictIsRehashing(d)) return NULL; } return NULL;}
因為rehash時,ht[0]與ht[1]上都有條目,所以需要在兩個表中都尋找不到元素時,才能確定元素是否存在。至於先尋找哪一個表,並不會影響結果。
在尋找過程中,如果進行中rehash,則會進行一次rehash操作,這樣的做法跟rehash的實現是相對應的,因為rehash並不會一次完成,需要分成多次完成。那麼如何分成多次,什麼時候該執行一次rehash操作?在dictRehash函數中已經知道是如何分成多次的,執行則是分散到一些操作中,如尋找元素等。這樣分散rehash步驟不會對一次查詢請求有很大的影響,保持查詢效能的穩定。
2. 如何插入新的key
//添加條目到字典中/* Add an element to the target hash table */int dictAdd(dict *d, void *key, void *val){ dictEntry *entry = dictAddRaw(d,key);//插入key if (!entry) return DICT_ERR; dictSetVal(d, entry, val);//設定key所對應的value return DICT_OK;}/* Low level add. This function adds the entry but instead of setting * a value returns the dictEntry structure to the user, that will make * sure to fill the value field as he wishes. * * This function is also directly exposed to the user API to be called * mainly in order to store non-pointers inside the hash value, example: * * entry = dictAddRaw(dict,mykey); * if (entry != NULL) dictSetSignedIntegerVal(entry,1000); * * Return values: * * If key already exists NULL is returned. * If key was added, the hash entry is returned to be manipulated by the caller. */dictEntry *dictAddRaw(dict *d, void *key){ int index; dictEntry *entry; dictht *ht; if (dictIsRehashing(d)) _dictRehashStep(d); //rehash //如果key已經存在,則返回null /* Get the index of the new element, or -1 if * the element already exists. */ if ((index = _dictKeyIndex(d, key)) == -1) return NULL; //如果進行中rehash,則就把新的元素插入到ht[1]中,否則插入到ht[0] /* Allocate the memory and store the new entry */ 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;}
當dict沒有進行rehash時,元素插入到ht[0]是比較容易的。但如果進行中rehash,則要把元素插入到ht[1]中。為什麼一定要把元素插入到ht[1]中,而不能是ht[0]?原因就在rehash的過程。rehash的過程是把條目由ht[0]移動到ht[1]的過程,當所有條目都移動完畢時,rehash的過程也就完成。要保證rehash過程能完成,需要注意幾點:
a. ht[0]的元素不能一直在增,即使元素在增長也不能快於移動元素到ht[1]的速度。
b. 確定下一個要移動的條目(如按某種方法支確定下一個條目,能否遍曆所有ht[0]上的條目)
c. 確定何時移動完所有條目
元素不能插入到ht[0]的原因,就是確保b。rehash過程中,通過rehashidx記錄已經處理過的桶,因為rehashidx是線性增長的,終會遍曆完ht[0]上所有的桶,但要想rehash能遍曆所有的條目,則還需要確保被處理過的桶不能再插入新的元素。所以新的元素只能插入到ht[1]上。另外,因為沒有新的元素插入到ht[0]中,a 也得到確保。
3.如何刪除一個key。
//先在ht[0]中尋找,如找不到則在ht[1]中尋找,有則刪除。/* Search and remove an element */static int dictGenericDelete(dict *d, const void *key, int nofree){ unsigned int h, idx; dictEntry *he, *prevHe; int table; if (d->ht[0].size == 0) return DICT_ERR; /* d->ht[0].table is NULL */ 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; } if (!dictIsRehashing(d)) break; } return DICT_ERR; /* not found */}
4.如何確保rehash過程不斷插入、刪除條目,而rehash沒有出錯。
從插入和刪除過程可以看出,是不會使rehash出錯的。
5. 如何遍曆dict所有條目,如何確保遍曆順序。
6.如何確保迭代器有效,且正確。
dict的遍曆是用迭代器,迭代器有兩種,一種是普通的迭代器,一種是安全迭代器,相比而言,普通迭代器就是不安全了。
迭代器是很多資料結構(容器)都會有的用於遍曆資料元素的工具。使用迭代器需要注意一些問題:
a. 迭代器的遍曆順序
b. 迭代器遍曆元素過程中是否可以改變容器的元素,如改變容器的元素會有什麼影響,如遍曆順序、迭代器失效
現在了看看dict的迭代器。
遍曆順序不確定,基本可認為是無序。
普通迭代器不允許在遍曆過程中個性dict。安全迭代器則允許。
下面看代碼,
//建立一個普通迭代器dictIterator *dictGetIterator(dict *d){ dictIterator *iter = zmalloc(sizeof(*iter)); iter->d = d; //記錄dict 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;}//遍曆過程dictEntry *dictNext(dictIterator *iter){ while (1) { if (iter->entry == NULL) { //當前條目為null,可能是剛建立,可能是一個為空白的桶,可能是到達桶的最後一個條目,也可能是遍曆完所有的桶 dictht *ht = &iter->d->ht[iter->table]; if (iter->index == -1 && iter->table == 0) { //剛建立的迭代器 if (iter->safe) iter->d->iterators++; //如是安全迭代器,dict中記下 else iter->fingerprint = dictFingerprint(iter->d); //普通迭代器,記下當前的Fringerprint } iter->index++; //下一個桶 if (iter->index >= (long) ht->size) { //如果已經遍曆完表,如果當前進行中rehash,且遍曆完ht[0],則遍曆ht[1] if (dictIsRehashing(iter->d) && iter->table == 0) { iter->table++; iter->index = 0; ht = &iter->d->ht[1]; } else { break; //遍曆完畢 } } //記下當前條目 iter->entry = ht->table[iter->index]; } else { //指向下一個條目 iter->entry = iter->nextEntry; } if (iter->entry) { //找到條目,記下此條目的下一個條目 /* We need to save the ‘next‘ here, the iterator user * may delete the entry we are returning. */ iter->nextEntry = iter->entry->next; return iter->entry; //返回找到的條目 } } //找不到條目了,已經遍曆完dict return NULL;}
從上面的遍曆過程可以看到迭代器遍曆的三個順序:
a. 先遍曆ht[0],如果進行中rehash,則遍曆完ht[0]的所有桶後,遍曆ht[1]
b. 在一個ht中,遍曆是按桶從小到大遍曆
c. 同一個桶中的多個條目,遍曆順序是從鏈頭遍曆到鏈尾,但是條目在鏈中的位置本身也是不確定的。
從上面三個順序中可以得出,迭代器遍曆過程是無序的。
下面來討論迭代器是否能遍曆所有條目的問題。此時要分開普通迭代器與安全迭代器來討論。
普通迭代器,從代碼上看到在普通迭代器開始遍曆時會計算dict的fingerprint,遍曆過程中可以允許dict插入、刪除條目,以及進行rehash。但是,在釋放迭代器時,會比較遍曆完的dict跟遍曆前的dict的fingerprint是否一致,如不一致則程式退出。此時便可以知道,普通迭代器其實並不允許遍曆,儘管遍曆時代碼上並沒有阻止,但最後卻會導致程式出錯退出。不過,比較fingerprint相同,並不能說明dict沒有變化,只能說如果fingerprint不同dict一定發出了變化。
void dictReleaseIterator(dictIterator *iter)
{
if (!(iter->index == -1 && iter->table == 0)) {
if (iter->safe)
iter->d->iterators--;
else
assert(iter->fingerprint == dictFingerprint(iter->d));
}
zfree(iter);
}
安全迭代器,在開始遍曆時會在dict上記下,遍曆過程則跟普通迭代器無區別。那麼在dict上記下有安全迭代器是用來做什麼的呢?通過尋找代碼,可以看到使用dict的安全迭代器計數器的地方是 _dictRehashStep 函數。
/* This function performs just a step of rehashing, and only if there are
* no safe iterators bound to our hash table. When we have iterators in the
* middle of a rehashing we can‘t mess with the two hash tables otherwise
* some element can be missed or duplicated.
*
* This function is called by common lookup or update operations in the
* dictionary so that the hash table automatically migrates from H1 to H2
* while it is actively used. */
static void _dictRehashStep(dict *d) {
if (d->iterators == 0) dictRehash(d,1); //如果安全迭代器計數器為0,則允許進行rehash操作
}
而從釋放迭代器的函數 dictReleaseIterator 可以看到並沒有檢查 fingerprint的操作,因此可以得出所謂的安全迭代器,實則是指:
a. 迭代過程中可以允許插入、刪除條目
b. 迭代過程中不會進行rehash,如開始迭代前已經進行了rehash,則迭代開始後rehash會被暫停,直到迭代完成後rehash接著進行。
既然遍曆過程中允許插入、刪除,那如何遍曆過程。
插入元素時,對遍曆過程無大影響,但能否遍曆到剛插入的元素則是不確定的。
刪除元素時,要分四種情況:刪除已經遍曆的元素,刪除當前元素,刪除下一個要遍曆的元素,刪除非下一個要遍曆的未遍曆的元素。
刪除已經遍曆的元素,對遍曆過程是無影響的。
刪除當前元素,對遍曆過程也是無影響的,因為當前元素已經被訪問,迭代器取下一個元素時不再依靠當前元素。
刪除下一個要遍曆的元素,又可以分成兩種情況,下一個元素已經記錄在迭代器的nextEntry中和沒有記錄在迭代器中。如果下一個元素沒有記錄在迭代器的nextEntry中,對遍曆過程是無影響的。如果已經被記錄在nextEntry中,則迭代器此時失效,企圖訪問下一個元素將會產生不可預期的效果。
刪除非下一個要遍曆的未遍曆的元素,對遍曆過程也是影響的,只是已經刪除了的元素是不會被遍曆到了。
從上面的討論可知,安全迭代器其實也並不是真正的安全,刪除元素時有可能引起迭代器失效。
現在討論為什麼安全迭代器在遍曆過程中不允許rehash,因為如果允許rehash,遍曆過程將無法保證,有些元素可能會遍曆多次,有些元素會沒有遍曆到。下面舉一些情景:
a. 迭代器現在遍曆到ht[0]某個元素x,此時x位於2號桶,由於rehash可以進行,剛好把ht[0]的1號桶的元素Y移動到ht[1]中,此後迭代器遍曆完ht[0]後就會遍曆到ht[1],會把Y再一次遍曆。
b. 迭代器此時正遍曆到ht[1]的4號桶,後面的桶都還沒遍曆,此時rehash過程進行且剛好把ht[0]的所有元素都移動到ht[1]上,rehash過程完成,ht[1]切換到ht[0]。由於迭代器中記錄目前正在遍曆ht[1],所以此後迭代器遍曆ht[1](原來的ht[0])的4號桶後的元素時已經沒有元素了,遍曆過程結束,而實際上還有一些元素沒有被遍曆。
從上面討論可以看出,遍曆過程中是不能允許rehash的。
綜合上面的討論,可以看出,使用安全迭代器,只要不進行刪除元素的操作,遍曆過程基本是沒有問題的,在遍曆開始時已經存在的元素是會被遍曆到的。只不過使用安全迭代器本身對dict是有一定的影響的。一是暫停rehash過程,二是如果一直持有安全迭代器不釋放,rehash過程無法進行下去。
本文出自 “chhquan” 部落格,請務必保留此出處http://chhquan.blog.51cto.com/1346841/1827440
redis底層資料結構之dict 字典2