欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Redis中哈希結(jié)構(gòu)(Dict)的實現(xiàn)

 更新時間:2023年06月05日 10:16:37   作者:啊vvvv  
本文主要介紹了Redis中哈希結(jié)構(gòu)(Dict)的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

前言

哈希結(jié)構(gòu)是一個在計算機中非常常見的結(jié)構(gòu)。哈希結(jié)構(gòu)可以讓我們在O(1)時間復(fù)雜度查找元素并且對其操作,并且增刪改查性能并不會隨著數(shù)據(jù)量的增多而改變。反而數(shù)據(jù)量的增大,會出現(xiàn)兩個關(guān)鍵問題,一個是哈希沖突,另一個是rehash。而在Redis中,使用拉鏈法來解決哈希沖突,使用漸進式rehash來降低rehash的性能開銷。

Redis中的Dict結(jié)構(gòu)

在Redis 6.2.4中,dict.h是這樣定義的。

typedef struct dictEntry {
    void *key;
    //只能為其中任意的一個
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; //-1未進行rehash 
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

它們的關(guān)系是這樣的

圖片.png

什么是哈希沖突

我們把存儲數(shù)據(jù)的地方看成一個個桶(Bucket),當(dāng)數(shù)據(jù)量超出桶容量或者Hash函數(shù)給出的桶號相同的時候,便會出現(xiàn)哈希沖突。解決辦法就是,采用鏈表的方式,將同一個Bucket位置上的元素連接起來。這樣也會有一個弊端,鏈表太長,開銷又大了起來。所以必定不會無休止的鏈下去,一定要做rehash。

圖片.png

Redis的漸進式rehash

Hash 表在執(zhí)行 rehash 時,由于 Hash 表空間擴大,原本映射到某一位置的鍵可能會被映射到一個新的位置上,因此,很多鍵就需要從原來的位置拷貝到新的位置。而在鍵拷貝時,由于 Redis 主線程無法執(zhí)行其他請求,所以鍵拷貝會阻塞主線程,這樣就會產(chǎn)生 rehash 開銷。而為了降低 rehash 開銷,Redis 就提出了漸進式 rehash 的方法。觀察dict結(jié)構(gòu),它存儲了兩個相同的dictht, 在正常情況下,所有的數(shù)據(jù)都存儲在ht[0]中。在進行rehash時,會先將數(shù)據(jù)遷移到ht[1]中,等到所有數(shù)據(jù)都遷移完成時,將ht[1] 賦值給ht[0],并且釋放掉ht[0]空間。

圖片.png

rehash的觸發(fā)條件

Redis 用來判斷是否觸發(fā) rehash 的函數(shù)是**_dictExpandIfNeeded**。所以接下來我們就先看看,_dictExpandIfNeeded 函數(shù)中進行擴容的觸發(fā)條件;

//如果Hash表為空,將Hash表擴為初始大小
if (d->ht[0].size == 0) 
   return dictExpand(d, DICT_HT_INITIAL_SIZE);
//如果Hash表承載的元素個數(shù)超過其當(dāng)前大小,并且可以進行擴容,或者Hash表承載的元素個數(shù)已是當(dāng)前大小的5倍
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);
}
  • 哈希表的 LoadFactor >= 1,并且服務(wù)器沒有執(zhí)行 BGSAVE(RDB快照) 或者 BGREWRITEAOF(AOF重寫) 等后臺進程;
  • 哈希表的 LoadFactor > 5 ,表明當(dāng)前的負(fù)載太嚴(yán)重了,需要立即進行擴容;(LoadFactor = used / size)

我們再來看下 Redis 會在哪些函數(shù)中,調(diào)用 _dictExpandIfNeeded 進行判斷。通過在dict.c文件中查看 _dictExpandIfNeeded 的被調(diào)用關(guān)系,我們可以發(fā)現(xiàn),_dictExpandIfNeeded 是被 _dictKeyIndex 函數(shù)調(diào)用的,而 _dictKeyIndex 函數(shù)又會被 dictAddRaw 函數(shù)調(diào)用,然后 dictAddRaw 會被以下三個函數(shù)調(diào)用。

  • dictAdd:用來往 Hash 表中添加一個鍵值對。
  • dictReplace:用來往 Hash 表中添加一個鍵值對,或者鍵值對存在時,修改鍵值對。
  • dictAddorFind:直接調(diào)用 dictAddRaw。

因此,當(dāng)我們往 Redis 中寫入新的鍵值對或是修改鍵值對時,Redis 都會判斷下是否需要進行 rehash。

圖片.png

擴容擴多大?

在 Redis 中,rehash 對 Hash 表空間的擴容是通過調(diào)用 dictExpand 函數(shù)來完成的。dictExpand 函數(shù)的參數(shù)有兩個,一個是要擴容的 Hash 表,另一個是要擴到的容量,下面的代碼就展示了 dictExpand 函數(shù)的原型定義:

int dictExpand(dict *d, unsigned long size);

那么,對于一個 Hash 表來說,我們就可以根據(jù)前面提到的 _dictExpandIfNeeded 函數(shù),來判斷是否要對其進行擴容。而一旦判斷要擴容,Redis 在執(zhí)行 rehash 操作時,對 Hash 表擴容的思路也很簡單,就是如果當(dāng)前表的已用空間大小為 size,那么就將表擴容到 size2 的大小。

如下所示,當(dāng) _dictExpandIfNeeded 函數(shù)在判斷了需要進行 rehash 后,就調(diào)用 dictExpand 進行擴容。這里你可以看到,rehash 的擴容大小是當(dāng)前 ht[0]已使用大小的 2 倍。

dictExpand(d, d->ht[0].used*2);

而在 dictExpand 函數(shù)中,具體執(zhí)行是由 _dictNextPower 函數(shù)完成的,以下代碼顯示的 Hash 表擴容的操作,就是從 Hash 表的初始大小(DICT_HT_INITIAL_SIZE),不停地乘以 2,直到達到目標(biāo)大小。

static unsigned long _dictNextPower(unsigned long size)
{
    //哈希表的初始大小
    unsigned long i = DICT_HT_INITIAL_SIZE;
    //如果要擴容的大小已經(jīng)超過最大值,則返回最大值加1
    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    //死循環(huán)直到找到不大于的最小值
    while(1) {
        //如果擴容大小大于等于最大值,就返回截至當(dāng)前擴到的大小
        if (i >= size)
            return i;
        //每一步擴容都在現(xiàn)有大小基礎(chǔ)上乘以2
        i *= 2;
    }
}

為什么叫漸進式

漸進式 rehash 的意思就是 Redis 并不會一次性把當(dāng)前 Hash 表中的所有鍵,都拷貝到新位置,而是會分批拷貝,每次的鍵拷貝只拷貝 Hash 表中一個 bucket 中的哈希項。這樣一來,每次鍵拷貝的時長有限,對主線程的影響也就有限了。

具體過程

圖片.png

關(guān)鍵函數(shù)dictRehash部分代碼

//入?yún)ⅲ篸ict , 需要遷移的元素個數(shù)
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;
	//遷移元素,直到遷移完畢或者遷移完n個
    while(n-- && d->ht[0].used != 0) {
        //....這段代碼在下面分析
    }
    /* Check if we already rehashed the whole table... */
    //判斷遷移是否完成
    if (d->ht[0].used == 0) {
    	//釋放ht[0]
        zfree(d->ht[0].table);
        //將ht[0] 指向 ht[1]
        d->ht[0] = d->ht[1];
        //讓ht[1]重新指向null
        _dictReset(&d->ht[1]);
        //表示rehash暫停
        d->rehashidx = -1;
        //返回遷移完成
        return 0;
    }
	//還需要繼續(xù)遷移
    /* More to rehash... */
    return 1;
}

那么,每次遷移幾個元素呢?

這就要提到rehashidx了。rehashidx 變量表示的是當(dāng)前 rehash 在對哪個 bucket 做數(shù)據(jù)遷移。比如,當(dāng) rehashidx 等于 0 時,表示對 ht[0]中的第一個 bucket 進行數(shù)據(jù)遷移;當(dāng) rehashidx 等于 1 時,表示對 ht[0]中的第二個 bucket 進行數(shù)據(jù)遷移,以此類推。

而 dictRehash 函數(shù)的主循環(huán),首先會判斷 rehashidx 指向的 bucket 是否為空,如果為空,那就將 rehashidx 的值加 1,檢查下一個 bucket。

所以,漸進式 rehash 在執(zhí)行時設(shè)置了一個變量 empty_visits,用來表示已經(jīng)檢查過的空 bucket,當(dāng)檢查了一定數(shù)量的空 bucket 后,這一輪的 rehash 就停止執(zhí)行,轉(zhuǎn)而繼續(xù)處理外來請求,避免了對 Redis 性能的影響。下面的代碼顯示了這部分邏輯。

while(n-- && d->ht[0].used != 0) {
    //如果當(dāng)前要遷移的bucket中沒有元素
    while(d->ht[0].table[d->rehashidx] == NULL) {
        //
        d->rehashidx++;
        if (--empty_visits == 0) return 1;
    }
    ...
}

而如果 rehashidx 指向的 bucket 有數(shù)據(jù)可以遷移,那么 Redis 就會把這個 bucket 中的哈希項依次取出來,并根據(jù) ht[1]的表空間大小,重新計算哈希項在 ht[1]中的 bucket 位置,然后把這個哈希項賦值到 ht[1]對應(yīng) bucket 中。

這樣,每做完一個哈希項的遷移,ht[0]和 ht[1]用來表示承載哈希項多少的變量 used,就會分別減一和加一。當(dāng)然,如果當(dāng)前 rehashidx 指向的 bucket 中數(shù)據(jù)都遷移完了,rehashidx 就會遞增加 1,指向下一個 bucket。下面的代碼顯示了這一遷移過程。

 while(n-- && d->ht[0].used != 0) {
   dictEntry *de, *nextde;
    /* Note that rehashidx can't overflow as we are sure there are more
     * elements because ht[0].used != 0 */
    assert(d->ht[0].size > (unsigned long)d->rehashidx);
    while(d->ht[0].table[d->rehashidx] == NULL) {
        d->rehashidx++; 
        if (--empty_visits == 0) return 1;
    }
    de = d->ht[0].table[d->rehashidx];
    /* Move all the keys in this bucket from the old to the new hash HT */
    while(de) {
        uint64_t h;
        nextde = de->next;
        /* Get the index in the new hash table */
        h = dictHashKey(d, de->key) & d->ht[1].sizemask;
        de->next = d->ht[1].table[h];
        d->ht[1].table[h] = de;
        d->ht[0].used--;
        d->ht[1].used++;
        de = nextde;
    }
    d->ht[0].table[d->rehashidx] = NULL;
    d->rehashidx++;
}

還有一個問題,n的大小是多少?從下面這個函數(shù)可以看到,是1。每次僅僅遷移一個元素,之后變?nèi)?zhí)行主要操作。

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

看看有哪些函數(shù)調(diào)用了_dictRehashStep,Ctrl+F找一下。它們發(fā)現(xiàn)分別是:dictAddRaw,dictGenericDelete,dictFind,dictGetRandomKey,dictGetSomeKeys。其中,dictAddRaw 和 dictGenericDelete 函數(shù),分別對應(yīng)了往 Redis 中增加和刪除鍵值對,而后三個函數(shù)則對應(yīng)了在 Redis 中進行查詢操作。調(diào)用關(guān)系如下圖。

圖片.png

總結(jié)

什么是漸進式rehash?為什么要設(shè)計兩個ht?
Redis核心命令執(zhí)行是單線程的,所以一次性遷移全部數(shù)據(jù)開銷很大并且會阻塞服務(wù)。在需要rehash的時候,不會立即將全部數(shù)據(jù)進行遷移,而是通過輔助表來慢慢進行遷移,每次遷移1個元素,進行正常服務(wù)的時候,在ht[1]中進行添加操作,其他操作在ht[0]和ht[1]中一起進行。

rehashidx有什么用?
為-1的時候表示沒有rehash,為0的時候表示要遷移ht[0]中0號元素到ht[1]中,后續(xù)依次類推

rehash觸發(fā)條件?
擴容或者收縮

dict的伸縮:

  • 當(dāng)LoadFactor大于5或者LoadFactor大于1并且沒有子進程任務(wù)時,Dict擴容
  • 當(dāng)LoadFactor小于0.1時,Dict收縮
  • 擴容大小為第一個大于等于used + 1的2^n
  • 收縮大小為第一個大于等于used 的2^n
  • dict采用漸進式rehash,每次訪問Dict時執(zhí)行一次rehash

到此這篇關(guān)于Redis中哈希結(jié)構(gòu)(Dict)的實現(xiàn)的文章就介紹到這了,更多相關(guān)Redis 哈希結(jié)構(gòu) 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 高并發(fā)場景分析之redis+lua防重校驗

    高并發(fā)場景分析之redis+lua防重校驗

    這篇文章主要介紹了高并發(fā)場景分析之redis+lua防重校驗,本文通過示例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-07-07
  • SpringBoot集成Redis的思路詳解

    SpringBoot集成Redis的思路詳解

    Redis是一個開源的使用ANSI C語言編寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫,并提供多種語言的API。接下來通過本文給大家分享SpringBoot集成Redis的詳細過程,感興趣的朋友一起看看吧
    2021-10-10
  • 如何查看redis服務(wù)的版本

    如何查看redis服務(wù)的版本

    這篇文章主要介紹了如何查看redis服務(wù)的版本問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-01-01
  • Redis可視化連接服務(wù)器的方法

    Redis可視化連接服務(wù)器的方法

    這篇文章主要介紹了Redis可視化連接服務(wù)器的方法,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-03-03
  • 關(guān)于使用Redisson訂閱數(shù)問題

    關(guān)于使用Redisson訂閱數(shù)問題

    本文主要介紹了關(guān)于使用Redisson訂閱數(shù)問題,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • 一文弄懂Redis Stream消息隊列

    一文弄懂Redis Stream消息隊列

    本文主要介紹了一文弄懂Redis Stream消息隊列,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-06-06
  • Redis存取序列化與反序列化性能問題詳解

    Redis存取序列化與反序列化性能問題詳解

    這篇文章主要給大家介紹了關(guān)于Redis存取序列化與反序列化性能問題的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-12-12
  • 百行代碼實現(xiàn)基于Redis的可靠延遲隊列

    百行代碼實現(xiàn)基于Redis的可靠延遲隊列

    本文主要介紹了百行代碼實現(xiàn)基于Redis的可靠延遲隊列,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06
  • Redis客戶端連接遠程Redis服務(wù)器方式

    Redis客戶端連接遠程Redis服務(wù)器方式

    這篇文章主要介紹了Redis客戶端連接遠程Redis服務(wù)器方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-06-06
  • redis通過redis-dump鏡像實現(xiàn)數(shù)據(jù)遷移

    redis通過redis-dump鏡像實現(xiàn)數(shù)據(jù)遷移

    本文主要介紹了redis通過redis-dump鏡像實現(xiàn)數(shù)據(jù)遷移,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2025-04-04

最新評論