memcached原始碼分析-----雜湊表基本操作以及擴容過程

FreeeLinux發表於2017-02-01

 轉載請註明出處:http://blog.csdn.net/luotuo44/article/details/42773231

 

        溫馨提示:本文用到了一些可以在啟動memcached設定的全域性變數。關於這些全域性變數的含義可以參考《memcached啟動引數詳解》。對於這些全域性變數,處理方式就像《如何閱讀memcached原始碼》所說的那樣直接取其預設值

 

        assoc.c檔案裡面的程式碼是構造一個雜湊表。memcached快的一個原因是使用了雜湊表。現在就來看一下memcached是怎麼使用雜湊表的。

 

雜湊結構:

 

        main函式會呼叫assoc_init函式申請並初始化雜湊表。為了減少雜湊表發生衝突的可能性,memcached的雜湊表是比較長的,並且雜湊表的長度為2的冪。全域性變數hashpower用來記錄2的冪次。main函式呼叫assoc_init函式時使用全域性變數settings.hashpower_init作為引數,用於指明雜湊表初始化時的冪次。settings.hashpower_init可以在啟動memcached的時候設定,具體可以參考《memcached啟動引數詳解以及關鍵配置的預設值》。

 

//memcached.h檔案  
#define HASHPOWER_DEFAULT 16  
  
//assoc.h檔案  
unsigned int hashpower = HASHPOWER_DEFAULT;  
  
#define hashsize(n) ((ub4)1<<(n))//這裡是1 左移 n次  
//hashsize(n)為2的冪,所以hashmask的值的二進位制形式就是後面全為1的數。這就很像位操作裡面的 &   
//value & hashmask(n)的結果肯定是比hashsize(n)小的一個數字.即結果在hash表裡面  
//hashmask(n)也可以稱為雜湊掩碼  
#define hashmask(n) (hashsize(n)-1)  
  
//雜湊表陣列指標  
static item** primary_hashtable = 0;  
  
  
//預設引數值為0。本函式由main函式呼叫,引數的預設值為0  
void assoc_init(const int hashtable_init) {  
    if (hashtable_init) {  
        hashpower = hashtable_init;  
    }  
  
    //因為雜湊表會慢慢增大,所以要使用動態記憶體分配。雜湊表儲存的資料是一個  
    //指標,這樣更省空間。  
    //hashsize(hashpower)就是雜湊表的長度了  
    primary_hashtable = calloc(hashsize(hashpower), sizeof(void *));  
    if (! primary_hashtable) {  
        fprintf(stderr, "Failed to init hashtable.\n");  
        exit(EXIT_FAILURE);//雜湊表是memcached工作的基礎,如果失敗只能退出執行  
    }  
      
}  


        說到雜湊表,那麼就對應有兩個問題:雜湊演算法,怎麼解決衝突。

        對於雜湊函式(演算法),memcached直接使用開源的MurmurHash3和jenkins_hash兩個中的一個。預設是使用jenkins,可以在啟動memcached的時候設定設定為MurmurHash3。memcached是直接把客戶端輸入的鍵值作為雜湊演算法的輸入,得到一個32位的無符號整型輸出(用變數hv儲存)。因為雜湊表的長度沒有2^32- 1這麼大,所以需要一個函式將hv對映在雜湊表的範圍之內。memcached採用了最簡單的取模運算作為對映函式,即hv%hashsize(hashpower)。對於CPU而言,取模運算是一個比較耗時的操作。所以memcached利用雜湊表的長度是2的冪的性質,採用位操作進行優化,即: hv & hashmask(hashpower)。因為對雜湊表進行增刪查操作都需要定位,所以經常本文的程式碼中經常會出現hv & hashmask(hashpower)。

        memcached使用最常見的鏈地址法解決衝突問題。從前面的程式碼可以看到,primary_hashtable是一個的二級指標變數,它指向的是一個一維指標陣列,陣列的每一個元素指向一條連結串列(連結串列上的item節點具有相同的雜湊值)。陣列的每一個元素,在memcached裡面也稱為桶(bucket),所以後文的表述中會使用桶。下圖是一個雜湊表,其中第0號桶有2個item,第2、3、5號桶各有一個item。item就是用來儲存使用者資料的結構體。

        


 

基本操作:

 

