一對一直播平臺原始碼,該擴容時就擴容

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

一對一直播平臺原始碼,該擴容時就擴容

Redis的擴容步驟

當一旦需要進行擴容時,此時會使用到dict中的ht[1],一對一直播平臺原始碼的Redis的擴容步驟如下所示。

1、計算ht[1]的容量size,即擴容後的容量,ht[1]的容量為大於等於ht[0].used * 2且同時為2的冪次方的最小值;
2、為ht[1]設定size,sizemask欄位的值,初始化used欄位為0,併為dictEntry陣列分配空間;
3、將dict的rehashidx欄位設定為0,表示此時開啟漸進式rehash,Redis會透過漸進式rehash的方式逐步將ht[0]上的dictEntry遷移到ht[1]上;
4、當ht[0]的所有鍵值對全部存放到ht[1]中後,釋放ht[0]的記憶體空間,然後ht[1]變為ht[0]。

特別注意,上述的步驟僅針對正常的擴容,如果是ht[0]的初始化,則與上述的步驟稍有不同,這裡不再贅述。當dict中鍵值對特別多時,rehash會特別耗時,所以Redis採用一種漸進式rehash的方式來完成擴容,dict中的rehashidx欄位用於記錄當前已經rehash到的雜湊桶的索引,而漸進式rehash就是Redis不會一次性將ht[0]上的鍵值對遷移到ht[1]上,而是會在某些時間點遷移一部分,這些時間點如下所示。

1、當對資料進行增刪改查時會從ht[0]遷移一個雜湊桶到ht[1]上;
2、Redis會定時的從ht[0]遷移一部分雜湊桶到ht[1]上。

特別注意,如果在漸進式rehash的過程中有新的鍵值對新增,那麼會直接新增到ht[1]中。

下面將結合Redis原始碼對一對一直播平臺原始碼中Redis的擴容步驟進行學習。在第一節中已知,執行擴容邏輯的方法是dict.c檔案的dictExpand()方法,其原始碼實現如下所示。

int dictExpand(dict *d, unsigned long size) {
    // 如果正在rehash或者ht[0]的當前大小大於了擴容後的大小的最小值
    // 此時返回狀態碼1,表示擴容發生異常
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    // n就是擴容後的雜湊表
    dictht n;
    // 得到一個大於等於size的滿足2的冪次方的最小值作為n的容量
    unsigned long realsize = _dictNextPower(size);

    // 如果擴容後的雜湊表的容量與老雜湊表容量一樣
    // 此時返回狀態碼1,表示擴容發生異常
    if (realsize == d->ht[0].size) return DICT_ERR;

    // 為n設定容量size
    n.size = realsize;
    // 為n設定掩碼sizemask
    n.sizemask = realsize-1;
    // 為n的陣列分配空間
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    // 初始化n的當前大小used為0
    n.used = 0;

    // 如果是初始化雜湊表,那麼直接將ht[0]置為n
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    // 執行到這裡,表明是非初始化雜湊表的擴容,將ht[1]置為n
    d->ht[1] = n;
    // 將dict的rehashidx欄位設定為0,表示開啟漸進式rehash
    d->rehashidx = 0;
    return DICT_OK;
}

dictExpand()方法主要完成的邏輯就是為ht[1]設定size,sizemask欄位的值,初始化used欄位為0,併為dictEntry陣列分配空間,最後將dict的rehashidx欄位設定為0以開啟漸進式rehash。下面再結合原始碼看一下什麼時候進行鍵值對的遷移,首先在第一節中分析dictAddRaw()方法時有提到,dictAddRaw()方法一開始就會判斷當前是否處於rehash階段,如果正在rehash,則觸發一次雜湊桶的遷移操作,這個遷移操作對應的方法是dict.c檔案的_dictRehashStep()方法,其原始碼實現如下。

static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

繼續看dictRehash()方法的實現。

