一對一聊天原始碼,你是否瞭解ERedis的擴容機制?

云豹科技-苏凌霄發表於2024-06-22

一對一聊天原始碼,你是否瞭解ERedis的擴容機制?

Redis的擴容時機

Redis會在如下兩種情況觸發擴容。

1、如果沒有fork子程序在執行RDB或者AOF的持久化,一旦滿足ht[0].used >= ht[0].size,此時觸發擴容;
2、如果有fork子程序在執行RDB或者AOF的持久化時,則需要滿足ht[0].used > 5 * ht[0].size,此時觸發擴容。

下面將結合原始碼對Redis的擴容時機進行學習。當向dict新增或者更新資料時,對應的方法是位於dict.c檔案中的dictReplace()方法,如下所示。

int dictReplace(dict *d, void *key, void *val) {
    dictEntry *entry, *existing, auxentry;

    // 如果新增成功,dictAddRaw()方法會把成功新增的dictEntry返回
    // 返回的dictEntry只設定了鍵,值需要在這裡呼叫dictSetVal()方法進行設定
    entry = dictAddRaw(d,key,&existing);
    if (entry) {
        dictSetVal(d, entry, val);
        return 1;
    }

    // 執行到這裡,表明在雜湊表中已經存在一個dictEntry的鍵與當前待新增的鍵值對的鍵相等
    // 此時應該做更新值的操作,且existing此時是指向這個已經存在的dictEntry
    auxentry = *existing;
    // 更新值,即為existing指向的dictEntry設定新值
    dictSetVal(d, existing, val);
    // 釋放舊值
    dictFreeVal(d, &auxentry);
    return 0;
}

dictReplace()方法會執行鍵值對的新增或更新,如果雜湊表中不存在dictEntry的鍵與待新增鍵值對的鍵相等,此時會基於待新增鍵值對新建立一個dictEntry並以頭插法插入雜湊表中,此時返回1;如果雜湊表中存在dictEntry的鍵與待新增鍵值對的鍵相等,此時就為已經存在的dictEntry設定新值並釋放舊值,然後返回0。通常要觸發擴容,觸發時機一般在新增鍵值對的時候,所以繼續分析dictAddRaw()方法,其原始碼實現如下所示。

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) {
    long index;
    dictEntry *entry;
    dictht *ht;

    // 判斷是否在進行rehash,如果正在進行rehash,則觸發漸進式rehash
    // dictIsRehashing()方法在dict.h檔案中,如果dict的rehashidx不等於-1,則表明此時在進行rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 獲取待新增鍵值對在雜湊表中的索引index
    // 如果雜湊表已經存在dictEntry的鍵與待新增鍵值對的鍵相等,此時_dictKeyIndex()方法返回-1
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;
    
    // 如果在進行rehash,待新增的鍵值對存放到ht[1],否則存放到ht[0]
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    // 為新dictEntry開闢記憶體,此時dictEntry的鍵和值尚未設定
    entry = zmalloc(sizeof(*entry));
    // 頭插的方式插入雜湊表index位置的雜湊桶中
    entry->next = ht->table[index];
    ht->table[index] = entry;
    // 雜湊表的當前大小加1
    ht->used++;

    // 為新dictEntry設定鍵
    dictSetKey(d, entry, key);
    return entry;
}

dictAddRaw()方法會首先判斷當前是否處於rehash階段(判斷當前是否正在擴容),如果正在rehash,則觸發一次雜湊桶的遷移操作(這一點後面再詳細分析),然後透過_dictKeyIndex()方法獲取待新增鍵值對在雜湊表中的索引index,如果獲取到的index為-1,表明存在dictEntry的鍵與待新增鍵值對的鍵相等,此時dictAddRaw()方法返回NULL以告訴方法呼叫方需要執行更新操作,如果index不為-1,則基於待新增鍵值對建立新的dictEntry並以頭插的方式插入雜湊表index位置的雜湊桶中,然後更新雜湊表的當前大小以及為新dictEntry設定鍵。擴容的觸發在_dictKeyIndex()方法中,其原始碼實現如下所示。