插入item:

 

        接著看一下怎麼在雜湊表中插入一個item。它是直接根據雜湊值找到雜湊表中的位置(即找到對應的桶),然後使用頭插法插入到桶的衝突鏈中。item結構體有一個專門的h_next指標成員變數用於連線雜湊衝突鏈。

static unsigned int hash_items = 0;//hash表中item的個數  
  
/* Note: this isn't an assoc_update.  The key must not already exist to call this */  
//hv是這個item鍵值的雜湊值  
int assoc_insert(item *it, const uint32_t hv) {  
    unsigned int oldbucket;  
  
    //使用頭插法 插入一個item  
    //第一次看本函式,直接看else部分  
    if (expanding &&  
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)  
    {  
        ...  
    } else {  
        //使用頭插法插入雜湊表中  
        it->h_next = primary_hashtable[hv & hashmask(hashpower)];  
        primary_hashtable[hv & hashmask(hashpower)] = it;  
    }  
  
    hash_items++;//雜湊表的item數量加一  
    …  
    return 1;  
}  



查詢item:

 

        往雜湊表插入item後,就可以開始查詢item了。下面看一下怎麼在雜湊表中查詢一個item。item的鍵值hv只能定位到雜湊表中的桶位置,但一個桶的衝突鏈上可能有多個item,所以除了查詢的時候除了需要hv外還需要item的鍵值。

//由於雜湊值只能確定是在雜湊表中的哪個桶(bucket),但一個桶裡面是有一條衝突鏈的  
//此時需要用到具體的鍵值遍歷並一一比較衝突鏈上的所有節點。雖然key是以'\0'結尾  
//的字串,但呼叫strlen還是有點耗時(需要遍歷鍵值字串)。所以需要另外一個引數  
//nkey指明這個key的長度  
item *assoc_find(const char *key, const size_t nkey, const uint32_t hv) {  
    item *it;  
    unsigned int oldbucket;  
  
    //直接看else部分  
    if (expanding &&  
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)  
    {  
        it = old_hashtable[oldbucket];  
    } else {  
        //由雜湊值判斷這個key是屬於那個桶(bucket)的  
        it = primary_hashtable[hv & hashmask(hashpower)];  
    }  
  
    //到這裡,已經確定這個key是屬於那個桶的。 遍歷對應桶的衝突鏈即可  
  
    item *ret = NULL;  
    while (it) {  
        //長度相同的情況下才呼叫memcmp比較,更高效  
        if ((nkey == it->nkey) && (memcmp(key, ITEM_key(it), nkey) == 0)) {  
            ret = it;  
            break;  
        }  
        it = it->h_next;  
    }  
    return ret;  
}  



刪除item:

 

        下面看一下從雜湊表中刪除一個item是怎麼實現的。從連結串列中刪除一個節點的常規做法是:先找到這個節點的前驅節點,然後使用前驅節點的next指標進行刪除和拼接操作。memcached的做法差不多,實現如下:

void assoc_delete(const char *key, const size_t nkey, const uint32_t hv) {  
    item **before = _hashitem_before(key, nkey, hv);//得到前驅節點的h_next成員地址  
  
    if (*before) {//查詢成功  
        item *nxt;  
        hash_items--;  
        //因為before是一個二級指標,其值為所查詢item的前驅item的h_next成員地址.  
        //所以*before指向的是所查詢的item.因為before是一個二級指標,所以  
        //*before作為左值時,可以給h_next成員變數賦值。所以下面三行程式碼是  
        //使得刪除中間的item後,前後的item還能連得起來。  
        nxt = (*before)->h_next;  
        (*before)->h_next = 0;   /* probably pointless, but whatever. */  
        *before = nxt;  
        return;  
    }  
    /* Note:  we never actually get here.  the callers don't delete things 
       they can't find. */  
    assert(*before != 0);  
}  
  
  
  
