1. 程式人生 > 其它 >《Redis設計與實現》讀書筆記(二) ——Redis中的字典(Hash)

《Redis設計與實現》讀書筆記(二) ——Redis中的字典(Hash)

《Redis設計與實現》讀書筆記(二) ——Redis中的字典(Hash)

(原創內容,轉載請註明來源,謝謝)

一、概述

字典,又稱符號表、關聯陣列、對映,是一種儲存鍵值對的抽象資料結構。每個鍵(key)和唯一的值(value)關聯,鍵是獨一無二的,通過對鍵的操作可以對值進行增刪改查。

redis中字典應用廣泛,對redis資料庫的增刪改查就是通過字典實現的。即redis資料庫的儲存,和大部分關係型資料庫不同,不採用B+tree進行處理,而是採用hash的方式進行處理。

另外,毫無疑問,redis的hash資料型別也是通過字典方式實現。

二、字典的實現

redis的字典,底層是使用雜湊表實現,每個雜湊表有多個雜湊節點,每個雜湊節點儲存了一個鍵值對。

1、雜湊表

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

其中,table是一個數組,裡面的每個元素指向dictEntry(雜湊表節點)結構的指標,dictEntry結構是鍵值對的結構;size表示雜湊表的大小,也是table陣列的大小;used表示table目前已有的鍵值對節點數量;sizemask一直等於size-1,該值與雜湊值一起決定一個屬性應該放到table的哪個位置。

大小為4的空雜湊表結構如下圖(左邊一列的圖)所示:

2、雜湊表節點

typedef structdictEntry{
void *key;
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
struct dictEntry *next;
}dictEntry;

其中,key表示節點的鍵;union表示key對應的值,可以是指標、uint64_t整數或int64_t整數;next是指向另一個雜湊表節點的指標,該指標將多個雜湊值相同的鍵值對連線在一起,避免因為雜湊值相同導致的衝突。

雜湊表節點如下圖(左邊第一列是雜湊表結構,表節點結構從左邊第二列開始)所示:

3、字典

1)dict

typedef structdict{
dictType *type;
void *privdata;
dictht ht[2];
int rehashidx;
}dict;

其中,type用於存放用於處理特定型別的處理函式,下面會提;privdata用於存放私有資料,儲存傳給type內的函式的資料;rehash是一個索引,當沒有在rehash進行時,值是-1;ht是包含兩個項的陣列,每個項是一個雜湊表,一般情況下只是用ht[0],只有在對ht[0]進行rehash時,才會使用ht[1]。

2)dictType

typedef structdictType{
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 *(*keyDestructor) (void *privdata, void *key);
void *(*valDestructor) (void *privdata, void *obj);
}dictType;

該結構體定義了字典的各種操作函式,hashFunction是雜湊值計算函式,keyDup是鍵複製,valDup是值複製,keyCompare是鍵比較,keyDestructor是鍵銷燬,valDestructor是值銷燬。

完整的字典結構如下圖所示:

三、雜湊演算法

要將新的鍵值對加到字典,程式要先對鍵進行雜湊演算法,算出雜湊值和索引值,再根據索引值,把包含新鍵值對的雜湊表節點放到雜湊表陣列指定的索引上。

redis實現雜湊的程式碼是:

hash =dict->type->hashFunction(key);
index = hash& dict->ht[x].sizemask;

算出來的結果中,index的值是多少,則key會落在table裡面的第index個位置(第一個位置index是0)。

其中,redis的hashFunction,採用的是murmurhash2演算法,是一種非加密型hash演算法,其具有高速的特點。

而hash的結果,還需要和sizemask進行二進位制的&計算,由於sizemask一直都是size-1,因此保證沒有資料的情況下必定放在table的第一個位置,而其後的值按照表格順序往下排的可能性也很大。

四、鍵衝突解決

當兩個或者以上的鍵,算出來的第三步的index的值一樣,則稱為有衝突。

為了解決此問題,redis採用鏈地址法,每個雜湊表節點都有一個指向next的指標,當發生衝突時,直接將當前雜湊表節點的next指標指向新的結果。後面如果還有衝突的鍵,則當前鍵的next會指向下一個雜湊表節點。

五、rehash(重新雜湊)

隨著操作進行,雜湊表儲存的鍵值對會增加或減少,為了讓雜湊表的負載因子(load factor)維持在一個合理範圍,當一個雜湊表儲存的鍵太多或者太少,需要對雜湊表進行擴充套件或者收縮。擴充套件或收縮雜湊表的過程,就稱為rehash。

rehash步驟如下:

