1. 程式人生 > 其它 >四、Redis原始碼資料結構之雜湊表Hash

四、Redis原始碼資料結構之雜湊表Hash

Redis作為一款記憶體資料庫,解決記憶體效能問題就顯得尤為重要。作為Hash表這種應用資料結構,當資料量大的時候,就會出現2大問題:雜湊衝突rehash開銷

一、什麼是雜湊衝突以及redis如何解決雜湊衝突

雜湊表是基於陣列的一種儲存方式.它主要由雜湊函式和陣列構成。

當要儲存一個數據的時候,首先用一個函式計算資料的地址,然後再將資料存進指定地址位置的數組裡面。這個函式就是雜湊函式,而這個陣列就是雜湊表。

雜湊表的優勢在於:相比於簡單的陣列以及連結串列,它能夠根據元素本身在第一時間,也就是時間複雜度為0(1)內找到該元素的位置。這使得它在查詢和刪除、插入上會比陣列和連結串列要快很多。因為他們的時間複雜度為o(n)。

雜湊衝突是指雜湊函式算出來的地址被別的元素佔用了,也就是,這個位置有人了。而解決雜湊衝突的辦法之一就是開放定址法(發生衝突,繼續尋找下一塊未被佔用的儲存地址),再雜湊函式法,鏈地址法

雜湊函式用的就是鏈地址法,也就是陣列+連結串列的方法,hashMap用的就是鏈地址法.。鏈地址法就是,當沒有發生雜湊衝突的時候hashmap主要只有陣列。但是當發生衝突的時候,它會在雜湊函式找到的當前陣列記憶體地址位置下新增一條連結串列。

Redis所採用的正是這樣鏈地址法-所謂的鏈式雜湊

相關原始碼檔案:dict.c 和 dict.h

/* 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; 二維陣列 unsigned long size; 表大小 unsigned long sizemask; hash取模的用的 unsigned long used; 元素個數 } dictht; typedef struct dictEntry { // 雜湊實體 void *key; key鍵 union { value值 void *val; uint64_t u64; 無符合 int64_t s64; 有符號
double d; } v; 聯合體V struct dictEntry *next; 下一個雜湊實體 } dictEntry;

補充一句:伴隨著連結串列的增長效能也就隨之變慢了

二、rehash詳細介紹以及Redis如何解決rehash造成的效能問題

rehash是在hash表的大小不能滿足需求,造成過多hash碰撞後需要進行的擴容hash表的操作,

其實通常的做法確實是建立一個額外的hash表,將原來的hash表中的資料在新的資料中進行重新輸入,從而生成新的hash表。

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2]; 2個hash表,交替使用
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */ -1標識無需rehash
    unsigned long iterators; /* number of iterators currently running */
} dict;

1、觸發rehash:

 /* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
/* Incremental rehashing already in progress. Return. */ 正在進行rehashing 則直接返回 if (dictIsRehashing(d)) return DICT_OK; /* If the hash table is empty expand it to the initial size. */ 條件一:如果雜湊表為空,將其擴充套件為初始大小4。 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. */
// 如果我們達到1:1的比例,我們可以調整雜湊標的的大小(全域性設定)或我們應該避免它但之間的比率元素/桶超過“安全”閾值時,我們將大小加倍桶的數量。
if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) 條件二:承載的元素個數大於等於表大小,且允許擴容 dict_can_resize 1 條件三: 承載元素與表大小比率大於安全閾值 dict_force_resize_ratio 5 (負載因子factor) { return dictExpand(d, d->ht[0].used*2); } return DICT_OK; }
void dictEnableResize(void) {
    dict_can_resize = 1; 啟用
}

void dictDisableResize(void) {
    dict_can_resize = 0; 禁止
}
/* This function is called once a background process of some kind terminates,
 * as we want to avoid resizing the hash tables when there is a child in order
 * to play well with copy-on-write (otherwise when a resize happens lots of
 * memory pages are copied). The goal of this function is to update the ability
 * for dict.c to resize the hash tables accordingly to the fact we have o not
 * running childs. */
void updateDictResizePolicy(void) {
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) 該呼叫方法在server.c檔案中,條件:沒有rdb快照、aof重寫場景才啟用rehash
        dictEnableResize();
    else
        dictDisableResize();
}

int hasActiveChildProcess() {
    return server.rdb_child_pid != -1 ||
           server.aof_child_pid != -1;
}

2、rehash擴容大小:

dictExpand函式入參:擴容的表,擴容容量

dictExpand(d, DICT_HT_INITIAL_SIZE); DICT_HT_INITIAL_SIZE 4
dictExpand(d, d->ht[0].used*2);
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size);

    /* 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;
}
/* Our hash table capability is a power of two */
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

3、rehash執行過程:

20220511待更新