//查詢item。返回前驅節點的h_next成員地址,如果查詢失敗那麼就返回衝突鏈中最後  
//一個節點的h_next成員地址。因為最後一個節點的h_next的值為NULL。通過對返回值  
//使用 * 運算子即可知道有沒有查詢成功。  
static item** _hashitem_before (const char *key, const size_t nkey, const uint32_t hv) {  
    item **pos;  
    unsigned int oldbucket;  
  
    //同樣,看的時候直接跳到else部分  
    if (expanding &&//正在擴充套件雜湊表  
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)  
    {  
        pos = &old_hashtable[oldbucket];  
    } else {  
        //找到雜湊表中對應的桶位置  
        pos = &primary_hashtable[hv & hashmask(hashpower)];  
    }  
  
    //遍歷桶的衝突鏈查詢item  
    while (*pos && ((nkey != (*pos)->nkey) || memcmp(key, ITEM_key(*pos), nkey))) {  
        pos = &(*pos)->h_next;  
    }  
    //*pos就可以知道有沒有查詢成功。如果*pos等於NULL那麼查詢失敗,否則查詢成功。  
    return pos;  
}  


 

擴充套件雜湊表:

        當雜湊表中item的數量達到了雜湊表表長的1.5倍時,那麼就會擴充套件雜湊表增大雜湊表的表長。memcached在插入一個item時會檢查當前的item總數是否達到了雜湊表表長的1.5倍。由於item的雜湊值是比較均勻的,所以平均來說每個桶的衝突鏈長度大概就是1.5個節點。所以memcached的雜湊查詢還是很快的。

 

遷移執行緒:

 

        擴充套件雜湊表有一個很大的問題:擴充套件後雜湊表的長度變了,item雜湊後的位置也是會跟著變化的(回憶一下memcached是怎麼根據鍵值的雜湊值確定桶的位置的)。所以如果要擴充套件雜湊表,那麼就需要對雜湊表中所有的item都要重新計算雜湊值得到新的雜湊位置(桶位置),然後把item遷移到新的桶上。對所有的item都要做這樣的處理,所以這必然是一個耗時的操作。後文會把這個操作稱為資料遷移。

        因為資料遷移是一個耗時的操作,所以這個工作由一個專門的執行緒(姑且把這個執行緒叫做遷移執行緒吧)負責完成。這個遷移執行緒是由main函式呼叫一個函式建立的。看下面程式碼:

#define DEFAULT_HASH_BULK_MOVE 1  
int hash_bulk_move = DEFAULT_HASH_BULK_MOVE;  
  
//main函式會呼叫本函式,啟動資料遷移執行緒  
int start_assoc_maintenance_thread() {  
    int ret;  
    char *env = getenv("MEMCACHED_HASH_BULK_MOVE");  
    if (env != NULL) {  
        //hash_bulk_move的作用在後面會說到。這裡是通過環境變數給hash_bulk_move賦值  
        hash_bulk_move = atoi(env);  
        if (hash_bulk_move == 0) {  
            hash_bulk_move = DEFAULT_HASH_BULK_MOVE;  
        }  
    }  
    if ((ret = pthread_create(&maintenance_tid, NULL,  
                              assoc_maintenance_thread, NULL)) != 0) {  
        fprintf(stderr, "Can't create thread: %s\n", strerror(ret));  
        return -1;  
    }  
    return 0;  
}  

 

        遷移執行緒被建立後會進入休眠狀態(通過等待條件變數),當worker執行緒插入item後,發現需要擴充套件雜湊表就會呼叫assoc_start_expand函式喚醒這個遷移執行緒。

static bool started_expanding = false;  
  
//assoc_insert函式會呼叫本函式,當item數量到了雜湊表表長的1.5倍才會呼叫的  
static void assoc_start_expand(void) {  
    if (started_expanding)  
        return;  
    started_expanding = true;  
    pthread_cond_signal(&maintenance_cond);  
}  
  
  
static bool expanding = false;//標明hash表是否處於擴充套件狀態  
static volatile int do_run_maintenance_thread = 1;  
static void *assoc_maintenance_thread(void *arg) {  
  
    //do_run_maintenance_thread是全域性變數,初始值為1,在stop_assoc_maintenance_thread  
    //函式中會被賦值0,終止遷移執行緒  
    while (do_run_maintenance_thread) {  
        int ii = 0;  
  
        //上鎖  
        item_lock_global();  
        mutex_lock(&cache_lock);  
  
        ...//進行item遷移  
  
        //遍歷完就釋放鎖  
        mutex_unlock(&cache_lock);  
        item_unlock_global();  
  
  
        if (!expanding) {//不需要遷移資料(了)。  
            /* We are done expanding.. just wait for next invocation */  
            mutex_lock(&cache_lock);  
            started_expanding = false; //重置  
  
            //掛起遷移執行緒,直到worker執行緒插入資料後發現item數量已經到了1.5倍雜湊表大小,  
            //此時呼叫worker執行緒呼叫assoc_start_expand函式,該函式會呼叫pthread_cond_signal  
            //喚醒遷移執行緒  
            pthread_cond_wait(&maintenance_cond, &cache_lock);  
            mutex_unlock(&cache_lock);  
  
            ...  
  
            mutex_lock(&cache_lock);  
            assoc_expand();//申請更大的雜湊表,並將expanding設定為true  
            mutex_unlock(&cache_lock);  
        }  
    }  
    return NULL;  
}  


