[Redis]擴容

Duancf發表於2024-07-06

原因

擴容原因:當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)是一種用於資源管理和最佳化的技術,主要應用在記憶體管理和系統設計中。它的基本思想是:如果有多個程序或執行緒需要讀取同一個資源(如記憶體塊或資料結構),它們可以共享該資源的單一副本;但是,當其中任何一個程序或執行緒嘗試修改資源時,系統會為其建立一個資源的獨立副本,這樣可以確保其他程序或執行緒看到的仍然是原始未修改的資源。

應用場景

  1. 記憶體管理

    • 作業系統中的程序建立

      • 當作業系統使用 fork() 系統呼叫建立一個新的程序時,新的程序通常是父程序的一個副本。為了節省記憶體和提高效率,子程序會共享父程序的記憶體空間。在這種情況下,COW 技術允許子程序和父程序在讀操作時共享同一塊記憶體,只有當某個程序嘗試寫入記憶體時,才會為該程序建立記憶體的副本。
    • 虛擬記憶體管理

      • 在作業系統的虛擬記憶體管理中,COW 可以用於延遲分配實體記憶體。初始時,所有程序共享同一塊實體記憶體,當有程序嘗試寫操作時,作業系統才分配新的實體記憶體頁。
  2. 資料結構和演算法

    • 在某些資料結構(如字串、陣列、列表等)中,COW 可以用來最佳化修改操作。對於不可變的資料結構或在多執行緒環境中,COW 能夠提供一種安全且高效的方法來處理修改操作。

工作原理

以下是 COW 的基本工作原理:

  1. 初始狀態:多個程序或執行緒共享同一個資源(如記憶體塊)。此時,資源的引用計數器可能為多個。
  2. 讀操作:所有程序或執行緒可以自由地讀取共享的資源,而不需要建立副本。
  3. 寫操作:當某個程序或執行緒需要修改資源時,系統會執行以下步驟:
    • 檢查資源的引用計數器。如果引用計數器大於1,意味著資源被多個程序或執行緒共享。
    • 建立資源的一個獨立副本,僅供當前進行寫操作的程序或執行緒使用。
    • 將引用計數器減1。
  4. 更新引用:修改後的副本成為當前程序或執行緒的資源,其他程序或執行緒仍然引用原始資源。

優缺點

優點

  • 節省記憶體:多個程序或執行緒共享資源時,只需要儲存一份資源副本,直到需要寫操作時才建立副本。
  • 提高效能:減少不必要的資源複製操作,提高了系統的整體效能。

缺點

  • 寫操作開銷:首次寫操作需要進行復制操作,這會引入額外的記憶體和時間開銷。
  • 複雜性增加:實現 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]分配一個空白雜湊表:

相關文章