Redis中的字典

壹言發表於2020-10-15

原文連結:https://www.changxuan.top/?p=1122

簡介

字典是一種在 Redis 中高頻使用的用於儲存鍵值對的抽象資料結構,在 Java 中常用的有 HasmMap 等。

由於字典中鍵的唯一性,所以在 Redis 中得到了廣泛的應用。

實現

Redis 中的字典是基於雜湊表 (dictht, dict hash table)實現的,雜湊表中的每個節點儲存一個鍵值對。雜湊表的結構體定義如下:

typedef struct dictht {
  // 雜湊表陣列
  dictEntry **table;
  // 雜湊表大小
  unsigned long size;
  // 雜湊表大小掩碼,用於計算索引值 size - 1,用來計算鍵值對放在哪個索引上
  unsigned long sizemask;
  // 雜湊表已有節點的數量
  unsigned long used;
} dictht;

雜湊表節點 dictEntry 的結構則如下所示:

typedef struct dictEntry {
  // 鍵
  void *key;
  // 值
  union {
    void *val;
    uint64_t u64;
    int64_t s64;
  }v;
  // 指向下個雜湊表節點
  struct dictEntry *next;
} dictEntry;

dictEntry 中的值有些特別,它表示其值有可能是一個指標或者是一個 uint64_t 整數,或者是一個 int64_t 整數。

因為存在 next 屬性,很顯然它是使用鏈地址法解決的雜湊鍵衝突。

接下來我們看一下字典(dict)的定義:

typedef struct dict {
  // 型別特定函式
  dictType *type;
  // 私有資料
  void *privdata;
  // 雜湊表
  dictht ht[2];
  // rehash 索引 當不在進行 rehash 的時候,值為-1
  int trehashids; 
} dict;

屬性 type 是一個指向 dictType 結構體的指標,每個 dictType 儲存了一些用於操作特定型別鍵值對的函式。

typedef struct dictType {
 // 計算雜湊值的函式
 unsigned int (*hashFunction)(const void *key);
 // 複製鍵的函式
 void *(*keyDup)(void *privdata, const void *key);
 // 複製值的函式
 void *(*valDup)(void *privdata, const void *obj);
 // 對比鍵的函式
 int (*keyCompare)(void *privdata, const void *key1, const void *key2);
 // 銷燬鍵的函式
 void (*valDestructor)(void *privdata, void *obj);
} dictType;

ht 陣列表示儲存兩個雜湊表,平常情況下只使用 ht[0] ,只有在 rehash 時才會使用到 h[1]trehashids

字典的結構就是,一個字典中有兩個雜湊表,平時只用一個雜湊表。另一個雜湊表在 rehash 的時候使用。每個雜湊表中存在一個節點陣列,節點則用於存放鍵值對。

新增鍵值對

新增鍵值對就意味著需要計算鍵的雜湊值,從而得出索引值。根據索引值將鍵值對的雜湊節點放到雜湊表的指定位置上。計算雜湊值使用的是字典結構體中的 type 中的函式,即 hash = dict->type->hashFunction(key) 。計算索引值則是 index = hash & dict->ht[x].sizemask ,x 取決於當前使用的是ht[1]還是ht[2]。

不過,總會有不同的鍵對應相同的索引值,產生衝突。Redis 中使用了常用的“鏈地址法”來解決這個問題,當出現衝突時就把新節點放到表頭的位置。

Rehash

隨著字典中鍵值對數量的不斷變化,為了保證雜湊表的空間利用率以及效率,在雜湊表過大或者過小是要對雜湊表大小進行調整。如果過小,則會不斷髮生鍵衝突導致效率低下,如果過大則會浪費儲存空間。所以,經過不斷調整可以使其維持在一個合理的範圍。

步驟

  1. ht[1] 分配空間,大小取決於是擴大雜湊表還是縮小雜湊表。如果擴大,其大小為第一個大於等於 ht[0].used * 2 且同時為2的n次方冪 的值。如果縮小,其大小為第一個大於等於 ht[0].used 其同時為 2的n次方冪 的值。

  2. 將儲存在 ht[0] 中所有的鍵值對重新計算雜湊值和索引值後,存放在 ht[1] 中。

  3. 當遷移完所有的鍵值之後,釋放原 ht[0] 的空間,將原 h[1] 改為 h0, 並在 ht[1] 新建立一個空白雜湊表。

那麼何時擴充套件雜湊表大小呢? 一是當沒有在執行 BGSAVE 或者 BGREWRITEAOF 命令時,並且雜湊表的負載因子大於等於1時。 二是當在執行這倆命令,但是負載因子大於等於5時(節約記憶體,上述兩命令消耗記憶體)。

負載因子計算公式為:負載因子 = 雜湊表儲存節點數量/雜湊表大小

那麼何時縮小雜湊表大小呢? 當雜湊表負載因子小於 0.1 時則會進行縮小。

漸進式 Rehash

其實對於上述步驟 2 ,普通人覺得這不就是把鍵值對重新分配一下嗎?但是如果此時存在百萬、千萬甚至億級的鍵值對時,恐怕就是不是一眨眼的功夫就可以完成的了。如果非得一次性完成,那麼可能會導致伺服器的不可用。所以為了解決這個問題,Redis 採用了慢慢來的辦法漸進式 Rehash

其主要步驟與前面的有些相似,只不過在漸進式Rehash中使用到了 dict->trehashids 值來記錄當前rehash到了哪個索引。在 Rehash 期間,可以對字典正常進行增加、刪除、查詢和更新。然後同時也會將 trehashids 上記錄的索引值上的節點遷移到 h[1] 上。並且所有的新增節點都會放到 h[1]中,這樣就會導致 h[0] 中的節點越來越少,最終完成 rehash。其它的操作則會在兩個表上進行。

相關文章