逐步遷移資料:

 

        為了避免在遷移的時候worker執行緒增刪雜湊表,所以要在資料遷移的時候加鎖,worker執行緒搶到了鎖才能增刪查詢雜湊表。memcached為了實現快速響應(即worker執行緒能夠快速完成增刪查詢操作),就不能讓遷移執行緒佔鎖太久。但資料遷移本身就是一個耗時的操作,這是一個矛盾。

        memcached為了解決這個矛盾,就採用了逐步遷移的方法。其做法是,在一個迴圈裡面:加鎖-》只進行小部分資料的遷移-》解鎖。這樣做的效果是:雖然遷移執行緒會多次搶佔鎖,但每次佔有鎖的時間都是很短的,這就增加了worker執行緒搶到鎖的概率,使得worker執行緒能夠快速完成它的操作。一小部分是多少個item呢?前面說到的全域性變數hash_bulk_move就指明是多少個桶的item,預設值是1個桶,後面為了方便敘述也就認為hash_bulk_move的值為1。

        逐步遷移的具體做法是,呼叫assoc_expand函式申請一個新的更大的雜湊表,每次只遷移舊雜湊表一個桶的item到新雜湊表,遷移完一桶就釋放鎖。此時就要求有一箇舊雜湊表和新雜湊表。在memcached實現裡面,用primary_hashtable表示新表(也有一些博文稱之為主表),old_hashtable表示舊錶(副表)。

        前面說到,遷移執行緒被建立後就會休眠直到被worker執行緒喚醒。當遷移執行緒醒來後,就會呼叫assoc_expand函式擴大雜湊表的表長。assoc_expand函式如下:

static void assoc_expand(void) {  
    old_hashtable = primary_hashtable;  
  
    //申請一個新雜湊表,並用old_hashtable指向舊雜湊表  
    primary_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));  
    if (primary_hashtable) {  
  
        hashpower++;  
        expanding = true;//標明已經進入擴充套件狀態  
        expand_bucket = 0;//從0號桶開始資料遷移  
    } else {  
        primary_hashtable = old_hashtable;  
        /* Bad news, but we can keep running. */  
    }  
}  

        現在看一下完整一點的assoc_maintenance_thread執行緒函式,體會遷移執行緒是怎麼逐步資料遷移的。為什麼說完整一點呢?因為該函式裡面還是有一些東西本篇博文是沒有解釋的,但這並不妨礙我們閱讀該函式。後面還會有其他博文對這個執行緒函式進行講解的。

static unsigned int expand_bucket = 0;//指向待遷移的桶  
  
#define DEFAULT_HASH_BULK_MOVE 1  
int hash_bulk_move = DEFAULT_HASH_BULK_MOVE;  
  
static volatile int do_run_maintenance_thread = 1;  
  
