1. 程式人生 > 其它 >Redis 原始碼簡潔剖析 03 - Dict Hash 基礎

Redis 原始碼簡潔剖析 03 - Dict Hash 基礎

Redis Hash 原始碼

  • dict.h:定義 Hash 表的結構、雜湊項,和 Hash 表的各種函式操作
  • dict.c:函式的具體實現

Redis Hash 資料結構

在 dict.h 檔案中,Hash 表是一個二維陣列(dictEntry **table)。

typedef struct dictht {
    // 二維陣列
    dictEntry **table;
    // Hash 表大小
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

dictEntry **table 是個二維陣列,其中第一維是 bucket,每一行就是 bucket 指向的元素列表(因為鍵雜湊衝突,Redis 採用了鏈式雜湊)。

為了實現鏈式雜湊,Redis 的 dictEntry 結構中,除了包含鍵和值的指標,還包含了一個指向下一個雜湊項的指標 next。

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

整體的雜湊流程都是老生常談了,和 Java 幾乎是一樣的,這裡就不敘述了。

Redis rehash 原理

為什麼要 rehash?

為了效能。如果雜湊表 bucket 的數量是 1,但是裡面有了 1000 個元素,不管怎麼樣都變成了一個連結串列,查詢效率變得很低。同理,當雜湊表裡元素的個數比 bucket 數量多很多的時候,效率也會低很多。

Redis dict 資料結構

Redis 實際使用的是 dict 資料結構,內部用兩個 dictht(ht[0] 和 ht[1]),用於 rehash 使用。

typedef struct dict {
    ……
    // 兩個 Hash 表,交替使用,用於 rehash 操作
    dictht ht[2];
    // Hash 表是否進行 rehash 的標識,-1 表示沒有進行 rehash
    long rehashidx;
    ……
} dict;

Redis rehash 過程

  • 正常請求階段,所有的鍵值對都寫入雜湊表 ht[0]
  • 進行 rehash 時,鍵值對被遷移到 ht[1]
  • 遷移完成後,是否 ht[0] 空間,把 ht[1] 的地址賦值給 ht[0],ht[1] 的表大小設定為 0

什麼時候觸發 rehash?

  • ht[0] 大小=0
  • ht[0] 裡的元素個數已經超過 ht[0] 大小 && Hash 表可以擴容
  • ht[0] 裡的元素個數,是 ht[0] 大小的 5 倍(dict_force_resize_ratio)(類似於 Java 裡 HashMap 的負載因子)
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;

    // Hash 表為空,將 Hash 表擴充套件為初始大小 DICT_HT_INITIAL_SIZE(4)
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    // Hash 表當前的元素數量超過表的大小 && (可以擴容 || 當前數量是表大小的 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) &&
        dictTypeExpandAllowed(d))
    {
        return dictExpand(d, d->ht[0].used + 1);
    }
    return DICT_OK;
}

上面程式碼中有個引數 dict_can_resize,設定函式為:

void dictEnableResize(void) {
    dict_can_resize = 1;
}

void dictDisableResize(void) {
    dict_can_resize = 0;
}

這兩個函式被封裝在了 server.c 中的 updateDictResizePolicy:

void updateDictResizePolicy(void) {
    if (!hasActiveChildProcess())
        dictEnableResize();
    else
        dictDisableResize();
}
/* Return true if there are active children processes doing RDB saving,
 * AOF rewriting, or some side process spawned by a loaded module. */
int hasActiveChildProcess() {
    return server.child_pid != -1;
}

我們可以看到,hasActiveChildProcess 函式是判斷 Redis 存在 RDB 子程序、AOF 子程序是否存在。可以看到 dict_can_resize 只有在不存在 RDB 子程序、AOF 子程序時才為 TRUE。

那 _dictExpandIfNeeded 是在哪裡呼叫的呢?

rehash 擴容多大?

_dictExpandIfNeeded 裡呼叫了擴容函式 dictExpand。

/* return DICT_ERR if expand was not performed */
int dictExpand(dict *d, unsigned long size) {
    return _dictExpand(d, size, NULL);
}
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
    ……
    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size);
    ……
}

裡面有一個 _dictNextPower 函式,啥都不說了,都在註釋裡。

static unsigned long _dictNextPower(unsigned long size) {
    unsigned long i = DICT_HT_INITIAL_SIZE;

    // 要擴容的大小已經超過了最大值
    if (size >= LONG_MAX) return LONG_MAX + 1LU;

    // 要擴容的大小沒有超過最大值,找到第一個比 size 大的 2^i
    while (1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

漸進式 rehash

為什麼需要漸進式 rehash?

Hash 表空間很大,全量 rehash 時間會很長,阻塞 Redis 主執行緒。為了降低 rehash 開銷,Redis 使用了「漸進式 rehash」。

具體一點

漸進式 rehash 並不是一次性把當前 Hash 表的所有鍵,都拷貝到新的位置,而是「分批拷貝」,每次只拷貝 Hash 表中一個 bucket 中的雜湊項。

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    // 迴圈 n 次後停止,或 ht[0] 遷移完成
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        assert(d->ht[0].size > (unsigned long) d->rehashidx);

        // 如果要遷移的 bucket 中沒有元素
        while (d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        // 獲取待遷移的 ht[0] 的 bucket
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while (de) {
            uint64_t h;

            // 獲取下一個遷移項
            nextde = de->next;
            // 計算 de 在 ht[1](擴容後)中的位置
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            // 將當前的雜湊項放到擴容後的 ht[1] 中
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            //指向下一個雜湊項
            de = nextde;
        }
        // 當前 bucket 已經沒有雜湊項了,將該 bucket 設定為 null
        d->ht[0].table[d->rehashidx] = NULL;
        // 將 rehash+1,下次遷移下一個 bucket
        d->rehashidx++;
    }

    // 判斷 ht[0] 是否已經全部遷移
    if (d->ht[0].used == 0) {
        // ht[0] 已經全部遷移到 ht[1] 了,釋放 ht[0]
        zfree(d->ht[0].table);
        // ht[0] 指向 ht[1]
        d->ht[0] = d->ht[1];
        // 重置 ht[1] 大小為 0
        _dictReset(&d->ht[1]);
        //設定全域性雜湊表的 rehashidx=-1,表示 rehash 結束
        d->rehashidx = -1;
        return 0;
    }

    // ht[0] 中仍然有元素沒有遷移完
    return 1;
}

幾點說明:

  • rehashidx 表示當前 rehash 在對哪個 bucket 做資料遷移,每次遷移完對應 bucket 時,會將 rehashidx+1。
  • empty_visits 表示連續 bucket 為空的情況,此時漸進式 rehash 不會一直遞增檢查 rehashidx,因為一直檢測會阻塞主執行緒,Redis 主執行緒就無法處理其他請求了。

那麼 rehash 是在什麼哪些步驟進行操作的呢?檢視原始碼發現 dictRehash 是在 _dictRehashStep 函式中呼叫的,且傳入的 n=1。

static void _dictRehashStep(dict *d) {
    if (d->pauserehash == 0) dictRehash(d,1);
}

而 _dictRehashStep 分別被 5 個方法呼叫了:

  • dictAddRaw
  • dictGenericDelete
  • dictFind
  • dictGetRandomKey
  • dictGetSomeKeys

下面是 dictAddRaw 部分程式碼:

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    ……
    if (dictIsRehashing(d)) _dictRehashStep(d);
    ……
}

下面是 dictAdd 部分程式碼:

int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}

Redis 原始碼簡潔剖析系列

最簡潔的 Redis 原始碼剖析系列文章

Java 程式設計思想-最全思維導圖-GitHub 下載連結,需要的小夥伴可以自取~

原創不易,希望大家轉載時請先聯絡我,並標註原文連結。