字典擴容需要同時滿足如下兩個條件:
1、哈希表中保存的key數量超過了哈希表的大小(可以看出size既是哈希表大小,同時也是擴容閾值)
2、當前沒有子進程在執行aof文件重寫或者生成RDB文件;或者保存的節點數與哈希表大小的比例超過了安全閾值(默認值為5)
一、rehash
字典初始化,在redis中字典中的hash表也是采用延遲初始化策略,在創建字典的時候並沒有為哈希表分配內存,只有當第一次插入數據時,才真正分配內存。看看字典創建函數dictCreate
隨着操作的不斷進行,哈希表保存的鍵值對會逐漸增多或減少,為了讓哈希表負載因子維持在一個合理范圍之內,當哈希表保存的鍵值對太多或太少時,程序要對哈希表的大小進行相應的擴展或收縮。
Redis對字典的哈希表執行rehash的步驟如下:
1、為字典的ht[1]哈希表分配空間,這個空間大小取決於要執行的操作:
如果執行的是擴展操作,則ht[1]的大小為第一個大於等於等於ht[0].used*2的2^n;
rehash后新生成的dictEntry節點數組大小等於超過當前key個數向上求整的2的n次方,比如當前key個數為100,則新生成的節點數組大小就是128
2、如果執行的收縮操作,則ht[1]的大小為第一個大於等於ht[0].used的2^n;
將保存在ht[0]中的所有鍵值對rehash到ht[1]上面:rehash指的是重新計算鍵的哈希值和索引值,然后將鍵值對放置到ht[1]的指定位置上。
3、當ht[0]包含的所有鍵值對都遷移到ht[1]之后,釋放ht[0],將ht[1]設置為ht[0],並在ht[1]新創建一個空白哈希表,為下一次rehash做准備。
下面為一個rehash的實例 :
2 * 4 = 8 ( 2 ^ 3 ) :
重新計算索引,並復制, h [0] 所有的鍵值都遷移到 h [1]
完成 rehash 之后的字典
哈希表的擴展與收縮
redis中,每次插入鍵值對時,都會檢查是否需要擴容。如果滿足擴容條件,則進行擴容。
當以下條件中任意一個被滿足時,程序會自動開始對哈希表執行擴展操作:
1、服務器目前沒有執行BGSAVE或BGREWRITEAOF命令,並且哈希表負載因子大於等於1。
2、服務器正在執行BGSAVE或BGREWRITEAOF命令,並且哈希表負載因子大於等於5。
區分這兩種情況的目的在於,因為執行BGSAVE與BGREWRITEAOF過程中,Redis都需要創建子進程,而大多數操作系統都采用寫時復制技術來優化子進程使用效率,所以在子進程存在期間,服務器會提高執行擴展操作所需的負載因子,
從而盡可能避免在子進程存在期間進行哈希表擴展操作,這可以避免不必要的內存寫入,最大限度的節約空間。
另一方面,當哈希表負載因子小於0.1時,程序自動開始對哈希表執行收縮操作。
二、漸進式rehash
Redis中的rehash動作並不是一次性、集中式完成的,而是分多次、漸進式的完成的。
這樣做的目的是,如果服務器中包含很多鍵值對,要一次性的將這些鍵值對全部rehash到ht[1]的話,龐大的計算量可能導致服務器在一段時間內停止服務於。
為了避免這種影響,Redis采用了漸進式Redis:
1、為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表。
2、在字典中維持一個索引計數器變量rehashidx,並將它置為0,表示rehash工作開始。
3、在rehash進行期間,每次對字典執行添加、刪除、查找或者更新操作時,程序除了執行指定操作以外,還會順帶將ht[0]哈希表在rehashidx索引上的所有鍵值對rehash到ht[1]中,當rehash工作完成之后,程序將rehashidx屬性的值+1。
4、隨着字典操作的不斷進行,最終在某個時間點上,ht[0]的所有鍵值對都被rehash到ht[1]上,這時將rehashidx屬性設為-1,表示rehash完成。
漸進式rehash 的好處在於其采取分而治之的方式,將rehash鍵值對所需要的計算工作均攤到字典的每個添加、刪除、查找和更新操作上,從而避免了集中式rehash而帶來的龐大計算量。
下面為一個漸進式rehash的實例:
准備開始 rehash
rehash 索引 0 上的鍵值對
rehash 索引 1 上的鍵值對
rehash 索引 2 上的鍵值對
rehash 索引 3 上的鍵值對
rehash 執行完畢
漸進式rehash執行期間的哈希表操作
因為在漸進式rehash的過程中,字典會同時使用ht[0]和ht[1]兩個哈希表,所以在漸進式rehash進行期間,字典的刪除、查找、更新等操作都是在兩個表上進行的。
例如,查找操作會先在ht[0]上進行,如果沒找到再在ht[1]上進行。
添加操作的鍵值對會一律保存到ht[1]中,這一措施保證ht[0]包含的鍵值對只會減少不會增加。
rehash后新生成的dictEntry節點數組大小等於超過當前key個數向上求整的2的n次方,比如當前key個數為100,則新生成的節點數組大小就是128