Redis源码解析——字典结构
? ? ? ? C++語(yǔ)言中有標(biāo)準(zhǔn)的字典庫(kù),我們可以通過(guò)pair(key,value)的形式存儲(chǔ)數(shù)據(jù)。但是C語(yǔ)言中沒(méi)有這種的庫(kù),于是就需要自己實(shí)現(xiàn)。本文講解的就是Redis源碼中的字典庫(kù)的實(shí)現(xiàn)方法。(轉(zhuǎn)載請(qǐng)指明出于breaksoftware的csdn博客)
? ? ? ? 一般情況下,我們談到字典,難免要談到紅黑樹(shù)。但是Redis這套字典庫(kù)并沒(méi)有使用該方案去實(shí)現(xiàn),而是使用的是鏈表,且整個(gè)代碼行數(shù)在1000行以內(nèi)。所以這塊邏輯還是非常好分析的。
? ? ? ? 我們可以想象下,如果使用普通的鏈表去實(shí)現(xiàn)字典,那么是不是整個(gè)數(shù)據(jù)都在一條鏈表結(jié)構(gòu)上呢?如果是這么設(shè)計(jì),插入和刪除操作是非常方便的,但是查找操作可能就非常耗時(shí)——需要從前向后一個(gè)個(gè)遍歷對(duì)比。很顯然不能采用這種方案。于是有一種替代性的方案,就是使用數(shù)組去存儲(chǔ),然后通過(guò)下標(biāo)去訪問(wèn)。因?yàn)橄聵?biāo)操作就是指針的移動(dòng),所以查找元素變得非常快。相應(yīng)的問(wèn)題便是如何將數(shù)據(jù)的Key轉(zhuǎn)換成數(shù)組下標(biāo)?
? ? ? ? 一種比較容易想到的就是使用Key對(duì)應(yīng)的二進(jìn)制碼作為下標(biāo)。比如我們要保存pair(1,"String1"),則使用其Key的值1對(duì)應(yīng)的二進(jìn)值1作為下標(biāo);再比如pair('A',"stringA"),則使用A字符對(duì)應(yīng)的編碼十進(jìn)制值65作為下標(biāo)。這種設(shè)計(jì)方法固然簡(jiǎn)單,但是有個(gè)非常現(xiàn)實(shí)的問(wèn)題——到底要分配多大的數(shù)組?上面兩個(gè)例子還比較簡(jiǎn)單,我們看個(gè)稍微復(fù)雜的例子,比如要保存pair("AAAA","StringAAA"),則AAAA的二進(jìn)制編碼對(duì)應(yīng)的十進(jìn)制是65656565,難道我們要分配那么大的數(shù)組?想想也不可能,因?yàn)槲覀兺枰4娴臄?shù)據(jù)比上面這些例子還要復(fù)雜很多。如果這么設(shè)計(jì),我們的內(nèi)存可能是否不夠分配的,且其使用率也非常低。那怎么解決呢?于是我們就要提到Hash算法了。
? ? ? ? 我們看下Hash中文定義:Hash,一般翻譯做“散列”,也有直接音譯為“哈希”的,就是把任意長(zhǎng)度的輸入(又叫做預(yù)映射, pre-image),通過(guò)散列算法,變換成固定長(zhǎng)度的輸出,該輸出就是散列值。這種轉(zhuǎn)換是一種壓縮映射,也就是,散列值的空間通常遠(yuǎn)小于輸入的空間,不同的輸入可能會(huì)散列成相同的輸出,所以不可能從散列值來(lái)唯一的確定輸入值。簡(jiǎn)單的說(shuō)就是一種將任意長(zhǎng)度的消息壓縮到某一固定長(zhǎng)度的消息摘要的函數(shù)。(源自百度百科)
? ? ? ? 上面的加粗文字,說(shuō)明Hash算法可以解決我們之前的問(wèn)題。但是可想想下,將無(wú)限的數(shù)據(jù)歸于有限的空間之內(nèi),必然會(huì)出現(xiàn)碰撞的問(wèn)題。對(duì)于碰撞問(wèn)題的解決,也有很多方法。下面將介紹Redis的Dict庫(kù)中Hash碰撞解決方案,只有弄明白這個(gè)方案,才能理解該庫(kù)的設(shè)計(jì)思想。
Hash算法碰撞解決方案——拉鏈法
? ? ? ? 為了讓我們的例子說(shuō)明比較簡(jiǎn)單,我杜撰出一種Hash算法和限定使用范圍,這樣將復(fù)雜的問(wèn)題簡(jiǎn)單化,從而讓我們一窺問(wèn)題究竟。
? ? ? ? 我們將Key的使用范圍限定于0~4,Hash算法的定義是hash_value = key%5。則我們可以構(gòu)建一個(gè)數(shù)組保存key為0~4的數(shù)據(jù)
? ? ? ? 但是,當(dāng)我們認(rèn)知范圍從0~4擴(kuò)展到0~9,則通過(guò)我們上面的Hash算法將產(chǎn)生大量的碰撞。在碰撞無(wú)法避免的情況下,只有改變我們的存儲(chǔ)結(jié)構(gòu),但是我們還想使用數(shù)組,那怎么辦呢?那我們就對(duì)Hash的值再Hash,再Hash的方法是hash_value%3。于是有
?
? ? ? ? 上面就是拉鏈解決Hash碰撞的思路。它將碰撞的數(shù)據(jù)通過(guò)鏈表的形式連接在一塊,而通過(guò)數(shù)組的形式找到該鏈表的起始元素。這種方案可以解決碰撞問(wèn)題,但是相應(yīng)的效率也會(huì)有所下降,但是下降的幅度要視鏈表的長(zhǎng)度來(lái)決定。因?yàn)橥ㄟ^(guò)Hash值尋找數(shù)組元素是非常快速的,通過(guò)數(shù)組元素定位到鏈表的時(shí)間消耗也是快速的,因?yàn)樗鼈兌际菍ぶ愤\(yùn)算。所以可以想象真正消耗時(shí)間的是鏈表中數(shù)據(jù)的查找。
? ? ? ? 對(duì)上面的問(wèn)題,我們?cè)撊绾蝺?yōu)化呢?我們可以想到的最簡(jiǎn)單的方法就是適度的擴(kuò)大數(shù)組的長(zhǎng)度。比如我們將數(shù)組長(zhǎng)度擴(kuò)大到5個(gè),則鏈表長(zhǎng)度將縮小,其查找效率會(huì)明顯提升:
? ? ? ? 現(xiàn)在再考慮一個(gè)情況,如果我們隨機(jī)的去掉大部分元素,僅僅留下元素1和4,那么我們上面的結(jié)構(gòu)變?yōu)?/p>
? ? ? ? 上圖可以看出該結(jié)構(gòu)顯得非常松散,也浪費(fèi)內(nèi)存。這個(gè)時(shí)候我們可以重新定義再Hash算法,比如讓hash_value%2,則
? ? ? ? 上面這兩種再Hash是針對(duì)鏈表過(guò)長(zhǎng)或者空間過(guò)于零散的場(chǎng)景設(shè)計(jì)的。如果把這些看明白了,那么Redis的Dict的實(shí)現(xiàn)思想也就大致清楚了。
Dict的基礎(chǔ)結(jié)構(gòu)
? ? ? ? Redis的Dict中最基礎(chǔ)的元素結(jié)構(gòu)是
typedef struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next;
} dictEntry;
? ? ? ? 該結(jié)構(gòu)自身內(nèi)部有一個(gè)指向下一個(gè)該結(jié)構(gòu)對(duì)象的指針,可以見(jiàn)得這是鏈表元素的結(jié)構(gòu)。key字段是一個(gè)無(wú)類(lèi)型指針,我們可以讓該key指向任意類(lèi)型,從而支撐Dict的key是任意類(lèi)型的能力。聯(lián)合體v則是key對(duì)應(yīng)的value,它可以是uint_64_t、int64_t、double和void*型,void*型是無(wú)類(lèi)型指針,它使得Dict可以承載任意類(lèi)型的value值。
? ? ? ? 一般一個(gè)dict只能承載一種類(lèi)型的(key,value)對(duì),而key和value的類(lèi)型則可以是自定義的。這種開(kāi)放的能力需要優(yōu)良架構(gòu)設(shè)計(jì)的支持。因?yàn)閷?duì)類(lèi)型沒(méi)有約束,而框架自身無(wú)法得知這些類(lèi)型的一些信息。但是流程上卻需要得知一些必要信息,比如key字段如何進(jìn)行Hash?key和value如何復(fù)制和析構(gòu)?key字段如何進(jìn)行等值對(duì)比?這些框架無(wú)法提前預(yù)知的能力只能讓數(shù)據(jù)類(lèi)型提供者去提供。Redis的Dict中通過(guò)下面的結(jié)構(gòu)來(lái)指定這些信息
typedef struct dictType {unsigned int (*hashFunction)(const void *key);void *(*keyDup)(void *privdata, const void *key);void *(*valDup)(void *privdata, const void *obj);int (*keyCompare)(void *privdata, const void *key1, const void *key2);void (*keyDestructor)(void *privdata, void *key);void (*valDestructor)(void *privdata, void *obj);
} dictType;
? ? ? ? 承載dictEntry的是下面這個(gè)結(jié)構(gòu),它就是我們之前討論Hash碰撞時(shí)拉鏈算法的體現(xiàn)
typedef struct dictht {dictEntry **table;unsigned long size;unsigned long sizemask;unsigned long used;
} dictht;
? ? ? ? table是一個(gè)保存dicEntry指針的數(shù)組;size是數(shù)組的長(zhǎng)度;sizemask是用于進(jìn)行hash再歸類(lèi)的桶,它的值是size-1;used是元素個(gè)數(shù),我們通過(guò)一個(gè)圖來(lái)解釋
? ? ? ??
? ? ? ? 似乎我們可以用這個(gè)結(jié)構(gòu)已經(jīng)可以實(shí)現(xiàn)字典了。但是Redis在這個(gè)基礎(chǔ)上做了一些優(yōu)化,我們看下它定義的字典結(jié)構(gòu):
typedef struct dict {dictType *type;void *privdata;dictht ht[2];long rehashidx; /* rehashing not in progress if rehashidx == -1 */int iterators; /* number of iterators currently running */
} dict;
? ? ? ? type字段定義了字典處理key和value的相應(yīng)方法,通過(guò)這個(gè)字段該框架開(kāi)放了處理自定義類(lèi)型數(shù)據(jù)的能力。privdata是私有數(shù)據(jù),但是一般都傳NULL。ht是個(gè)數(shù)組,它有兩個(gè)元素,都是可以用于存儲(chǔ)數(shù)據(jù)的。這兒有個(gè)問(wèn)題,就是為什么要兩個(gè)dictht對(duì)象?我們?cè)谥v解拉鏈法時(shí)拋出過(guò)兩個(gè)問(wèn)題,即數(shù)據(jù)鏈過(guò)長(zhǎng)時(shí)或數(shù)據(jù)松散時(shí)如何進(jìn)行優(yōu)化?我們采用的是擴(kuò)大數(shù)組個(gè)數(shù)和縮小數(shù)組個(gè)數(shù),即再Hash(rehash)的方案。其實(shí)Redis就是這樣的方案去做的,只是它處理的比較精細(xì)。ht[0]作為主要的數(shù)據(jù)存儲(chǔ)區(qū)域,ht[1]則是用于rehash操作的結(jié)果,但是一旦rehash完成,就將ht[1]中的數(shù)據(jù)賦值給ht[0]。那么為什么不讓ht[1]作為rehash操作中一個(gè)棧上臨時(shí)變量,而要保存在字典結(jié)構(gòu)中呢?這是因?yàn)槿绻覀儗ehash操作當(dāng)成一個(gè)原子操作在一個(gè)函數(shù)中去做,此時(shí)如果有數(shù)據(jù)插入或者刪除,則需要等到rehash操作完成才可以執(zhí)行。而當(dāng)數(shù)據(jù)量很大時(shí),rehash操作會(huì)比較慢,這樣勢(shì)必影響其他操作的速度。于是Redis在設(shè)計(jì)時(shí),采用的是一種漸進(jìn)式的rehash方法。因?yàn)闈u進(jìn)式非原子性,所以中間狀態(tài)也要保存在字典結(jié)構(gòu)中以保證數(shù)據(jù)完整性。這就是為什么有兩個(gè)dictht的原因。rehashidx是rehash操作時(shí)ht[0]中正在被rehash操作的數(shù)組下標(biāo),如果它是-1則代表沒(méi)有在進(jìn)行rehash操作。iterators是迭代器,我們會(huì)在之后講解。
總結(jié)
以上是生活随笔為你收集整理的Redis源码解析——字典结构的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Redis源码解析——内存管理
- 下一篇: Redis源码解析——字典基本操作