redis字典結構終結篇你肯定不看嗎?

這是我參與更文挑戰的第2天,活動詳情查看: 更文挑戰redis

概述:

在redis中有不少地方都用到了字典結構,好比咱們redis中的哈希結構就是用的字典實現的,而字典結構的底層事使用哈希表實現的(😄有點繞~),XDM能夠經過這篇文章瞭解到如下知識點。算法

  1. 哈希表、哈希表節點、以及字典的結構和實現。數組

  2. 哈希算法redis是如何解決哈希衝突的。服務器

  3. redis的rehash原理和實現markdown

哈希表、哈希表節點、以及字典的結構和實現

哈希表

哈希表的結構是這個樣子的:函數

typedef struct dictht {
    // 哈希表數組
    dictEntry **table;(這個是指向指針的指針)
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩碼,用於計算索引值
    // 老是等於 size - 1
    unsigned long sizemask;
    // 該哈希表已有節點的數量
    unsigned long used;
} dictht;
複製代碼

table是一個指針數組,他裏面的每一個元素都指向一個哈希表節點dictEntry。post

size 是當前哈希表的大小,也就是咱們table數組中元素數性能

used 記錄了哈希表目前已有節點(鍵值對)的數量ui

sizemask 屬性的值老是等於size - 1 , 這個屬性和哈希值一塊兒決定一個鍵應該被放到數組的哪一個索引上面。這個若是沒有記錯的話應該是用來rehash使用的。spa

哈希表節點

typedef struct dictEntry {
    // 鍵
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下個哈希表節點,造成鏈表
    struct dictEntry *next;
} dictEntry;
複製代碼

key就是咱們哈希的key,指向了一個SDS對象或者是一個整數。

next是一個指針指向一個dictEntry對象,這個是爲了解決哈希衝突的,當哈希衝突產生的時候,全部衝突的key的就會造成一條鏈表。

字典

typedef struct dict {
    // 類型特定函數
    dictType *type;
    // 私有數據
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 當 rehash 不在進行時,值爲 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;
複製代碼

type 是一個指向dictType結構的指針,每一個dictType中都包含了操做不一樣類型字符串的方法,privdata則保存了這些方法須要的參數

dictType的結構以下:

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;
複製代碼

咱們能夠看到還有兩個字段 ht[2]和rehashidx,正常狀況下咱們的數據都是在ht[0]中的。那ht[1]只有在rehash的時候纔會使用,而rehashidx就是咱們的rehash進度,當rehashidx爲-1的時候就說明當前哈希表沒有進行rehash操做。在redis源碼中咱們能夠看到這種判斷。

這是一個沒有進行rehash的普通字典結構:

哈希算法redis是如何解決哈希衝突的

當兩個或者多個的key經過哈希算法算出來的值是同樣的時候咱們就稱這種是出現了哈希衝突,在上邊介紹基本結構的時候就提到過這個next是一個指針指向一個dictEntry對象,當哈希衝突產生的時候,全部衝突的key的就會造成一條鏈表。當查詢的時候就會按照這個鏈表一個一個日後查找到和須要獲取的key相等的值,而後返回。

redis的rehash原理和實現

負載因子

首先咱們要先介紹一下負載因子的概念,在咱們對哈希表的增長和刪除操做的過程當中會致使咱們的哈希表不斷增大。從而致使ht[0].used / ht[0].size 比例失衡 這兩個的概念須要看下上邊的定義。

咱們能夠想一下這比例在何時這個哈希表是最優狀態?

應該是1,由於若是是1空間沒有浪費,也說明沒有那麼多的哈希衝突。

redis會根據這個比例判斷咱們哈希表是要擴展仍是收縮。接下來咱們大概看一下這個rehash的過程。

  1. 這第一步就要使用到咱們的ht[1],先爲它分配空間。具體分配多大的空間要取決於咱們是要執行擴展仍是收縮操做,還有就是當前ht[0]中的元素數量。(擴展操做ht[1]的大小是第一個大於等於ht[0].used * 2^n 若是是收縮操做大小爲第一個大於等於ht[0].used的2^n。 這個咱們解釋一下,好比當前咱們的used是6,咱們須要申請的空間就是(6*2 = 12)<2^4是16。應該均可以理解哈~)。
  2. 而後就是將咱們ht[0]上的key使用新的從新計算哈希值和索引值,並按照新的索引值放到咱們的ht[1]上。
  3. 當咱們的ht[0]上的全部鍵值對都遷移到ht[1]以後ht[0]是空表了,此時將ht[0]指向ht[1] ht[1]指向null 至此咱們的rehash就完成了。

何時會進行rehash呢?

  1. 服務器目前沒有在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 而且哈希表的負載因子大於等於1進行擴展操做

  2. 服務器正在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 而且哈希表的負載因子大於等於5進行擴展操做。

  3. 當哈希表的負載因子小於0.1時, 程序自動開始對哈希表執行收縮操做

具體爲何 BGSAVE 命令或者 BGREWRITEAOF 命令執行期間負載因子的判斷值不同,我猜想是由於性能的考慮,xdm能夠查一下相關資料評論下

咱們上邊說的rehash並非一下就所有完成,若是咱們有個很大的哈希表那rehash過程是要消耗很長時間的咱們的redis是單線程,這種消耗主線程時間的邏輯很致命的。redis在這個地方的設計真的是很優雅,xdm確定發現咱們還有一個rehashidx字段沒用到,字段就是咱們後邊要寫的漸進式rehash中要使用到的重要內容。

總結

xdm這篇先到這,主要整理了下redis字典底層哈希表的實現,以及rehash的過程。接下來會整理一下redis的優雅設計漸進式rehash和他的源碼實現。

敬請期待~

相關文章
相關標籤/搜索