《閑扯Redis八》Redis字典的哈希表執行Rehash過程分析



一、前言

隨着操作的不斷執行, 哈希表保存的鍵值對會逐漸地增多或者減少, 為了讓哈希表的負載因子(load factor)維持在一個合理的范圍之內, 當哈希表保存的鍵值對數量太多或者太少時, 程序需要對哈希表的大小進行相應的擴展或者收縮。

原文解析

Redis五種數據類型

二、實現分析

1.rehash過程分析

擴展和收縮哈希表的工作可以通過執行 rehash (重新散列)操作來完成。

Redis 對字典的哈希表執行 rehash 的步驟

1.為字典的 ht[1] 哈希表分配空間, 這個哈希表的空間大小取決於要執行的操作, 以及 ht[0] 當前包含的鍵值對數量 (也即是ht[0].used 屬性的值):

如果執行的是擴展操作, 那么 ht[1] 的大小為第一個大於等於 ht[0].used * 2 的 2^n (2 的 n 次方冪);

如果執行的是收縮操作, 那么 ht[1] 的大小為第一個大於等於 ht[0].used 的 2^n 。

2.將保存在 ht[0] 中的所有鍵值對 rehash 到 ht[1] 上面: rehash 指的是重新計算鍵的哈希值和索引值, 然后將鍵值對放置到 ht[1] 哈希表的指定位置上。

3.當 ht[0] 包含的所有鍵值對都遷移到了 ht[1] 之后 (ht[0] 變為空表), 釋放 ht[0] , 將 ht[1] 設置為 ht[0] , 並在 ht[1] 新創建一個空白哈希表, 為下一次 rehash 做准備。

結構圖解,程序對字典的 ht[0] 進行擴展操作, 步驟如下:Redis五種數據類型

1. ht[0].used 當前的值為 4 , 4 * 2 = 8 , 而 8 (2^3)恰好是第一個大於等於 4 的 2 的 n 次方, 所以程序會將 ht[1] 哈希表
的大小設置為 8 。圖 4-9 展示了 ht[1] 在分配空間之后, 字典的樣子。

Redis五種數據類型

2. 將 ht[0] 包含的四個鍵值對都 rehash 到 ht[1] , 如圖 4-10 所示。

Redis五種數據類型

3. 釋放 ht[0] ,並將 ht[1] 設置為 ht[0] ,然后為 ht[1] 分配一個空白哈希表,如圖 4-11 所示。
至此, 對哈希表的擴展操作執行完畢, 程序成功將哈希表的大小從原來的 4 改為了現在的 8

Redis五種數據類型

2.哈希表的擴展與收縮

當以下條件中的任意一個被滿足時, 程序會自動開始對哈希表執行擴展操作:

服務器目前沒有在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 並且哈希表的負載因子大於等於 1 ;
服務器目前正在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 並且哈希表的負載因子大於等於 5 ;

其中哈希表的負載因子可以通過公式計算:

# 負載因子 = 哈希表已保存節點數量 / 哈希表大小
load_factor = ht[0].used / ht[0].size

比如說, 對於一個大小為 4 , 包含 4 個鍵值對的哈希表來說, 這個哈希表的負載因子為:

load_factor = 4 / 4 = 1

又比如說, 對於一個大小為 512 , 包含 256 個鍵值對的哈希表來說, 這個哈希表的負載因子為:

load_factor = 256 / 512 = 0.5

根據 BGSAVE 命令或 BGREWRITEAOF 命令是否正在執行, 服務器執行擴展操作所需的負載因子並不相同, 這是因為在執行 BGSAVE 命令或BGREWRITEAOF 命令的過程中, Redis 需要創建當前服務器進程的子進程, 而大多數操作系統都采用寫時復制(copy-on-write)技術來優化子進程的使用效率, 所以在子進程存在期間, 服務器會提高執行擴展操作所需的負載因子, 從而盡可能地避免在子進程存在期間進行哈希表擴展操作, 這可以避免不必要的內存寫入操作, 最大限度地節約內存。

另一方面, 當哈希表的負載因子小於 0.1 時, 程序自動開始對哈希表執行收縮操作。

