漸進式rehash
在上一節,我們瞭解了字典的rehash 過程,需要特別指出的是,rehash 程式並不是在啟用之
後就馬上執行直到完成的,而是分多次、漸進式地完成的。
假設這樣一個情境:在一個有很多索引值對的字典裡,某個使用者在添加新索引值對時觸發了rehash
過程,如果這個rehash 過程必須將所有索引值對遷移完畢之後才將結果返回給使用者,這樣的處理
方式將是非常不友好的。
另一方面,要求伺服器必須阻塞直到rehash 完成,這對於Redis 伺服器本身也是不能接受的。
為瞭解決這個問題,Redis 使用了漸進式incremental)的rehash 方式:通過將rehash 分散
到多個步驟中進行,從而避免了集中式的計算。
漸進式rehash 主要由_dictRehashStep 和dictRehashMilliseconds 兩個函數進行:
. _dictRehashStep 用於對資料庫字典、以及雜湊鍵的字典進行被動rehash ;
. dictRehashMilliseconds 則由Redis 伺服器常規任務程式server cron job)執行,用
於對資料庫字典進行主動rehash ;
_dictRehashStep
每次執行_dictRehashStep ,ht[0]->table 雜湊表第一個不為空白的索引上的所有節點就會全
部遷移到ht[1]->table 。
在rehash 開始進行之後d->rehashidx 不為-1),每次執行一次添加、尋找、刪除操作,
_dictRehashStep 都會被執行一次:
650) this.width=650;" title="QQ20140122212848.png" src="http://www.bkjia.com/uploads/allimg/140207/2242303230-0.jpg" alt="wKiom1Lfx8-xIvSUAAFG-ophSV4572.jpg" />
因為字典會保持雜湊表大小和節點數的比率在一個很小的範圍內,所以每個索引上的節點數量
不會很多從目前版本的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 時// 返回1return (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 是分多次、漸進式地進行的。
本文出自 “phper-每天一點點~” 部落格,請務必保留此出處http://janephp.blog.51cto.com/4439680/1353930