漸進式rehashnode
在上一節,咱們瞭解了字典的rehash 過程,須要特別指出的是,rehash 程序並非在激活之redis
後就立刻執行直到完成的,而是分屢次、漸進式地完成的。算法
假設這樣一個場景:在一個有不少鍵值對的字典裏,某個用戶在添加新鍵值對時觸發了rehash數據庫
過程,若是這個rehash 過程必須將全部鍵值對遷移完畢以後纔將結果返回給用戶,這樣的處理數組
方式將是很是不友好的。安全
另外一方面,要求服務器必須阻塞直到rehash 完成,這對於Redis 服務器自己也是不能接受的。服務器
爲了解決這個問題,Redis 使用了漸進式(incremental)的rehash 方式:經過將rehash 分散數據結構
到多個步驟中進行,從而避免了集中式的計算。ide
漸進式rehash 主要由_dictRehashStep 和dictRehashMilliseconds 兩個函數進行:
. _dictRehashStep 用於對數據庫字典、以及哈希鍵的字典進行被動rehash ;
. dictRehashMilliseconds 則由Redis 服務器常規任務程序(server cron job)執行,用
於對數據庫字典進行主動rehash ;
_dictRehashStep
每次執行_dictRehashStep ,ht[0]->table 哈希表第一個不爲空的索引上的全部節點就會全
部遷移到ht[1]->table 。函數
在rehash 開始進行以後(d->rehashidx 不爲-1),每次執行一次添加、查找、刪除操做,
_dictRehashStep 都會被執行一次:
由於字典會保持哈希表大小和節點數的比率在一個很小的範圍內,因此每一個索引上的節點數量
不會不少(從目前版本的rehash 條件來看,平均只有一個,最多一般也不會超過五個),因此
在執行操做的同時,對單個索引上的節點進行遷移,幾乎不會對響應時間形成影響。
dictRehashMilliseconds
dictRehashMilliseconds 能夠在指定的毫秒數內,對字典進行rehash 。
當Redis 的服務器常規任務執行時,dictRehashMilliseconds 會被執行,在規定的時間內,
儘量地對數據庫字典中那些須要rehash 的字典進行rehash ,從而加速數據庫字典的rehash
進程(progress)。
其餘措施
在哈希表進行rehash 時,字典還會採起一些特別的措施,確保rehash 順利、正確地進行:
由於在rehash 時,字典會同時使用兩個哈希表,因此在這期間的全部查找、刪除等操做,
除了在ht[0] 上進行,還須要在ht[1] 上進行。
在執行添加操做時,新的節點會直接添加到ht[1] 而不是ht[0] ,這樣保證ht[0] 的節
點數量在整個rehash 過程當中都只減不增。
字典的收縮
上面關於rehash 的章節描述了經過rehash 對字典進行擴展(expand)的狀況,若是哈希表的
可用節點數比已用節點數大不少的話,那麼也能夠經過對哈希表進行rehash 來收縮(shrink)
字典。
收縮rehash 和上面展現的擴展rehash 的操做幾乎同樣,它執行如下步驟:
1. 建立一個比ht[0]->table 小的ht[1]->table ;
2. 將ht[0]->table 中的全部鍵值對遷移到ht[1]->table ;
3. 將原有ht[0] 的數據清空,並將ht[1] 替換爲新的ht[0] ;
擴展rehash 和收縮rehash 執行徹底相同的過程,一個rehash 是擴展仍是收縮字典,關鍵在於
新分配的ht[1]->table 的大小:
. 若是rehash 是擴展操做,那麼ht[1]->table 比ht[0]->table 要大;
. 若是rehash 是收縮操做,那麼ht[1]->table 比ht[0]->table 要小;
字典的收縮規則由redis.c/htNeedsResize 函數定義:
/* * 檢查字典的使用率是否低於系統容許的最小比率 ** 是的話返回1 ,不然返回0 。 */ 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)); }
在默認狀況下,REDIS_HT_MINFILL 的值爲10 ,也便是說,當字典的填充率低於10% 時,程
序就能夠對這個字典進行收縮操做了。
字典收縮和字典擴展的一個區別是:
. 字典的擴展操做是自動觸發的(不論是自動擴展仍是強制擴展);
. 而字典的收縮操做則是由程序手動執行。
所以,使用字典的程序能夠決定什麼時候對字典進行收縮:
. 當字典用於實現哈希鍵的時候,每次從字典中刪除一個鍵值對,程序就會執行一次
htNeedsResize 函數,若是字典達到了收縮的標準,程序將當即對字典進行收縮;
. 當字典用於實現數據庫鍵空間(key space) 的時候, 收縮的時機由
redis.c/tryResizeHashTables 函數決定.
字典其餘操做
除了添加操做和伸展/收縮操做以外,字典還定義了其餘一些操做,好比常見的查找、刪除和更
新。
由於鏈地址法哈希表實現的相關信息能夠從任何一本數據結構或算法書上找到,這裏再也不對字
典的其餘操做進行介紹,不過前面對建立字典、添加鍵值對、收縮和擴展rehash 的討論已經涵
蓋了字典模塊的核心內容。
字典的迭代
字典帶有本身的迭代器實現——對字典進行迭代實際上就是對字典所使用的哈希表進行迭代:
. 迭代器首先迭代字典的第一個哈希表,而後,若是rehash 正在進行的話,就繼續對第二
個哈希表進行迭代。
. 當迭代哈希表時,找到第一個不爲空的索引,而後迭代這個索引上的全部節點。
. 當這個索引迭代完了,繼續查找下一個不爲空的索引,如此循環,一直到整個哈希表都迭
代完爲止。
整個迭代過程能夠用僞代碼表示以下:
def iter_dict(dict): // 迭代0 號哈希表 iter_table(ht[0]->table) // 若是正在執行rehash ,那麼也迭代1 號哈希表 if dict.is_rehashing(): iter_table(ht[1]->table) def iter_table(table): // 遍歷哈希表上的全部索引 for index in table: // 跳過空索引 if table[index].empty(): continue // 遍歷索引上的全部節點 for node in table[index]: // 處理節點 do_something_with(node)
字典的迭代器有兩種:
安全迭代器:在迭代進行過程當中,能夠對字典進行修改。
不安全迭代器:在迭代進行過程當中,不對字典進行修改。
如下是迭代器的數據結構定義:
/* * 字典迭代器 */ typedef struct dictIterator { dict *d; // 正在迭代的字典 int table, // 正在迭代的哈希表的號碼(0 或者1) index, // 正在迭代的哈希表數組的索引 safe; // 是否安全? dictEntry *entry, // 當前哈希節點 *nextEntry; // 當前哈希節點的後繼節點 } dictIterator;
小結 字典由鍵值對構成的抽象數據結構。 Redis 中的數據庫和哈希鍵都基於字典來實現。 Redis 字典的底層實現爲哈希表,每一個字典使用兩個哈希表,通常狀況下只使用0 號哈希表,只有在rehash 進行時,纔會同時使用0 號和1 號哈希表。 哈希表使用鏈地址法來解決鍵衝突的問題。 Rehash 能夠用於擴展或收縮哈希表。 對哈希表的rehash 是分屢次、漸進式地進行的。