Redis設計於實現之字典

一隻牛_007發表於2020-10-29

字典

簡介

  • 字典又稱符號表,對映或關聯陣列,是一種儲存鍵值對的抽象資料結構。
  • Redis資料庫的底層也是用字典實現的,對資料庫的增刪改查也是基於對字典的操作之上的。
  • 字典還是雜湊鍵的底層實現之一,當雜湊鍵對比較多或者鍵值對中的元素都是比較長的,Redis就會使用字典作為底層實現。

字典實現

字典的實現是以雜湊表作為它的底層實現,一個雜湊表可以有多個雜湊表節點,每個節點儲存了字典中的一個鍵值對。
1.雜湊表節點
key就是鍵,v就是鍵中的值(可以是指標,unit64_t 整數, int64_t s64整數),next是將另外一個雜湊值相同的鍵值對連線在一起的指標(為了解決衝突)

typedef struct dictEntry{
   void *key;
   union{
   void *var;
   unit64_t u64;
   int64_t  s64;
   }v;
 struct dictEntry *next;
}

2.雜湊表
table屬性是一個陣列,陣列中每個元素都指向一個雜湊表節點 ,每個雜湊表節點都儲存著一個鍵值對。
size記錄了雜湊表的大小,也就是table陣列的大小。
used屬性記錄雜湊表目前已有雜湊表節點(鍵值對)的數量。
sizemask總是等於size-1(這個屬性和雜湊值一起決定一個鍵應該被放到table的那個索引上)。

typedef struct dictht{
   dictEntry **table;
   unsigned long size;
   unsigned long sizemask;
   unsigned long used;
}dictht;

3.字典
type和pribdata是配套的,針對不同型別的鍵值對,為建立多型字典而設定的。
type指向dictType結構的指標,每一個dictType裡都儲存了一簇用於操作特定型別鍵值對的函式(為用途不同的字典設定不同的型別特定函式)。
privdata儲存了需要傳給那些型別特定函式的可選引數(也就是在dictType結構體中的引數)。
ht包含了兩項陣列,每個項都是dictht雜湊表,字典只使用ht[0]雜湊表,ht[1]只會在ht[0]rehash時使用,除了ht[1]之外另一個和rehash(重新雜湊)有關的就是rehashidx(記錄rehash進度,若沒在進行rehash則值為-1).

typedef struct dict{
   dicType *type;
   void *pribdata;
   dictht  ht[2];
   int trehashidx;
}dict;
/* 儲存一連串操作特定型別鍵值對的函式 */
typedef struct dictType {
    uint64_t (*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 (*keyDestructor)(void *privdata, void *key); /* 銷燬鍵函式 */
    void (*valDestructor)(void *privdata, void *obj); /* 銷燬值函式 */
} dictType;

這就是字典實現的資料結構,如果要資料庫效能好,還是要用一些效能較好的演算法,Redis使用的MurmurHash2演算法來計算鍵的雜湊值(資料庫的底層實現或者雜湊鍵的底層實現)。

雜湊演算法

在我們需要把一個新的鍵值對加入到字典裡,程式得先根據鍵值對的鍵計算出雜湊值和索引值,然後根據索引,將新鍵值對的雜湊表節點方法雜湊表陣列的指定索引(位置)上,這都是由雜湊演算法來完成的。雜湊演算法的設計推理就不寫了,因為這個是一個很複雜的過程。
Redis計算雜湊值和索引值的方法

//用字典設定的雜湊函式計算key的雜湊值
hash = dict->type->hashFunction(key);
//利用雜湊表的sizemask和雜湊值來計算出索引值,h[x]可以是h[1]或h[0]因情況而定。
index = hash & dict->ht[x].sizemask;

這種演算法的優點就是:對於輸入有規律的鍵仍能給出一個很好的隨機分佈性而且計算速度也很快!但是呢,這種演算法可能會出現衝突,因此要避免衝突就得由個解決衝突的辦法?(在前面提到過)

解決鍵衝突

什麼是衝突:因為演算法執行時會有可能多個鍵被分配到雜湊陣列的同一個索引上。
Redis中解決鍵衝突採用的是鏈地址法,每個節點都有一個next指標,構成衝突的節點可以用next指標構成一個單連結串列來共同佔有同一個索引。這個解決方法也是解決衝突比較經典的方法,也是比較簡單的方法。

rehash

rehash是什麼?為什麼要rehash?
rehash是重新雜湊的意思,因為在不斷的執行中,雜湊表儲存的鍵值對在逐漸增多或減少,為了讓雜湊表的負載因子(load factor)維持在合理範圍內,當雜湊表中的鍵值對過多或者過少時,需要對錶的大小進行擴充套件或收縮
雜湊表執行rehash的步驟:
1.為字典的ht[1]分配空間,此雜湊表的大小取決於要執行的操作和h[0]包含的鍵值對的數量(ht[0].used)

  • 擴充套件操作:h[1]的大小等於第一個大於ht[0].used*2的2的n次方。
  • 收縮操作:h[1]的大小等於第一個大於ht[0].used的2的n次方。

2.將儲存在ht[0]中的鍵值對到ht[1]上:重新計算雜湊值和索引,然後將鍵值對放到ht[1]雜湊表的指定位置上。
3. ht[0]遷移到ht[1]後,ht[0]變為空表然後釋放掉,然後再將ht[1]設定為ht[0],並再ht[1]位置上新建一個空白的雜湊表,供下一次rehash使用。

雜湊表的自動擴充套件與收縮

如果在負載因子不合理時沒有進行手動的rehash的話,那系統會在某些條件成立下自動進行擴充套件或收縮。
雜湊表的負載因子求法:負載因子=以儲存節點數量/雜湊表大小

load_factor = ht[0].userd/ht[0].size
  •  
  • 當系統滿足以下的任意一個條件程式就會自動開始對雜湊表執行擴充套件操作:
    1.伺服器目前沒在執行BGSAVE命令或者BGREWRITEAOF命令並且雜湊表的負載因子大於1
    2.伺服器目前正執行BGSAVE命令或者BGREWRITEAOF命令並且負載因子大於5
  • 當系統滿足負載因子小於0.1,就會自動進行收縮操作。

漸進式rehash

其實在擴充套件或者收縮雜湊表的時候並不是一次性,集中性的執行的,而是分多次,漸進式地完成的。
漸進式的詳細步驟:
1.為ht[1]分配空間,此時字典同時有ht[0]和ht[1]兩個雜湊表。
2.在字典中維持一個索引計數器變數rehashidx,並設為0,表示rehash正式開始。
3.在rehash期間,每次對字典進行新增,刪除,查詢,更新時,程式除了執行指定操作以外,還會順帶將ht[0]雜湊表在rehashidx索引上的所有鍵值對rehash到ht[1]上,rehash完成,然後rehashidx+1。
4.直到全部內移到ht[1],這時rehashidx屬性的值設為-1,表示rehash操作完成。
採取分而治之的方式,將rehash鍵值對所需的工作均攤到對字典的增刪改查上,避免了集中式rehash帶來的龐大計算量。

漸進式rehash執行期間的雜湊表操作

因為在rehash期間字典會同時使用ht[0]和ht[1],因此,增刪改查會在兩個雜湊表上進行,比如查詢操作,先對0表掃描如果沒找到,就再從1表裡找。注意的是,如果在此期間進行插入操作的話,那就會插入到1表,而不是0表。因為插入到0表就沒意義了等於浪費體力。也保證了0表只減不增。

相關文章