Redis 字典底層基於雜湊表實現。
一、雜湊表結構
1、dictht:
typedef struct dictht {
dictEntry **table; //雜湊表陣列,儲存具體的鍵值對元素,物件型別 dictEntry
unsigned long size; //雜湊表容量
unsigned long sizemask; //雜湊表大小掩碼,計算索引使用
unsigned long used; //已使用容量
} dictht
2、示例資料:
二、雜湊表節點
1、dictEntry:
typedef struct dictEntry {
void *key; //鍵值對 key
union{ //鍵值對 value 三種型別
void *val;
uint64_tu64;
int64_ts64;
} v;
struct dictEntry *next; //下一個節點指標
} dictEntry;
說明:next 為指向下一個節點的指標,是我們熟悉的連結串列節點結構,單向連結串列,用於處理鍵雜湊衝突問題。
相同雜湊值的鍵的鍵值對會以連結串列的形式存在同一位置。
2、示例資料:
三、Redis 字典
1、dict:
typedef struct dict{
dictType *type; //型別特定函式
void *privdata; //私有資料
dictht ht[2]; //雜湊表陣列,型別為dictht,ht[0]為實際儲存資料使用,ht[1] 為rehash時使用
int rehashidx; //rehash進度標誌,-1 代表當前不在 rehash
} dict
2、示例資料:
四、新增元素
向字典中新增元素主要涉及一下幾步操作:
1、計算鍵值對鍵的雜湊值
hash:dict->type->hashFunction(key)
使用dictType內部的雜湊函式得到鍵雜湊值
2、計算需要放入的位置索引
index:hash&dict->ht[0].sizemask
使用上一步計算得到的雜湊值與雜湊表的sizemask屬性進行與操作得到需要放入的位置索引值
3、鍵衝突解決
沒有完美的雜湊函式,雜湊衝突往往無法避免,當多個鍵被所引導同一個位置時,這種現象,我們稱之為鍵衝突。
解決間衝突,Redis 採用鏈地址法,也即將衝突的鍵值對組成一條鏈條放到同一個雜湊位置上。上面第二節我們介紹過 dictEntry的結構,其中包含一個指向另一個節點的指標next。
這裡需要說明的一點是,衝突節點插入時,是插入到連結串列的頭部,這樣只需要執行操作一次操作即可,也即時間複雜度為O(1)。
如下圖:(k2,v2)與(k1,v1)發生衝突,直接將(k2,v2)插入到連結串列頭部:
五、rehash
rehash過程是在重新規劃雜湊表佔用空間時發生的。
負載因子 load_factor:已儲存節點數量(dict.ht[0].used)/ 雜湊表容量(dict.ht[0].size)
負載因子用以表名當前雜湊表的使用狀態,它需要保持在一個合理的範圍,以保障資源的最優利用。通常需要適時的對雜湊表進行擴充套件或者收縮來對負載因子進行維護,而這個過程,我們稱之為 rehash。
這裡涉及到一個問題,就是什麼時候需要進行伸縮維護?
1、擴充套件時機:
當前無bgsave及bgrewriteaop操作,load_factor >= 1
當前存在bgsave及bgrewriteaop操作,load_factor >= 5
Redis伺服器通過fork子程式形式執行bgsave及bgrewriteaop操作,此時整個服務的資源耗費較大,為了避免可能發生的rehash帶來額外的資源壓力,此期間,伺服器會調高觸發執行擴充套件操作的負載因子界限。
2、收縮時機:
load_factor < 0.1
3、rehash 基本操作:
a) 為dict.ht[1]分配空間:
空間大小計算如下:
擴充套件:最小n滿足2n >= dict.ht[0].used * 2
收縮:最小n滿足2n >= dict.ht[0].used
如下圖:ht[0].used = 3,假定無bg相關任務,則h[1]大小需要計算:2n >= 3 * 2 = 6
n = 3,ht[1].size = 23 = 8
b) rehash
對於dict.ht[0] 中的元素,依據dict.ht[1]特性(sizemask)重新計算索引值,並放置到dict.ht[1]中。
c) 當所有元素遷移完畢,釋放dict.ht[0],並將dict.ht[1]設定為dict.ht[0],重新在dict.ht[1]上建立空的雜湊表。
六、漸進式rehash
所謂漸進式,是針對大資料量字典資料。直接一次性的執行rehash會導致服務資源的集中佔用,影響正常的服務響應。因此需要進行分而治之。
這裡會用到上面我們介紹的dict字典結構中的 rehashidx屬性,用以標識當前rehash進度。
首先將rehashidx置0,標示rehash開始,每次rehash一個元素,rehashidx值增加1,當最終所有元素rehash完成,將rehashidx置-1。
這裡需要說明下rehash中對正常的服務請求的處理:
1、刪除、查詢、更新:
會涉及到兩個雜湊表(ht[0]、ht[1])操作,如查詢元素,首先嚐試在ht[0]上查詢,找不到,則繼續在h[1]上查詢。
2、新增
新增元素只會在h[1]上操作,h[0]上只減不增。