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]。防止遷移過程資料命不中的問題。











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

聯繫我們

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