static void *assoc_maintenance_thread(void *arg) {  
  
    //do_run_maintenance_thread是全域性變數,初始值為1,在stop_assoc_maintenance_thread  
    //函式中會被賦值0,終止遷移執行緒  
    while (do_run_maintenance_thread) {  
        int ii = 0;  
  
        //上鎖  
        item_lock_global();  
        mutex_lock(&cache_lock);  
  
        //hash_bulk_move用來控制每次遷移,移動多少個桶的item。預設是一個.  
        //如果expanding為true才會進入迴圈體,所以遷移執行緒剛建立的時候,並不會進入迴圈體  
        for (ii = 0; ii < hash_bulk_move && expanding; ++ii) {  
            item *it, *next;  
            int bucket;  
  
            //在assoc_expand函式中expand_bucket會被賦值0  
            //遍歷舊雜湊表中由expand_bucket指明的桶,將該桶的所有item  
            //遷移到新雜湊表中。  
            for (it = old_hashtable[expand_bucket]; NULL != it; it = next) {  
                next = it->h_next;  
  
                //重新計算新的雜湊值,得到其在新雜湊表的位置  
                bucket = hash(ITEM_key(it), it->nkey) & hashmask(hashpower);  
  
                //將這個item插入到新雜湊表中  
                it->h_next = primary_hashtable[bucket];  
                primary_hashtable[bucket] = it;  
            }  
  
            //不需要清空舊桶。直接將衝突鏈的鏈頭賦值為NULL即可  
            old_hashtable[expand_bucket] = NULL;  
  
            //遷移完一個桶,接著把expand_bucket指向下一個待遷移的桶  
            expand_bucket++;  
              
            if (expand_bucket == hashsize(hashpower - 1)) {//全部資料遷移完畢  
                expanding = false; //將擴充套件標誌設定為false  
                free(old_hashtable);  
            }  
        }  
  
        //遍歷完hash_bulk_move個桶的所有item後,就釋放鎖  
        mutex_unlock(&cache_lock);  
        item_unlock_global();  
  
  
        if (!expanding) {//不再需要遷移資料了。  
            /* finished expanding. tell all threads to use fine-grained(細粒度的) locks */  
            //進入到這裡,說明已經不需要遷移資料(停止擴充套件了)。  
  
            ...  
            mutex_lock(&cache_lock);  
            started_expanding = false; //重置  
  
            //掛起遷移執行緒,直到worker執行緒插入資料後發現item數量已經到了1.5倍雜湊表大小,  
            //此時呼叫worker執行緒呼叫assoc_start_expand函式,該函式會呼叫pthread_cond_signal  
            //喚醒遷移執行緒  
            pthread_cond_wait(&maintenance_cond, &cache_lock);  
            /* Before doing anything, tell threads to use a global lock */  
            mutex_unlock(&cache_lock);  
  
            ...  
            mutex_lock(&cache_lock);  
            assoc_expand();//申請更大的雜湊表,並將expanding設定為true  
            mutex_unlock(&cache_lock);  
        }  
    }  
    return NULL;  
}  


回馬槍:

 

        現在再回過頭來再看一下雜湊表的插入、刪除和查詢操作,因為這些操作可能發生在雜湊表遷移階段。有一點要注意,在assoc.c檔案裡面的插入、刪除和查詢操作,是看不到加鎖操作的。但前面已經說了,需要和遷移執行緒搶佔鎖,搶到了鎖才能進行對應的操作。其實,這鎖是由插入、刪除和查詢的呼叫者(主調函式)負責加的,所以在程式碼裡面看不到。

        因為插入的時候可能雜湊表正在擴充套件,所以插入的時候要面臨一個選擇:插入到新表還是舊錶?memcached的做法是:當item對應在舊錶中的桶還沒被遷移到新表的話,就插入到舊錶,否則插入到新表。下面是插入部分的程式碼。

/* Note: this isn't an assoc_update.  The key must not already exist to call this */  
//hv是這個item鍵值的雜湊值  
int assoc_insert(item *it, const uint32_t hv) {  
    unsigned int oldbucket;  
  
  
    //使用頭插法 插入一個item  
    if (expanding &&//目前處於擴充套件hash表狀態  
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)//資料遷移時還沒遷移到這個桶  
    {  
        //插入到舊錶  
        it->h_next = old_hashtable[oldbucket];  
        old_hashtable[oldbucket] = it;  
    } else {  
        //插入到新表  
        it->h_next = primary_hashtable[hv & hashmask(hashpower)];  
        primary_hashtable[hv & hashmask(hashpower)] = it;  
    }  
  
    hash_items++;//雜湊表的item數量加一  
    //當hash表的item數量到達了hash表容量的1.5倍時,就會進行擴充套件  
    //當然如果現在正處於擴充套件狀態,是不會再擴充套件的  
    if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {  
        assoc_start_expand();//喚醒遷移執行緒,擴充套件雜湊表  
    }  
  
    return 1;  
}  


        這裡有一個疑問,為什麼不直接插入到新表呢?直接插入到新表對於資料一致性來說完全是沒有問題的啊。網上有人說是為了保證同一個桶item的順序,但由於遷移執行緒和插入執行緒對於鎖搶佔的不確定性,任何順序都不能通過assoc_insert函式來保證。本文認為是為了快速查詢。如果是直接插入到新表,那麼在查詢的時候就可能要同時查詢新舊兩個表才能找到item。查詢完一個表,發現沒有,然後再去查詢另外一個表,這樣的查詢被認為是不夠快速的。

        如果按照assoc_insert函式那樣的實現,不用查詢兩個表就能找到item。看下面的查詢函式。

