baiyanphp
所有視頻:【每日學習記錄】使用錄像設備記錄天天的學習redis
dict,即字典,也被稱爲哈希表hashtable。在redis的五大數據結構中,有以下兩種情形會使用dict結構:算法
- hash:數據量小的時候使用ziplist,量大時使用dict
- zset:數據量小的時候使用ziplist,數據量大的時候使用skiplist + dict
結合以上兩種狀況,咱們能夠看出,dict也是一種較爲複雜的數據結構,一般用在數據量大的情形中。一般狀況下,一個dict長這樣:
在這個哈希表中,每一個存儲單元被稱爲一個桶(bucket)。咱們向這個dict(hashtable)中插入一個"name" => "baiyan"的key-value對,假設對這個key 「name」作哈希運算結果爲3,那麼咱們尋找這個hashtable中下標爲3的位置並將其插入進去,獲得如圖所示的情形。咱們能夠看到,dict最大的優點就在於其查找的時間複雜度爲O(1),是任何其它數據結構所不能比擬的。咱們在查找的時候,首先對key 」name「進行哈希運算,獲得結果3,咱們直接去dict索引爲3的位置進行查找,便可獲得value 」baiyan「,時間複雜度爲O(1),是至關快的。編程
在redis中,在普通字典的基礎上,爲了方便進行擴容與縮容,增長了一些描述字段。仍是以上面的例子爲基礎,在redis中存儲結構以下圖所示:
dictht的結構以下:segmentfault
typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht;
在dictht中,真正存儲數據的地方是**table這個dictEntry類型二級指針。咱們能夠把它拆分來看,首先第一個指針能夠表明一個一維數組,即哈希表。然後面的指針表明,在每一個一維數組(哈希表)的存儲單元中,存儲的都是一個dictEntry類型的指針,這個指針就指向咱們存儲key-value對的dictEntry類型結構的所在位置,如上圖所示。
存儲最終key-value對的dictEntry的結構以下:數組
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry;
一個存儲key-value對的entry,最主要仍是這裏的key和value字段。因爲存儲在dict中的key和value能夠是字符串、也能夠是整數等等,因此在這裏均用一個void * 指針來表示。咱們注意到最後有一個也是同類型dictEntry的next指針,它就是用來解決咱們常常說的哈希衝突問題。數據結構
當咱們對不一樣的key進行哈希運算以後結果相同時,就碰到了哈希衝突的問題。經常使用的兩種哈希衝突的解決方案有兩種:開放定址法與鏈地址法。redis使用的是後者。經過這個next指針,咱們就能夠將哈希值相同的元素都串聯起來,解決哈希衝突的問題。注意在redis的源碼實現中,在往dict插入元素的時使用的是鏈表的頭插法,即將新元素插到鏈表的頭部,這樣就不用每次遍歷到鏈表的末尾進行插入,下降了插入的時間複雜度。架構
假設咱們一直往dict中插入元素,那麼這個哈希表的全部bucket都會被佔滿,並且在鏈地址法解決哈希衝突的過程當中,每一個bucket後面的鏈表會很是長。這樣一來,這個鏈表的時間複雜度就會逐漸退化成O(n)。對於總體的dict而言,其查詢效率就會大大下降。爲了解決數據量過大致使dict性能降低的問題,咱們須要對其進行擴容,來知足後續插入元素的存儲須要。性能
typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehash進程標識。若是值爲-1則不在rehash,不然在進行rehash */ unsigned long iterators; /* number of iterators currently running */ } dict;
int dictRehash(dict *d, int n) { int empty_visits = n*10; /* Max number of empty buckets to visit. */ if (!dictIsRehashing(d)) return 0; while(n-- && d->ht[0].used != 0) { dictEntry *de, *nextde; /* Note that rehashidx can't overflow as we are sure there are more * elements because ht[0].used != 0 */ assert(d->ht[0].size > (unsigned long)d->rehashidx); while(d->ht[0].table[d->rehashidx] == NULL) { d->rehashidx++; if (--empty_visits == 0) return 1; } de = d->ht[0].table[d->rehashidx]; /* 將老的哈希表ht[0]中的元素移動到新哈希表ht[1]中 */ while(de) { uint64_t h; nextde = de->next; /* 計算新哈希表ht[1]的索引下標*/ h = dictHashKey(d, de->key) & d->ht[1].sizemask; //哈希算法 de->next = d->ht[1].table[h]; d->ht[1].table[h] = de; d->ht[0].used--; d->ht[1].used++; de = nextde; } d->ht[0].table[d->rehashidx] = NULL; d->rehashidx++; } /* 檢查是否rehash完成,若完成則置rehashidx爲-1 */ if (d->ht[0].used == 0) { zfree(d->ht[0].table); d->ht[0] = d->ht[1]; _dictReset(&d->ht[1]); d->rehashidx = -1; return 0; } /* More to rehash... */ return 1; }
若是在rehash的過程當中(例如容量由4擴容到8),若是須要查找一個元素。首先咱們會計算哈希值(假設爲3)去找老的哈希表ht[0],若是咱們發現位置3上已經沒有了元素,說明這個元素已經被rehash過了,到新的哈希表上對應的位置3或7上尋找便可。學習
試想這麼一種狀況:在rehash以前,咱們使用SCAN命令對dict進行第一次遍歷;而rehash結束以後咱們進行第二次SCAN遍歷,會發生什麼狀況?
在討論這個問題以前,咱們先熟悉一下SCAN命令。咱們知道在咱們執行keys這種返回全部key值的命令,因爲全部key加在一塊是至關多的,若是一次性所有把它遍歷完成,可以讓單進程的redis阻塞至關長的時間,在這段時間裏都沒法對外提供服務。爲了解決這個問題,SCAN命令橫空出世。它並非一次性將全部的key都返回,而是每次返回一部分key並記錄一下當前遍歷的進度,這裏用一個遊標去記錄。下次再次運行SCAN命令的時候,redis會從遊標的位置開始繼續往下遍歷。SCAN命令實際上也是一種分而治之的思想,這樣一次遍歷一小部分,直到遍歷完成。SCAN命令官方解釋以下:
SCAN 命令是一個基於遊標的 迭代器: SCAN 命令每次被調用以後, 都會向用戶返回一個新的遊標,用戶在下次迭代時須要使用這個新遊標做爲 SCAN 命令的遊標參數, 以此來延續以前的迭代過程。
SCAN命令的使用方法以下:
redis 127.0.0.1:6379> scan 0 1) "17" 2) 1) "key:12" 2) "key:8" 3) "key:4" 4) "key:14" 5) "key:16" 6) "key:17" 7) "key:15" 8) "key:10" 9) "key:3" 10) "key:7" 11) "key:1" redis 127.0.0.1:6379> scan 17 1) "0" 2) 1) "key:5" 2) "key:18" 3) "key:0" 4) "key:2" 5) "key:19" 6) "key:13" 7) "key:6" 8) "key:9" 9) "key:11"
- 在上面這個例子中,第一次迭代使用0做爲遊標,表示開始一次新的迭代。第二次迭代使用的是第一次迭代時返回的遊標,也便是命令回覆第一個元素的值17 。
- 從上面的示例能夠看到, SCAN 命令的回覆是一個包含兩個元素的數組,第一個數組元素是用於進行下一次迭代的新遊標,而第二個數組元素則是一個數組,這個數組中包含了全部被迭代的元素。
- 在第二次調用 SCAN 命令時,命令返回了遊標0,這表示迭代已經結束,整個數據集(collection)已經被完整遍歷過了。
- 以0做爲遊標開始一次新的迭代,一直調用 SCAN 命令,直到命令返回遊標0,咱們稱這個過程爲一次完整遍歷。
回到正題,咱們來解決以前的問題。 咱們簡化一下dict的結構,只留下兩個基本的哈希表結構,咱們如今有4個元素:十二、1三、1四、15,假設哈希算法爲取餘。
那麼咱們將兩次SCAN的結果合起來,爲十二、十二、1三、1四、15。咱們發現,元素12被多遍歷了一次,與咱們的預期不符。因此咱們得出結論:在rehash過程當中執行SCAN命令會致使遍歷結果出現冗餘。
爲了解決擴容和縮容進行rehash的過程當中重複遍歷的問題,redis對哈希表的下標作出了以下變化(v就是哈希表的下標):
v = rev(v); v++; v = rev(v);
首先將遊標倒置,加一後,再倒置,也就是咱們所說的「高位++」的操做。這裏的這幾步操做是來經過前一個下標,計算出哈希表下一個bucket的下標。舉一個例子:最開始00這個bucket不用動,以前通過正常的低位++以後,00的後面應該爲01。然而如今是高位++,原來01的位置的下標就會變成10.......以此類推。最終,哈希表的下標就會由原來順序的00、0一、十、11變成了00、十、0一、11,如圖所示:
這樣就可以保證咱們屢次執行SCAN命令就不會重複遍歷了嗎?接下來就是見證奇蹟的時刻:
開始進行rehash
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, dictScanBucketFunction* bucketfn, void *privdata) { dictht *t0, *t1; const dictEntry *de, *next; unsigned long m0, m1; if (dictSize(d) == 0) return 0; // 若是SCAN的時候沒有進行rehash if (!dictIsRehashing(d)) { t0 = &(d->ht[0]); m0 = t0->sizemask; /* Emit entries at cursor */ if (bucketfn) bucketfn(privdata, &t0->table[v & m0]); de = t0->table[v & m0]; while (de) { //遍歷同一個bucket上後面掛接的鏈表 next = de->next; fn(privdata, de); de = next; } /* Set unmasked bits so incrementing the reversed cursor * operates on the masked bits */ v |= ~m0; /* Increment the reverse cursor */ v = rev(v); //反轉v v++; //反轉以後即爲高位++ v = rev(v); //再反轉回來,獲得下一個遊標值 // 若是SCAN的時候正在進行rehash } else { t0 = &d->ht[0]; t1 = &d->ht[1]; /* Make sure t0 is the smaller and t1 is the bigger table */ if (t0->size > t1->size) { t0 = &d->ht[1]; t1 = &d->ht[0]; } m0 = t0->sizemask; m1 = t1->sizemask; /* Emit entries at cursor */ if (bucketfn) bucketfn(privdata, &t0->table[v & m0]); de = t0->table[v & m0]; while (de) { //遍歷同一個bucket上後面掛接的鏈表 next = de->next; fn(privdata, de); de = next; } /* Iterate over indices in larger table that are the expansion * of the index pointed to by the cursor in the smaller table */ do { /* Emit entries at cursor */ if (bucketfn) bucketfn(privdata, &t1->table[v & m1]); de = t1->table[v & m1]; while (de) { //遍歷同一個bucket上後面掛接的鏈表 next = de->next; fn(privdata, de); de = next; } /* Increment the reverse cursor not covered by the smaller mask.*/ v |= ~m1; v = rev(v); //反轉v v++; //反轉以後即爲高位++ v = rev(v); //再反轉回來,獲得下一個遊標值 /* Continue while bits covered by mask difference is non-zero */ } while (v & (m0 ^ m1)); } return v; }
有關rehash過程對SCAN的影響,限於篇幅僅僅展現這種狀況。更多的情形請參考:Redis scan命令原理