概念
- 哈希表:也叫散列表,是根據關鍵碼值(Key value)而直接進行訪問的數據結構也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。
- 哈希函數:哈希表中元素是由哈希函數肯定的。將數據元素的關鍵字K做爲自變量,經過必定的函數關係(稱爲哈希函數)
- 哈希衝突:計算關鍵碼獲取的位置可能會重複,就就是衝突。如何解決衝突Redis中使用了鏈址法
字典實現
哈希表
typedef struct dictht{
//哈希表數組
dictEntry **table;
//哈希表大小
unsigned long size;
//哈小標大小掩碼,用於計算 下面一小節的如何計算位置能夠看到具體的意義。
unsigned long sizemask;
//哈希表已有節點的數量
unsigned long used;
}
- table:是一個數組,數組的每一個元素都是一個指向 dict.h/dictEntry 結構的指針;(結構下面可見)
- size:記錄哈希表的大小,即 table 數組的大小,且必定是2的冪;
- used:記錄哈希表中已有結點的數量;
- sizemask:用於對哈希過的鍵進行映射,索引到 table 的下標中,且值永遠等於 size-1。具體映射方法很簡單,就是對 哈希值 和 sizemask 進行位與操做,因爲 size 必定是2的冪,因此 sizemask=size-1,天然它的二進制表示的每個位(bit)都是1,等同於下文提到的取模;
哈希表節點
typedef struct dictEntry{
// 鍵
void *key
//值
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
//只想下一個哈希表節點,造成鏈表
struct dictEntry *next;
}dictEntry;
- key:是鍵值對中的鍵;
- v:是鍵值對中的值,它是一個聯合類型,方便存儲各類結構;
- next:是鏈表指針,指向下一個哈希表節點,他將多個哈希值相同的鍵值對串聯在一塊兒,用於解決鍵衝突;如圖所示,兩個dictEntry 的 key 分別是 k0 和 k1,經過某種哈希算法計算出來的哈希值和 sizemask 進行位與運算後都等於 3,因此都被放在了 table 數組的 3號槽中,而且用 next 指針串聯起來。
redis中的字典
typedef struct dict{
//
dictType *type
//
void *privdata;
//
dictht ht[2]
//
int trehashidx;
}dict;
- type: 是一個指向 dict.h/dictType 結構的指針,保存了一系列用於操做特定類型鍵值對的函數;
- ht:是兩個哈希表,通常狀況下,只使用ht[0],只有當哈希表的鍵值對數量超過負載(元素過多)時,纔會將鍵值對遷移到ht[1]
- trehashidx:因爲哈希表鍵值對有可能不少不少,因此 rehash 不是瞬間完成的,須要循序漸進,那麼 rehashidx 就記錄了當前 rehash 的進度,當 rehash 完畢後,將 rehashidx 置爲-1;
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 (*keyDestructor)(void *privdata, void *key); // 銷燬鍵的函數
void (*valDestructor)(void *privdata, void *obj); // 銷燬值的函數
} dictType;
Rehash
其實沒有想象中的那麼複雜,隨着字典操做的不斷執行,哈希表保存的鍵值對會不斷增多(或者減小),爲了讓哈希表的負載因子維持在一個合理的範圍以內,當哈希表保存的鍵值對數量太多或者太少時,須要對哈希表大小進行擴展或者收縮。redis
擴展與收縮的條件
負載因子:load_factor=ht[0].used/ht[0].size 當前使用過的節點數除以哈希大小。
當如下條件知足任意一個時,程序就會對哈希表進行擴展操做算法
- 服務器目前沒有執行bgsave或bgrewriteaof命令,而且哈希表的負載因子>=1
- 服務器目前正在執行bgsave或bgrewriteaof命令,而且哈希表的負載因子>=5
當負載因子的值小於0.1時,程序就會對哈希表進行收縮操做數組
Rehash 操做步驟
哈希表的擴容:服務器
- 爲字典ht[1]分配空間,大小爲第一個大於等於 ht[0].used * 2 的 2 的冪;
好比:ht[0].used 當前的值爲 4 , 4 * 2 = 8 , 而 8 (2^3)剛好是第一個大於等於 4 的 2 的 n 次方, 因此程序會將 ht[1] 哈希表的大小設置爲 8 。
- 將保存在 ht[0] 上的鍵值對 rehash 到 ht[1] 上,rehash 就是從新計算哈希值和索引,而且從新插入到 ht[1] 中,插入一個刪除一個
- 當 ht[0] 包含的全部鍵值對所有 rehash 到 ht[1] 上後,釋放 ht[0] 的控件, 將 ht[1] 設置爲 ht[0],而且在 ht[1] 上新創件一個空的哈希表,爲下一次 rehash 作準備;
哈希表的收縮:
一樣是爲 ht[1] 分配空間, 大小等於 max( ht[0].used, DICT_HT_INITIAL_SIZE ),而後和擴展作一樣的處理便可。數據結構
漸進式rehash
擴展或者收縮哈希表的時候,須要將 ht[0] 裏面全部的鍵值對 rehash 到 ht[1] 裏,當鍵值對數量很是多的時候,這個操做若是在一幀內完成,大量的計算極可能致使服務器宕機,因此不能一次性完成,須要漸進式的完成。
漸進式Rehash的操做步驟:函數
- 爲 ht[1] 分配指定空間,讓字典同時持有 ht[0] 和 ht[1] 兩個哈希表。
- 將 rehashidx 設置爲0,表示正式開始 rehash。
- 在進行 rehash 期間,每次對字典執行 增、刪、改、查操做時,程序除了執行指定的操做外,還會將 哈希表 ht[0].table中下標爲 rehashidx 位置上的全部的鍵值對 所有遷移到 ht[1].table 上,完成後 rehashidx 自增。這一步就是 rehash 的關鍵一步。爲了防止 ht[0] 是個稀疏表 (遍歷好久遇到的都是NULL),從而致使函數阻塞時間太長,這裏引入了一個 「最大空格訪問數」,也即代碼中的 enmty_visits,初始值爲 n*10。當遇到NULL的數量超過這個初始值直接返回。
- 最後,當 ht[0].used 變爲0時,表明全部的鍵值對都已經從 ht[0] 遷移到 ht[1] 了,釋放 ht[0].table, 而且將 ht[0] 設置爲 ht[1],rehashidx 標記爲 -1 表明 rehash 結束。
心中的疑問
如何計算位置
其實哈希表、字典、map,咱們並不陌生可是我對這個位置如何計算出的一直是隻知其一;不知其二。也查了一些資料下面整合一下。
若是咱們有一個長度爲4的哈希表,有4個數字要放入(6,7,9,12)那很簡單隻要對4個數字用4取模就能夠。結果爲:
[12,9,6,7]。
咱們知道取模操做的效率是很低的,那麼咱們能夠用位運算來代替。
解決方案爲:把哈希表的長度 L 設置爲2的冪(L = 2^n),那麼 L-1 的二進制表示就是n個1,任何值 x 對 L 取模等同於和
(L-1) 進行位與(C語言中的&)運算。
如何解決衝突
那麼問題來了若是數字是(6,7,9,11) 若是用4取模
[nil,9,6,7 11]這樣就會出現7 11
對4取模結果都爲3發生了衝突。 這就是所謂的
哈希鍵衝突,那麼如何解決這樣的衝突有不少解決方法,開放地址法、再散列法、鏈地址法
等等。redis中使用的是鏈址法,在對象中保存下一個值得地址。接下來繼續上面的算法。其實這就是redis 哈希表結構中
sizemask 字段的意義,就是用來保存L-1這個數字直接用於計算。