//由於雜湊值只能確定是在雜湊表中的哪個桶(bucket),但一個桶裡面是有一條衝突鏈的  
//此時需要用到具體的鍵值遍歷並一一比較衝突鏈上的所有節點。因為key並不是以'\0'結尾  
//的字串,所以需要另外一個引數nkey指明這個key的長度  
item *assoc_find(const char *key, const size_t nkey, const uint32_t hv) {  
    item *it;  
    unsigned int oldbucket;  
  
    if (expanding &&//正在擴充套件雜湊表  
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)//該item還在舊錶裡面  
    {  
        it = old_hashtable[oldbucket];  
    } else {  
        //由雜湊值判斷這個key是屬於那個桶(bucket)的  
        it = primary_hashtable[hv & hashmask(hashpower)];  
    }  
  
    //到這裡已經確定了要查詢的item是屬於哪個表的了,並且也確定了桶位置。遍歷對應桶的衝突鏈即可  
  
    item *ret = NULL;  
    while (it) {  
        //長度相同的情況下才呼叫memcmp比較,更高效  
        if ((nkey == it->nkey) && (memcmp(key, ITEM_key(it), nkey) == 0)) {  
            ret = it;  
            break;  
        }  
        it = it->h_next;  
    }  
  
    return ret;  
}  


        刪除操作和查詢操作差不多,這裡直接貼出,不多說了。刪除操作也是要進行查詢操作的。

void assoc_delete(const char *key, const size_t nkey, const uint32_t hv) {  
    item **before = _hashitem_before(key, nkey, hv);//得到前驅節點的h_next成員地址  
  
    if (*before) {//查詢成功  
        item *nxt;  
        hash_items--;  
  
        //因為before是一個二級指標,其值為所查詢item的前驅item的h_next成員地址.  
        //所以*before指向的是所查詢的item.因為before是一個二級指標,所以  
        //*before作為左值時,可以給h_next成員變數賦值。所以下面三行程式碼是  
        //使得刪除中間的item後,前後的item還能連得起來。  
        nxt = (*before)->h_next;  
        (*before)->h_next = 0;   /* probably pointless, but whatever. */  
        *before = nxt;  
        return;  
    }  
    /* Note:  we never actually get here.  the callers don't delete things 
       they can't find. */  
    assert(*before != 0);  
}  
  
  
//查詢item。返回前驅節點的h_next成員地址,如果查詢失敗那麼就返回衝突鏈中最後  
//一個節點的h_next成員地址。因為最後一個節點的h_next的值為NULL。通過對返回值  
//使用 * 運算子即可知道有沒有查詢成功。  
static item** _hashitem_before (const char *key, const size_t nkey, const uint32_t hv) {  
    item **pos;  
    unsigned int oldbucket;  
  
    if (expanding &&//正在擴充套件雜湊表  
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)  
    {  
        pos = &old_hashtable[oldbucket];  
    } else {  
        //找到雜湊表中對應的桶位置  
        pos = &primary_hashtable[hv & hashmask(hashpower)];  
    }  
  
    //到這裡已經確定了要查詢的item是屬於哪個表的了,並且也確定了桶位置。遍歷對應桶的衝突鏈即可  
      
    //遍歷桶的衝突鏈查詢item  
    while (*pos && ((nkey != (*pos)->nkey) || memcmp(key, ITEM_key(*pos), nkey))) {  
        pos = &(*pos)->h_next;  
    }  
    //*pos就可以知道有沒有查詢成功。如果*pos等於NULL那麼查詢失敗,否則查詢成功。  
    return pos;  
}  


        由上面的討論可以知道,插入和刪除一個item都必須知道這個item對應的桶有沒有被遷移到新表上了。

相關文章