1、給字典的ht[1]申請儲存空間,大小取決於要進行的操作,以及ht[0]當前鍵值對的數量(ht[0].used)。假設當前ht[0].used=x。

1)如果是擴充套件,則ht[1]的值是第一個大於等於x*2的2n的值。例如x是30,則ht[1]的大小是第一個大於等於30*2的2n的值,即64。

2)如果是收縮,則ht[1]的值是第一個大於等於x的2n的值。例如x是30,則ht[1]的大小是第一個大於等於30的2n的值,即32。

2、將儲存在ht[0]上面的所有鍵值對,rehash到ht[1],即對每個鍵重新採用第三大點雜湊演算法的方式計算雜湊值(index的值),再放到相應的ht[1]的表格指定位置。

3、當ht[0]的所有鍵值對都rehash到ht[1]後,釋放ht[0],並將ht[1]設定為ht[0],再新建一個空的ht[1],用於下一次rehash。

六、rehash條件

1、負載因子(load factor)計算

load_factor =ht[0].used / ht[0].size,即負載因子大小等於當前雜湊表的鍵值對數量,除以當前雜湊表的大小。

2、擴充套件

當以下任一條件滿足,雜湊表會自動進行擴充套件操作:

1)伺服器目前沒有在執行BGSAVE或者BGREWRITEAOF命令,且負載因子大於等於1。

2)伺服器目前正在在執行BGSAVE或者BGREWRITEAOF命令,且負載因子大於等於5。

要判斷是否在進行bgsave或bgwriteaof,是因為這兩個命令執行過程中,redis需要建立當前伺服器程序的子程序,而大多數作業系統又都是用寫時複製(copy-on-write)技術優化子程序的使用效率。

因此,在執行這兩個命令期間,redis會提高雜湊表擴充套件操作的負載因子,以避免不必要的資料寫入記憶體(例如ht[1]建立好而ht[0]尚未清空清空下,同時存在兩份資料),最大限度節約記憶體。

3、收縮

當負載因子小於0.1時,redis自動開始雜湊表的收縮工作。

4、copy-on-write簡介

寫時複製技術,是作業系統層面,為了提升效能而進行的一個策略。

策略如下:每次寫檔案操作,都寫在特定大小的一塊記憶體中(磁碟快取),並不是直接寫到磁碟中。只有當我們關閉檔案時,才寫到磁碟上(這就是為什麼如果檔案不關閉,所寫的東西會丟失的原因)。更有甚者是檔案關閉時都不寫磁碟,而一直等到關機或是記憶體不夠時才寫磁碟,Unix就是這樣一個系統,如果非正常退出,那麼資料就會丟失,檔案就會損壞。

寫時複製技術,大大降低了磁碟I/O的次數,而I/O往往是效能瓶頸,這樣一來就最大程度上避免了此瓶頸。

七、漸進式rehash

redis對ht[0]擴充套件或收縮到ht[1]的過程,並不是一次性完成的,而是漸進式、分多次的完成,以避免如果雜湊表中存有大量鍵值對,一次性複製過程中,佔用資源較多,會導致redis服務停用的問題。

漸進式rehash過程如下:

1、為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩張雜湊表。

2、將字典中的rehashidx設定成0,表示正在rehash。rehashidx的值預設是-1,表示沒有在rehash。

3、在rehash進行期間,程式處理正常對字典進行增刪改查以外,還會順帶將ht[0]雜湊表上,rehashidx索引上,所有的鍵值對資料rehash到ht[1],並且rehashidx的值加1。

4、當某個時間節點,全部的ht[0]都遷移到ht[1]後,rehashidx的值重新設定為-1,表示rehash完成。

漸進式rehash採用分而治之的工作方式,將雜湊表的遷移工作所耗費的時間,平攤到增刪改查中,避免集中rehash導致的龐大計算量。

在rehash期間,對雜湊表的查詢、修改、刪除,會先在ht[0]進行。如果ht[0]中沒找到相應的內容,則會去ht[1]查詢,並進行相關的修改、刪除操作。而增加的操作,會直接增加到ht[1]中,目的是讓ht[0]只減不增,加快遷移的速度。

八、總結

字典在redis中廣泛應用,包括資料庫和hash資料結構。每個字典有兩個雜湊表,一個是正常使用,一個用於rehash期間使用。當redis計算雜湊時,採用的是MurmurHash2雜湊演算法。雜湊表採用鏈地址法避免鍵的衝突,被分配到同一個地址的鍵會構成一個單向連結串列。在rehash對雜湊表進行擴充套件或者收縮過程中,會將所有鍵值對進行遷移,並且這個遷移是漸進式的遷移。

——written by linhxx 2017.08.29