Redis原理再學習04:數據結構-哈希表hash表(dict字典)


哈希函數簡介

哈希函數(hash function),又叫散列函數,哈希算法。散列函數把數據“壓縮”成摘要,有的也叫”指紋“,它使數據量變小且數據格式大小也固定。

哈希函數將數據打亂混合,重新創建一個散列值。

我們經常用到的對用戶登錄密碼加密,比如 md5 算法,其實就是一個散列函數。

value = hash_function(input_data),value 這個計算出來的值是大小固定的。

md5("hashmd5") = 46BD4AA9F79D359530D3D873BAC6F3DC,32 位的 md5 值。

當然也有 16 位的 md5 值。

經過哈希函數計算的散列值,會不會出現散列值相同情況?

當然會,這個就是散列值沖突

所以一個好的哈希函數就很重要,要盡量避免出現散列值沖突。

常用的哈希算法:md5,sha-1,sha-256,sha-512 等等。

哈希表簡介

哈希表可以有很多英文名稱,比如 hashtable,hashmap,symbol table,map 等等,英文名稱雖然不同,但是數據結構基本差不多。

在 map 中,就是一種映射關系。一般保存 key:value 的鍵值對映射關系。

在哈希表中,key 經過哈希函數計算后存儲到哈希表中,然后與 value 值關聯對應。

哈希表的結構組成:數組array + 鏈表list。是一個組合結構。

比如:key:value 值,數組用來存儲 key 經過哈希函數計算后的值與數組長度取余后的值,鏈表存儲 key:value 值。

如下圖:

上圖為什么是 2 個 key:val 在一起?

其實這就是 hash 沖突了,用鏈地址表來解決哈希沖突的問題。

Redis中的哈希表和字典dict

1. 哈希表各結構定義

哈希表dictht

redis3.0 中的哈希表叫 dictht,dictht 的定義:

// https://github.com/redis/redis/blob/3.0/src/dict.h#L69

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht { // 哈希表
    dictEntry **table; // 哈希表的數組,數組中每個元素都是指針,指向 dictEntry 結構
    unsigned long size; // 哈希表的大小,table 數組的大小
    unsigned long sizemask; // 哈希表掩碼,用於計算索引值,等於 size-1
    unsigned long used; // 哈希表已有的節點(鍵值對)數量
} dictht;

哈希表節點dictEntry

哈希表節點,有的地方取名為哈希桶 bucket,節點 Node 等等,不過表達意思是一樣的。

上面 redis3.0 哈希表 dictht 里的節點 dictEntry 是怎么定義? 代碼如下:

// https://github.com/redis/redis/blob/3.0/src/dict.h#L47
typedef struct dictEntry {
    void *key;  // 鍵 key
    union { // 值 val
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; // 指向下一個哈希表節點,鏈表法解決hash沖突
} dictEntry;

key 屬性保存鍵值對中的鍵,v 屬性保存鍵值對中的值,其中這個 v 值可能是一個指針,或者是一個 uint64_t 整數,或者是 int64_t 整數,或是 double 類型浮點數。

dictEnty 表節點和 dictht 哈希表結構關系如下圖:

next:指向下一個哈希節點,用鏈表法來解決哈希沖突。

hash沖突:

上面的 dictEntry 結構里的屬性 next 就是解決這個哈希鍵沖突問題的。

有沖突的值,就用鏈表來記錄下一個值。

哈希算法

Redis 中計算哈希值的哈希函數有好幾個。

  1. dictIntHashFunction 計算整型類型哈希值的哈希函數

    unsigned int dictIntHashFunction(unsigned int key)
    
  2. dictGenHashFunction MurmurHash2 哈希算法, by Austin Appleby,用於計算字符串的哈希值的哈希函數

    unsigned int dictGenHashFunction(const void *key, int len)
    
  3. dictGenCaseHashFunction djb 哈希算法,大小寫敏感的哈希函數

    /* And a case insensitive hash function (based on djb hash) */
    unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len)
    

2. 字典dict

字典dict

上面我們已經了解,在 Redis 中用 dictht 來表示哈希表,但是,在使用哈希表時,Redis 又定義了一個字典 dict 的數據結構。

為什么要再定義一個 dict 結構?

  • 為了擴展哈希表(rehash)的時候,能夠方面的操作哈希表。為此里面定義了 2 個哈希表 ht[2]。

字典 dict.h/dict 結構定義:

typedef struct dict {
    dictType *type; // 指針,指向dictType 結構,dictType 中包含很多自定義函數,見下面
    void *privdata; // 私有數據,保存dictType結構中的函數參數
    dictht ht[2]; // hash表,ht[2] 表示有2張表
    long rehashidx; /* rehashing not in progress if rehashidx == -1 *///rehash 標識,rehashidx=-1,沒進行rehash
    int iterators; /* number of iterators currently running */// 正在運行的迭代器數量
} dict;