// 引數n表示本次rehash需要遷移的雜湊桶個數
int dictRehash(dict *d, int n) {
    // 允許遍歷的最大空桶數
    int empty_visits = n*10;
    // 如果沒有在進行漸進式rehash,則返回
    if (!dictIsRehashing(d)) return 0;

    // 在ht[0]當前大小不為0的前提下
    // 需要遷移多少個雜湊桶,就迴圈多少次,每次迴圈遷移一個雜湊桶
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        // rehashidx的值不能大於等於ht[0]的容量
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        // 如果雜湊表ht[0]的rehashidx位置的雜湊桶是空,則繼續遍歷下一個雜湊桶
        // 如果遍歷的空桶數達到了empty_visits,則本次rehash結束,直接返回
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        // 得到ht[0]的rehashidx位置的雜湊桶
        de = d->ht[0].table[d->rehashidx];
        // 遍歷並將rehashidx位置的雜湊桶的連結串列上的節點全部遷移到ht[1]上
        while(de) {
            uint64_t h;

            nextde = de->next;
            // 將連結串列節點的鍵的hash值與ht[1]的掩碼相與得到當前節點在ht[1]上的索引
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            // 使用頭插法插入ht[1]
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            // ht[0]的當前大小減1
            d->ht[0].used--;
            // ht[1]的當前大小加1
            d->ht[1].used++;
            // 繼續遷移連結串列的下一節點
            de = nextde;
        }
        // 全部遷移完成後,將ht[0]的rehashidx位置置為空
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    // 判斷是否將ht[0]上的鍵值對全部遷移到了ht[1]
    if (d->ht[0].used == 0) {
        // 如果ht[0]上的鍵值對全部遷移到了ht[1]
        // 先釋放ht[0]的陣列空間
        zfree(d->ht[0].table);
        // 然後將ht[0]置為ht[1]
        d->ht[0] = d->ht[1];
        // 重置ht[1]
        // 即將ht[1]的陣列置為空,容量,掩碼和當前大小全部置為0
        _dictReset(&d->ht[1]);
        // 將dict的rehashidx欄位設定為-1,表示rehash結束
        d->rehashidx = -1;
        return 0;
    }

    return 1;
}

dictRehash()方法有兩個引數,第一個是需要進行rehash的dict,第二個是需要遷移的雜湊桶的個數,可知如果是對一對一直播平臺原始碼的資料的增刪改查而觸發的rehash,需要遷移的雜湊桶的個數為1。在dictRehash()方法一開始就定義了一個最大空桶數,其值為本次遷移數的10倍,因為在遍歷雜湊表時,可能會遇到很多空桶,所以為了避免遍歷大量空桶而帶來的時間消耗,Redis規定本次rehash遷移時,如果遇到的空桶數達到了本次需要遷移的雜湊桶數的10倍,則停止遷移並返回。在dictRehash()方法中對於每一個雜湊桶的遷移,其實就是遍歷這個雜湊桶上的連結串列,將每個連結串列節點重新基於ht[1]計算一個索引並遷移到ht[1]上。在dictRehash()方法的最後需要判斷一下是否將ht[0]上的資料全部遷移到了ht[1]上,如果已經全部完成遷移,此時需要先釋放老的ht[0]的陣列空間,然後將ht[0]置為ht[1],接著重置ht[1]即將其陣列置為空,容量,掩碼和當前大小全部置為0,最後將dict的rehashidx欄位設定為-1,表示rehash結束。

除了對一對一直播平臺原始碼的資料的增刪改查會呼叫到dictRehash()方法來遷移雜湊桶外,Redis也會定時的呼叫到dictRehash()方法來遷移雜湊桶,這個定時任務方法是server.c檔案的serverCron()方法,在該方法中會呼叫到server.c檔案的databasesCron()方法,該方法會處理Redis資料庫中的增量執行的後臺操作,這些操作中就包括漸進式rehash,所以在databasesCron()方法中又透過呼叫server.c檔案的incrementallyRehash()方法來執行rehash,接著又在incrementallyRehash()方法中呼叫到了dict.c檔案的dictRehashMilliseconds()方法,在dictRehashMilliseconds()方法中就真正呼叫到了dictRehash()方法來執行遷移雜湊桶的邏輯,dictRehashMilliseconds()方法的原始碼實現如下所示。

int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = 0;

    // 在1毫秒的時間內迴圈進行遷移
    // 每次迴圈遷移100個雜湊桶
    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}

那麼至此,Redis的擴容步驟的原始碼就分析完畢。

以上就是一對一直播平臺原始碼,該擴容時就擴容, 更多內容歡迎關注之後的文章

相關文章