static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing) {
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    // 在_dictExpandIfNeeded()方法中判斷是否需要擴容,如果需要,則執行擴容
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        // 將待新增鍵值對的鍵的hash值與雜湊表掩碼相與以得到待新增鍵值對在雜湊表中的索引
        idx = hash & d->ht[table].sizemask;
        // 遍歷雜湊表中idx索引位置的雜湊桶的連結串列
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                // 連結串列中存在dictEntry的key與待新增鍵值對的key相等
                // 此時讓existing指向這個已經存在的dictEntry,並返回-1
                // 表明存在dictEntry的key與待新增鍵值對的key相等且existing已經指向了這個存在的dictEntry
                if (existing) *existing = he;
                return -1;
            }
            // 繼續遍歷連結串列中的下一個節點
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    // 執行到這裡,表明雜湊表中不存在dictEntry的key與待新增鍵值對的key相等,返回索引idx
    return idx;
}

在_dictKeyIndex()方法的一開始,就會呼叫_dictExpandIfNeeded()方法來判斷是否需要擴容,如果需要,則會執行擴容邏輯,假如_dictExpandIfNeeded()方法在擴容過程中出現錯誤,會返回狀態碼1,也就是DICT_ERR欄位表示的值,此時_dictKeyIndex()方法直接返回-1,如果不需要擴容或者擴容成功,則將待新增鍵值對的鍵的hash值與雜湊表掩碼相與得到待新增鍵值對在雜湊表中的索引,然後遍歷索引位置的雜湊桶的連結串列,看是否能夠找到一個dictEntry的鍵與待新增鍵值對的鍵相等,如果能夠找到一個這樣的dictEntry,則返回-1並讓existing指向這個dictEntry,否則返回之前計算得到的索引。可知判斷擴容以及執行擴容的邏輯都在_dictExpandIfNeeded()方法中,其原始碼實現如下所示。

static int _dictExpandIfNeeded(dict *d) {
    // 如果已經在擴容,則返回狀態碼0
    if (dictIsRehashing(d)) return DICT_OK;

    // 如果雜湊表的容量為0,則初始化雜湊表的容量為4
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    // 如果雜湊表的當前大小大於等於容量,並且dict_can_resize為1或者當前大小大於容量的五倍
    // 此時判斷需要擴容,呼叫dictExpand()方法執行擴容邏輯,且指定擴容後的容量至少為當前大小的2倍
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) {
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

透過_dictExpandIfNeeded()方法的原始碼可知,要觸發擴容,首先需要滿足的條件就是雜湊表當前大小大於等於了雜湊表的容量,然後再判斷Redis當前是否允許擴容,如果允許擴容,則執行擴容邏輯,如果不允許擴容,那麼再判斷雜湊表當前大小是否已經大於了雜湊表容量的5倍,如果已經大於,則強制執行擴容邏輯。在_dictExpandIfNeeded()方法有兩個重要的引數,分別是dict_can_resize和dict_force_resize_ratio,這兩個引數定義在dict.c檔案中,初始值如下所示。

static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;

那麼現在最後還需要結合原始碼分析一下,什麼時候會更改dict_can_resize的值,在dict.c檔案中有如下兩個方法,會將dict_can_resize的值設定為1或者0,如下所示。

void dictEnableResize(void) {
    dict_can_resize = 1;
}

void dictDisableResize(void) {
    dict_can_resize = 0;
}

這兩個方法會被server.c檔案中的updateDictResizePolicy()方法呼叫,如下所示。

void updateDictResizePolicy(void) {
    // 如果有正在執行RDB或AOF持久化的子程序,hasActiveChildProcess()方法返回true
    if (!hasActiveChildProcess())
        // 沒有正在執行RDB或AOF持久化的子程序時將dict_can_resize設定為1,表示允許擴容
        dictEnableResize();
    else
        // 有正在執行RDB或AOF持久化的子程序時將dict_can_resize設定為0,表示不允許擴容
        dictDisableResize();
}

至此Redis的擴容時機的原始碼分析就到此為止,現在進行小節:當向Redis新增或者更新資料時,會判斷一下儲存資料的雜湊表的當前大小是否大於等於雜湊表容量,如果大於,再判斷Redis是否允許擴容,而決定Redis是否允許擴容的依據就是當前是否存在子執行緒在執行RDB或者AOF的持久化,如果存在就不允許擴容,反之則允許擴容,假如Redis不允許擴容,那麼還需要判斷一下是否要強制擴容,判斷依據就是儲存資料的雜湊表的當前大小是否已經大於雜湊表容量的5倍,如果大於,則強制擴容。

下面給出流程圖,對上述整個觸發擴容的原始碼流程進行示意。


以上就是一對一聊天原始碼,你是否瞭解Redis的擴容機制?, 更多內容歡迎關注之後的文章

相關文章