我們們接著上一部分來進行分享,我們可以在如下地址下載 redis 的原始碼:
此處我下載的是 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 次 嗎,可以往上看看文章內容
今天就到這裡,學習所得,若有偏差,還請斧正
歡迎點贊,關注,收藏
朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力
好了,本次就到這裡
技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。
我是阿兵雲原生,歡迎點贊關注收藏,下次見~