《閒扯Redis八》Redis字典的雜湊表執行Rehash過程分析

jstarseven發表於2020-07-28

一、前言

隨著操作的不斷執行, 雜湊表儲存的鍵值對會逐漸地增多或者減少, 為了讓雜湊表的負載因子(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] 兩個雜湊表

大道七哥,有趣話不多

相關文章