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