原因
擴容原因:當hashtable儲存的元素過多,可能由於碰撞也過多,導致其中某連結串列很長,最後致使查詢和插入時間複雜度很大。因此當元素超多一定的時候就需要擴容。
縮容原因:當元素數量比較少的時候就需要縮容以節約不必要的記憶體。為了讓雜湊表的負載因子(load factor)維持在一個合理的範圍內,會使用rehash(重新雜湊)操作對雜湊表進行相應的擴充套件或收縮。
負載因子的計算公式:雜湊表已儲存節點數量 / 雜湊表大小
== load_factor = ht[0].used / ht[0].size ==
擴容條件(滿足任意一個即可)
- Redis伺服器目前沒有在執行BGSAVE或BGREWRITEAOF命令,並且雜湊表的負載因子大於等於1。
- Redis伺服器目前在執行BGSAVE或BGREWRITEAOF命令,並且雜湊表的負載因子大於等於5。
為什麼BGSAVE或BGREWRITEAOF命令是否在執行,Redis伺服器雜湊表執行擴容所需的負載因子不相同(1或5)?
BGSAVE
:用於在後臺非同步儲存當前資料庫的資料到磁碟。
BGREWRITEAOF
:用於非同步執行一個 AOF( Append Only File ) 檔案重寫操作。
因為當執行BGSAVE或BGREWRITEAOF命令過程中,Redis需要建立伺服器程序的子程序,作業系統採用的是COW,即 寫時複製 copy-on-write的技術來最佳化子程序的使用效率。所以在子程序存在時,伺服器會提高執行擴容所需的負載因子,從而儘可能避免在子程序存在期間進行擴容,可以避免不必要的記憶體寫入操作,最大限度節約記憶體。
PS:COW
“寫時複製”(Copy-On-Write,簡稱 COW)是一種用於資源管理和最佳化的技術,主要應用在記憶體管理和系統設計中。它的基本思想是:如果有多個程序或執行緒需要讀取同一個資源(如記憶體塊或資料結構),它們可以共享該資源的單一副本;但是,當其中任何一個程序或執行緒嘗試修改資源時,系統會為其建立一個資源的獨立副本,這樣可以確保其他程序或執行緒看到的仍然是原始未修改的資源。
應用場景
-
記憶體管理:
-
作業系統中的程序建立:
- 當作業系統使用
fork()
系統呼叫建立一個新的程序時,新的程序通常是父程序的一個副本。為了節省記憶體和提高效率,子程序會共享父程序的記憶體空間。在這種情況下,COW 技術允許子程序和父程序在讀操作時共享同一塊記憶體,只有當某個程序嘗試寫入記憶體時,才會為該程序建立記憶體的副本。
- 當作業系統使用
-
虛擬記憶體管理:
- 在作業系統的虛擬記憶體管理中,COW 可以用於延遲分配實體記憶體。初始時,所有程序共享同一塊實體記憶體,當有程序嘗試寫操作時,作業系統才分配新的實體記憶體頁。
-
-
資料結構和演算法:
- 在某些資料結構(如字串、陣列、列表等)中,COW 可以用來最佳化修改操作。對於不可變的資料結構或在多執行緒環境中,COW 能夠提供一種安全且高效的方法來處理修改操作。
工作原理
以下是 COW 的基本工作原理:
- 初始狀態:多個程序或執行緒共享同一個資源(如記憶體塊)。此時,資源的引用計數器可能為多個。
- 讀操作:所有程序或執行緒可以自由地讀取共享的資源,而不需要建立副本。
- 寫操作:當某個程序或執行緒需要修改資源時,系統會執行以下步驟:
- 檢查資源的引用計數器。如果引用計數器大於1,意味著資源被多個程序或執行緒共享。
- 建立資源的一個獨立副本,僅供當前進行寫操作的程序或執行緒使用。
- 將引用計數器減1。
- 更新引用:修改後的副本成為當前程序或執行緒的資源,其他程序或執行緒仍然引用原始資源。
優缺點
優點:
- 節省記憶體:多個程序或執行緒共享資源時,只需要儲存一份資源副本,直到需要寫操作時才建立副本。
- 提高效能:減少不必要的資源複製操作,提高了系統的整體效能。
缺點:
- 寫操作開銷:首次寫操作需要進行復制操作,這會引入額外的記憶體和時間開銷。
- 複雜性增加:實現 COW 機制需要更復雜的記憶體管理邏輯,增加了系統的複雜性。
示例
以下是一個簡單的示例,演示 COW 在記憶體管理中的應用:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
int main() {
pid_t pid;
int *shared_memory = malloc(sizeof(int));
*shared_memory = 42;
printf("Original value: %d\n", *shared_memory);
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
} else if (pid == 0) {
// Child process
*shared_memory = 100; // This triggers COW
printf("Child process value: %d\n", *shared_memory);
exit(0);
} else {
// Parent process
wait(NULL); // Wait for child process to finish
printf("Parent process value: %d\n", *shared_memory);
}
free(shared_memory);
return 0;
}
在這個示例中,父程序和子程序在 fork()
後最初共享同一個 shared_memory
指標。當子程序嘗試修改 shared_memory
的值時,系統會為子程序建立一個獨立的副本,而父程序仍然保持對原始值的訪問。
縮容條件
雜湊表的負載因子小於0.1。
對字典的雜湊表rehash步驟
為ht[1]分配空間:擴充套件操作,那麼ht[1] 的大小為第一個大於等於ht[0] .used*2的2的n次冪;收縮操作,那麼ht[1] 的大小為第一個大於等於ht[0].used 的2的n次冪
將ht[0]中的資料轉移到ht[1]中,在轉移的過程中,重新計算鍵的雜湊值和索引值,然後將鍵值對放置到ht[1]的指定位置。
當ht[0]的所有鍵值對都遷移到了ht[1]之後(ht[0]變為空表),將ht[0]釋放,然後將ht[1]設定成ht[0],最後為ht[1]分配一個空白雜湊表: