Redis的字典(dict)rehash過程源碼解析

來源:互聯網
上載者:User

Redis的記憶體儲存結構是個大的字典儲存,也就是我們通常說的雜湊表。Redis小到可以儲存幾萬記錄的CACHE,大到可以儲存幾千萬甚至上億的記錄(看記憶體而定),這充分說明Redis作為緩衝的強大。Redis的核心資料結構就是字典(dict),dict在資料量不斷增大的過程中,會遇到HASH(key)碰撞的問題,如果DICT不夠大,碰撞的機率增大,這樣單個hash 桶儲存的元素會越來愈多,查詢效率就會變慢。如果資料量從幾千萬變成幾萬,不斷減小的過程,DICT記憶體卻會造成不必要的浪費。Redis的dict在設計的過程中充分考慮了dict自動擴大和收縮,實現了一個稱之為rehash的過程。使dict出發rehash的條件有兩個:

1)總的元素個數 除 DICT桶的個數得到每個桶平均儲存的元素個數(pre_num),如果 pre_num > dict_force_resize_ratio,就會觸發dict 擴大操作。dict_force_resize_ratio = 5。

2)在總元素 * 10 < 桶的個數,也就是,填充率必須<10%,DICT便會進行收縮,讓total / bk_num 接近 1:1。


dict rehash擴大流程:

原始碼函數調用和解析:

dictAddRaw->_dictKeyIndex->_dictExpandIfNeeded->dictExpand,這個函數調用關係是需要擴大dict的調用關係,
_dictKeyIndex函數代碼:

static int _dictKeyIndex(dict *d, const void *key){    unsigned int h, idx, table;    dictEntry *he;    // 如果有需要,對字典進行擴充    if (_dictExpandIfNeeded(d) == DICT_ERR)        return -1;    // 計算 key 的雜湊值    h = dictHashKey(d, key);    // 在兩個雜湊表中進行尋找給定 key    for (table = 0; table <= 1; table++) {        // 根據雜湊值和雜湊表的 sizemask         // 計算出 key 可能出現在 table 數組中的哪個索引        idx = h & d->ht[table].sizemask;        // 在節點鏈表裡尋找給定 key        // 因為鏈表的元素數量通常為 1 或者是一個很小的比率        // 所以可以將這個操作看作 O(1) 來處理        he = d->ht[table].table[idx];        while(he) {            // key 已經存在            if (dictCompareKeys(d, key, he->key))                return -1;            he = he->next;        }        // 第一次進行運行到這裡時,說明已經尋找完 d->ht[0] 了        // 這時如果雜湊表不在 rehash 當中,就沒有必要尋找 d->ht[1]        if (!dictIsRehashing(d)) break;    }    return idx;}
_dictExpandIfNeeded函數代碼解析:

static int _dictExpandIfNeeded(dict *d){    // 已經在漸進式 rehash 當中,直接返回    if (dictIsRehashing(d)) return DICT_OK;    // 如果雜湊表為空白,那麼將它擴充為初始大小    // O(N)    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);    // 如果雜湊表的已用節點數 >= 雜湊表的大小,    // 並且以下條件任一個為真:    //   1) dict_can_resize 為真    //   2) 已用節點數除以雜湊表大小之比大於     //      dict_force_resize_ratio    // 那麼調用 dictExpand 對雜湊表進行擴充    // 擴充的體積至少為已使用節點數的兩倍    // O(N)    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;}

dict rehash縮小流程:


原始碼函數調用和解析:

serverCron->tryResizeHashTables->dictResize->dictExpand

serverCron函數是個心跳函數,調用tryResizeHashTables段為:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {    ....    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {        // 將雜湊表的比率維持在 1:1 附近        tryResizeHashTables();        if (server.activerehashing) incrementallyRehash(); //進行rehash動作    }    ....}
tryResizeHashTables函數程式碼分析:

void tryResizeHashTables(void) {    int j;    for (j = 0; j < server.dbnum; j++) {        // 縮小鍵空間字典        if (htNeedsResize(server.db[j].dict))            dictResize(server.db[j].dict);        // 縮小到期時間字典        if (htNeedsResize(server.db[j].expires))            dictResize(server.db[j].expires);    }}


htNeedsResize函數是判斷是否可以需要進行dict縮小的條件判斷,填充率必須>10%,否則會進行縮小,具體代碼如下:

int htNeedsResize(dict *dict) {    long long size, used;    // 雜湊表大小    size = dictSlots(dict);    // 雜湊表已用節點數量    used = dictSize(dict);    // 當雜湊表的大小大於 DICT_HT_INITIAL_SIZE     // 並且字典的填充率低於 REDIS_HT_MINFILL 時    // 返回 1    return (size && used && size > DICT_HT_INITIAL_SIZE &&            (used*100/size < REDIS_HT_MINFILL));}

dictResize函數代碼:

int dictResize(dict *d){    int minimal;    // 不能在 dict_can_resize 為假    // 或者字典正在 rehash 時調用    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;    minimal = d->ht[0].used;    if (minimal < DICT_HT_INITIAL_SIZE)        minimal = DICT_HT_INITIAL_SIZE;    return dictExpand(d, minimal);}


以上兩個過程最終調用了dictExpand函數,這個函數主要是產生一個新的HASH表(dictht),並讓將dict.rehashidx= 0。表示開始進行rehash動作。具體的rehash動作是將ht[0]的資料按照hash隱射的規則重新隱射到 ht[1]上.具體代碼如下:

int dictExpand(dict *d, unsigned long size){    dictht n; /* 被轉移資料的新hash table */        // 計算雜湊表的真實大小    unsigned long realsize = _dictNextPower(size);    if (dictIsRehashing(d) || d->ht[0].used > size || d->ht[0].size == realsize)        return DICT_ERR;    // 建立並初始化新雜湊表    n.size = realsize;    n.sizemask = realsize-1;    n.table = zcalloc(realsize*sizeof(dictEntry*));    n.used = 0;    // 如果 ht[0] 為空白,那麼這就是一次建立新雜湊表行為    // 將新雜湊表設定為 ht[0] ,然後返回    if (d->ht[0].table == NULL) {        d->ht[0] = n;        return DICT_OK;    }    /* Prepare a second hash table for incremental rehashing */    // 如果 ht[0] 不為空白,那麼這就是一次擴充字典的行為    // 將新雜湊表設定為 ht[1] ,並開啟 rehash 標識    d->ht[1] = n;    d->rehashidx = 0;    return DICT_OK;}

字典dict的rehashidx被設定成0後,就表示開始rehash動作,在心跳函數執行的過程,會檢查到這個標誌,如果需要rehash,就行進行漸進式rehash動作。函數調用的過程為:

serverCron->incrementallyRehash->dictRehashMilliseconds->dictRehash

incrementallyRehash函數代碼:

/* * 在 Redis Cron 中調用,對資料庫中第一個遇到的、可以進行 rehash 的雜湊表 * 進行 1 毫秒的漸進式 rehash */void incrementallyRehash(void) {    int j;    for (j = 0; j < server.dbnum; j++) {        /* Keys dictionary */        if (dictIsRehashing(server.db[j].dict)) {            dictRehashMilliseconds(server.db[j].dict,1);            break; /* 已經耗盡了指定的CPU毫秒數 */        }...}


dictRehashMilliseconds函數是按照指定的CPU運算的毫秒數,執行rehash動作,每次一個100個為單位執行。代碼如下:

/* * 在給定毫秒數內,以 100 步為單位,對字典進行 rehash 。 */int dictRehashMilliseconds(dict *d, int ms) {    long long start = timeInMilliseconds();    int rehashes = 0;    while(dictRehash(d,100)) {/*每次100步資料*/        rehashes += 100;        if (timeInMilliseconds()-start > ms) break; /*耗時完畢,暫停rehash*/    }    return rehashes;}
/* * 執行 N 步漸進式 rehash 。 * * 如果執行之後雜湊表還有元素需要 rehash ,那麼返回 1 。 * 如果雜湊表裡面所有元素已經遷移完畢,那麼返回 0 。 * * 每步 rehash 都會移動雜湊表數組內某個索引上的整個鏈表節點, * 所以從 ht[0] 遷移到 ht[1] 的 key 可能不止一個。 */int dictRehash(dict *d, int n) {    if (!dictIsRehashing(d)) return 0;    while(n--) {        dictEntry *de, *nextde;        // 如果 ht[0] 已經為空白,那麼遷移完畢        // 用 ht[1] 代替原來的 ht[0]        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] 的指標            _dictReset(&d->ht[1]);            // 關閉 rehash 標識            d->rehashidx = -1;            // 通知調用者, rehash 完畢            return 0;        }        assert(d->ht[0].size > (unsigned)d->rehashidx);        // 移動到數組中首個不為 NULL 鏈表的索引上        while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;        // 指向鏈表頭        de = d->ht[0].table[d->rehashidx];        // 將鏈表內的所有元素從 ht[0] 遷移到 ht[1]        // 因為桶內的元素通常只有一個,或者不多於某個特定比率        // 所以可以將這個操作看作 O(1)        while(de) {            unsigned int h;            nextde = de->next;            /* Get the index in the new hash table */            // 計算元素在 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;            // 更新計數器            d->ht[0].used--;            d->ht[1].used++;            de = nextde;        }        // 設定指標為 NULL ,方便下次 rehash 時跳過        d->ht[0].table[d->rehashidx] = NULL;        // 前進至下一索引        d->rehashidx++;    }    // 通知調用者,還有元素等待 rehash    return 1;}

總結,Redis的rehash動作是一個記憶體管理和資料管理的一個核心操作,由於Redis主要使用單線程做資料管理和訊息效應,它的rehash資料移轉過程採用的是漸進式的資料移轉模式,這樣做是為了防止rehash過程太長堵塞資料處理線程。並沒有採用memcached的多線程移轉模式。關於memcached的rehash過程,以後再做介紹。從redis的rehash過程設計的很巧,也很優雅。在這裡值得注意的是,redis在find資料的時候,是同時尋找正在遷移的ht[0]和被遷移的ht[1]。防止遷移過程資料命不中的問題。









聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.