Redis 數據結構之Map(字典)

概念

  • 哈希表:也叫散列表,是根據關鍵碼值(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 操做步驟

哈希表的擴容服務器

  1. 爲字典ht[1]分配空間,大小爲第一個大於等於 ht[0].used * 2 的 2 的冪;
    好比:ht[0].used 當前的值爲 4 , 4 * 2 = 8 , 而 8 (2^3)剛好是第一個大於等於 4 的 2 的 n 次方, 因此程序會將 ht[1] 哈希表的大小設置爲 8 。
  2. 將保存在 ht[0] 上的鍵值對 rehash 到 ht[1] 上,rehash 就是從新計算哈希值和索引,而且從新插入到 ht[1] 中,插入一個刪除一個
  3. 當 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的操做步驟:函數

  1. 爲 ht[1] 分配指定空間,讓字典同時持有 ht[0] 和 ht[1] 兩個哈希表。
  2. 將 rehashidx 設置爲0,表示正式開始 rehash。
  3. 在進行 rehash 期間,每次對字典執行 增、刪、改、查操做時,程序除了執行指定的操做外,還會將 哈希表 ht[0].table中下標爲 rehashidx 位置上的全部的鍵值對 所有遷移到 ht[1].table 上,完成後 rehashidx 自增。這一步就是 rehash 的關鍵一步。爲了防止 ht[0] 是個稀疏表 (遍歷好久遇到的都是NULL),從而致使函數阻塞時間太長,這裏引入了一個 「最大空格訪問數」,也即代碼中的 enmty_visits,初始值爲 n*10。當遇到NULL的數量超過這個初始值直接返回。
  4. 最後,當 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這個數字直接用於計算。
相關文章
相關標籤/搜索