redis底層資料結構之dict 字典2

來源:互聯網
上載者:User

標籤: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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.