*type:保存了很多函數,這些函數是操作特定類型鍵值對的函數,Redis 會為用途不同的字典設置不同類型特定函數。

ht[2]:包含 2 個 dictht哈希表,為什么有2張表?rehash 時會用到 ht[1]。一般情況下只使用 ht[0]。

rehashidx:這個屬性與 rehash 有關,記錄 rehash 目前的進度,如果目前沒有進行 rehash,那么 rehashidx=-1。

dict.h/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;

字典 dict 圖示:

3. rehash

a. 什么是 rehash ?

  • 擴大或縮小哈希表容量。

b. 為什么有 rehash ?

  • 當哈希表的數據量持續增長,而哈希表容量大小固定時,就可能會有 2 個或以上數量的鍵被分配到哈希表數組的同一個索引上,於是就發生了沖突(collision)。
  • 當然沖突可以用鏈表法(separate chaining)解決,但是為了哈希表的性能,要盡量避免沖突,就要對哈希表進行擴容或縮容。

哈希表中有一個負載因子(load factor)的概念:

負載因子 = 哈希表已保存的鍵值對數量(使用的數量) / 哈希表的長度

load_factor = ht[0].used / ht[0].size

這個負載因子的概念是用來衡量哈希表容量大小情況的。哈希表中的鍵值對數量少,負載因子也小。

當負載因子超過某個闕值時,為了維持哈希的容量在一定合理范圍,就會對哈希表容量進行 resize 操作:

  1. 擴大哈希表容量
  2. 縮小哈希表容量

c. 什么時候進行擴容和縮容操作?

  • 擴容條件

    滿足下面任一條件都會觸發哈希表擴容

    1. 服務器目前沒有執行 bgsave 命令,或 bgrewriteaof 命令,並且哈希表的負載因子 >=1

    2. 服務器目前在執行 bgsave 命令,或 bgrewriteaof 命令並且哈希表的負載因子 >5

  • 縮容條件

    1. 哈希表的負載因子 < 0.1

d. 怎么操作擴容和縮容?

也就是說擴容和縮容的操作步驟是什么?

  1. 為字典 ht[1] 分配內存空間,空間大小取決於要執行的操作,以及當前 ht[0] 的鍵值對數量

    • 如果是擴容操作,那么 ht[1] 的空間大小等於第一個 ht[0].used * 2 的 2^n(2的n次冪)

    • 如果是縮容操作,那么 ht[1] 的空間大小等於第一個 ht[0].used 的 2^n(2的n次冪)

  2. 將 ht[0] 上所有鍵值重新計算哈希值和索引值后存放到 ht[1] 對應位置上

  3. 當 ht[0] 上所有的鍵值移動到 ht[1] 后,釋放 ht[0],將 ht[1] 變成 ht[0],並在 ht[1] 上新建一個空哈希表

擴容代碼簡析:

_dictExpandIfNeeded

// https://github.com/redis/redis/blob/3.0/src/dict.c#L923

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK; // 如果正在進行rehash,則返回

    /* If the hash table is empty expand it to the initial size. */
    // 如果 ht[0] 為空,則創建並初始化ht[0],然后返回
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    /*當 (ht[0].used/ht[0].size)>=1 並且,
       滿足dict_can_resize=1或ht[0].used/ht[0].size>5時,對字典進行擴容*/ 
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

// https://github.com/redis/redis/blob/3.0/src/dict.c#L58
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;

dictExpand:

// https://github.com/redis/redis/blob/3.0/src/dict.c#L204
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
    dictht n; /* the new hash table 新建一個哈希表*/
    unsigned long realsize = _dictNextPower(size); // 計算擴容或縮容新版哈希表大小

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    // 如果哈希表正在rehash或新建哈希表大小小於現已使用的,則返回錯誤
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

縮容操作:

dictResize

// https://github.com/redis/redis/blob/3.0/src/dict.c#L192
int dictResize(dict *d)
{
    int minimal;

    // dict_can_resize 在 https://github.com/redis/redis/blob/3.0/src/dict.c#L58 這里是設置為 1,如果為0就返回,不進行后面操心
    // 或者 dictIsRehashig() 真正進行rehash操心,也返回不rehash操作
    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht[0].used; // 獲得已經使用ht的數量
    if (minimal < DICT_HT_INITIAL_SIZE) // 這個最小值不能小於 DICT_HT_INITIAL_SIZE = 4
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal); // 用dictExpand函數調整字典大小
}

// https://github.com/redis/redis/blob/3.0/src/dict.h#L100
/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_SIZE     4

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM