redis 儲存結構原理 2

阿兵雲原生發表於2022-07-15

我們們接著上一部分來進行分享,我們可以在如下地址下載 redis 的原始碼

https://redis.io/download

此處我下載的是 redis-6.2.5 版本的,xdm 可以直接下載上圖中的 redis-6.2.6 版本,

redis 中 hash 表的資料結構

redis hash 表的資料結構定義在:

redis-6.2.5\src\dict.h

雜湊表的結構,每一個字典都有兩個實現從舊錶到新表的增量重雜湊

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

table:

table 是一個二級指標,對應這一個陣列,陣列中的每個元素都是指向了一個 dictEntry 結構體指標的,dictEntry 具體的資料結構是儲存一個鍵值對

具體的 dictEntry 資料結構是這樣的:

size:

size 屬性是記錄了整個 hash 表的大小,也可以理解為上述 table 陣列的大小

sizemask:

sizemask 屬性,和具體的 hash 值來一起決定鍵要放在 table 陣列的哪個位置

sizemask 的值,總是會比 size 小 1 ,我們可以來演示一下

使用取餘的方式,實際上是很低效的,我們們的計算機是不會做乘除法的,同樣都是用加減法來進行處理的,為了高效處理,我們可以使用二進位制的方式

使用二進位制的方式,就會用到該欄位 sizemask ,主要是用來 和 具體的 hash 值做按位與操作

如圖就很明確了, size = 4,sizemask = 3,hash 值為 7, 最後 hash 值 & sizemask = 0011, 也就是 3,因此就會插入到上圖的具體位置

used:

used 屬性表示 hash 表裡面已經有鍵值對的數量

對於上述的案例,可以用一個簡圖來表示一下 hash 表結構 dictht

dictEntry 結構每個屬性的含義

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

上面我們看到陣列中的節點資訊,是 dictEntry 結構,屬性分別是這些意思:

  • key

    具體的 redis 鍵

  • union v

    • val

      指向不同型別的資料,此處是 void * ,使用該型別,是為了節省記憶體

    • u64

      用於 redis 叢集中的哨兵模式和選舉模式

    • s64

      記錄過期時間的

  • next

    指向下一個節點的指標

dict 結構

src\dict.h 檔案中,我們們接著往下看,能夠看到一個非常關鍵的結構,就是 dict ,redis 中都是使用這個結構來進行組織的

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;
  • type

欄位對應的操作函式,具體有哪些操作函式,我們可以看到typedef struct dictType 給出的資訊

  • privdata

字典依賴的資料,例如 redis 具體的操作等等

  • ht[2]

hash 表的鍵值對,放在此處,一箇舊的,一個新的

ht[0] :是擴容前的陣列

ht[1]:是擴容後的陣列

這個是當資料量大的時候,用於漸進式 rehash 的

  • rehashidx

來指定具體 rehash 的位置,對應到 ht[0] 的索引上,rehashidx == -1 ,就是沒有進行再 hash , rehashidx != -1 時,說明正在進行再 hash

還記得我們之前說到 redis 有 16 個 db 嗎?

我們在 redis 原始碼中 src\server.h 也能夠看到 redisdb 的資料結構

我們可以看到 dict 這個字典,是 redis 中使用是相當頻繁和關鍵的

上面有說到 ht[2] 會用在漸進式 rehash 上,那麼為什麼要用漸進式 rehash 以及他是如何做的?

擴容的時候,會觸發 rehash

當資料量很大的時候,會涉及到擴容,若一次性從 ht[0] 拷貝到 ht[1] 是比較慢的,會阻塞其他操作,那麼就沒有辦法處理其他請求了,因為 redis 是單執行緒處理任務的

ht[0] 資料拷貝到 ht[1] 的方式一

是這樣進行 rehash 的

擴容的時候,rehash 是這樣做的:

  • 先會對上述說到的 ht[1] 開闢記憶體空間,會將 ht[0].size * 2 給到 ht[1]
  • 然後再將 ht[0] 的資料,從 ht[0][0] ... ht[0][size-1] 將資料拷貝到 ht[1] 裡面

如何做到漸進式呢?

使用分而治之的思想,無論 redis 目前是否在做持久化的時候,當我們每次操作 redis 增刪改查,就會進行邊列舉邊篩查的方式,逐步的將 ht[0][0] ... ht[0][size-1] rehash 到 ht[1] 中

可以追一下程式碼流程 , 我們從 src\server.c 註冊 setCommand 命令開始追起,程式碼設計關鍵流程如下

當追到 dictAddRaw 函式的時候,我們可以清晰的看出來,當 redis 加入資料的時候,使用的是頭插法

  • 先對新的節點開闢相應的記憶體
  • 將新建節點的 next 物件指向連結串列的頭
  • 然後將連結串列的頭指向新建的節點地址,即完成了一次 頭插

此處我們可以看到,實際上是做了一次 rehash

追到 dictRehash 函式的時候,可以看到此處的再 hash 函式 dictRehash,我們可以看到 rehash 的做法是:

  • 在 ht[0] 陣列中,取得 rehashidx 對應的桶,或者腳陣列對應的索引位置
  • 通過上述找到的索引位置,取 ht[0].table[d->rehashidx] 對應的連結串列
  • 然後將連結串列中的資料依次進行 rehash

此處 dictRehash 的 n 的引數,表示再 hash 的次數,再 hash 1 次,表示對於陣列的這個桶對應的連結串列上的所有資料,進行一輪 hash

可以看到程式碼中

 /* Get the index in the new hash table */
  h = dictHashKey(d, de->key) & d->ht[1].sizemask;

此處正是 dictHashKey 計算出一個整數,然後和我們 dictht 中的 sizemask 進行一次按位與操作 , 旨在得到一個新的 hash 表索引位置

redis 呼叫 _dictRehashStep 的位置

通過檢視程式碼中呼叫 _dictRehashStep 函式的位置並不多,我們一次檢視呼叫關係,我們會知道確實是當我們每次操作 redis 增刪改查的時候,會發生漸進式的 rehash , 這些是在我們進行擴容之後,如何將 ht[0] 的資料拷貝到 ht[1] 的實現方式

實際 redis 中涉及到如上幾個函式 都會呼叫 _dictRehashStep:

  • dictAddRaw
  • dictGenericDelete
  • dictFind
  • dictGetRandomKey
  • dictGetSomeKeys

ht[0] 資料拷貝到 ht[1] 的方式二

定時器呼叫 dictRehash的邏輯

當 redis 中沒有持久化操作的時候,redis 中的定時操作就會就會走定時的邏輯,邏輯是這樣的

我們可以在 redis 原始碼中搜尋使用 dictRehash 函式的位置

使用的位置也並不多,我們很容易就能找到按照毫秒級別來定時操作的位置

dictRehashMilliseconds

此處的邏輯是,while 迴圈是以 100 次 rehash 為一輪,時間限制是 1ms,只要時間不超過 1ms,能做的 rehash 次數至少是 100 次(每一輪 100 次),若超過 1 ms,則會立刻結束本次定時操作

此處我們可以看到,dictRehash(d,100) 傳遞的引數是 100,表示 rehash 100 次,還記得之前的漸進式 rehash 是 傳入的 1 次 嗎,可以往上看看文章內容

今天就到這裡,學習所得,若有偏差,還請斧正

歡迎點贊,關注,收藏

朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力

好了,本次就到這裡

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是阿兵雲原生,歡迎點贊關注收藏,下次見~

相關文章