注釋:寫時復制(copy-on-write)是一種可以推遲甚至避免復制數據的技術。內核此時並不是復制整個進程空間,而是讓父進程和子進程共享同一個副本。只有在需要寫入的時候,數據才會被復制,從而使父進程、子進程擁有各自的副本。也就是說,資源的復制只有在需要寫入的時候才進行,在此之前以只讀方式共享。

3.漸進式 rehash

擴展或收縮哈希表需要將 ht[0] 里面的所有鍵值對 rehash 到 ht[1] 里面, 但是, 這個 rehash 動作並不是一次性、集中式地完成的, 而是分多次、漸進式地完成的。

這樣做的原因在於, 如果 ht[0] 里只保存着四個鍵值對, 那么服務器可以在瞬間就將這些鍵值對全部 rehash 到 ht[1] ; 但是, 如果哈希表里保存的鍵值對數量不是四個, 而是四百萬、四千萬甚至四億個鍵值對, 那么要一次性將這些鍵值對全部 rehash 到 ht[1] 的話, 龐大的計算量可能會導致服務器在一段時間內停止服務。

因此, 為了避免 rehash 對服務器性能造成影響, 服務器不是一次性將 ht[0] 里面的所有鍵值對全部 rehash 到 ht[1] , 而是分多次、漸進式地將 ht[0] 里面的鍵值對慢慢地 rehash 到 ht[1] 。

以下是哈希表漸進式 rehash 的詳細步驟:

1.為 ht[1] 分配空間, 讓字典同時持有 ht[0] 和 ht[1] 兩個哈希表。

2.在字典中維持一個索引計數器變量 rehashidx , 並將它的值設置為 0 , 表示 rehash 工作正式開始。

3.在 rehash 進行期間, 每次對字典執行添加、刪除、查找或者更新操作時, 程序除了執行指定的操作以外, 
還會順帶將 ht[0] 哈希表在 rehashidx 索引上的所有鍵值對 rehash 到 ht[1] , 當 rehash 工作完成之后, 程序將 rehashidx 屬性的值增一。

4.隨着字典操作的不斷執行, 最終在某個時間點上, ht[0] 的所有鍵值對都會被 rehash 至 ht[1] , 
這時程序將 rehashidx 屬性的值設為 -1 , 表示 rehash 操作已完成。

漸進式 rehash 的好處在於它采取分而治之的方式, 將 rehash 鍵值對所需的計算工作均灘到對字典的每個添加、刪除、查找和更新操作上, 從而避免了集中式 rehash 而帶來的龐大計算量。

圖 4-12 至圖 4-17 展示了一次完整的漸進式 rehash 過程, 注意觀察在整個 rehash 過程中, 字典的 rehashidx 屬性是如何變化的。Redis五種數據類型
Redis五種數據類型
Redis五種數據類型
Redis五種數據類型
Redis五種數據類型
Redis五種數據類型

描述

因為在進行漸進式 rehash 的過程中, 字典會同時使用 ht[0] 和 ht1 兩個哈希表, 所以在漸進式 rehash 進行期間, 字典的刪除(delete)、查找(find)、更新(update)等操作會在兩個哈希表上進行: 比如說, 要在字典里面查找一個鍵的話, 程序會先在 ht[0] 里面進行查找, 如果沒找到的話, 就會繼續到 ht1 里面進行查找, 諸如此類。

另外, 在漸進式 rehash 執行期間, 新添加到字典的鍵值對一律會被保存到 ht1 里面, 而 ht[0] 則不再進行任何添加操作: 這一措施保證了 ht[0] 包含的鍵值對數量會只減不增, 並隨着 rehash 操作的執行而最終變成空表。

三、要點總結

1.字典使用哈希表作為底層實現, 每個字典帶有兩個哈希表, 一個用於平時使用, 另一個僅在進行 rehash 時使用

2.當哈希表保存的鍵值對數量太多或者太少時, 程序需要對哈希表的大小進行相應的擴展或者收縮(rehash)

3.rehash 動作並不是一次性、集中式地完成的, 而是分多次、漸進式地完成的

4.漸進式 rehash 的過程中, 字典會同時使用 ht[0] 和 ht[1] 兩個哈希表

大道七哥,有趣話不多


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM