一般情況下,我們談到字典,難免要談到紅黑樹。但是redis這套字典庫並沒有使用該方案去實現,而是使用的是鍊錶,且整個**行數在1000行以內。所以這塊邏輯還是非常好分析的。
我們可以想象下,如果使用普通的鍊錶去實現字典,那麼是不是整個資料都在一條鍊錶結構上呢?如果是這麼設計,插入和刪除操作是非常方便的,但是查詢操作可能就非常耗時——需要從前向後乙個個遍歷對比。很顯然不能採用這種方案。於是有一種替代性的方案,就是使用陣列去儲存,然後通過下標去訪問。因為下標操作就是指標的移動,所以查詢元素變得非常快。相應的問題便是如何將資料的key轉換成陣列下標?
一種比較容易想到的就是使用key對應的二進位製碼作為下標。比如我們要儲存pair(1,"string1"),則使用其key的值1對應的二進值1作為下標;再比如pair('a',"stringa"),則使用a字元對應的編碼十進位制值65作為下標。這種設計方法固然簡單,但是有個非常現實的問題——到底要分配多大的陣列?上面兩個例子還比較簡單,我們看個稍微複雜的例子,比如要儲存pair("aaaa","stringaaa"),則aaaa的二進位制編碼對應的十進位制是65656565,難道我們要分配那麼大的陣列?想想也不可能,因為我們往往需要儲存的資料比上面這些例子還要複雜很多。如果這麼設計,我們的記憶體可能是否不夠分配的,且其使用率也非常低。那怎麼解決呢?於是我們就要提到hash演算法了。
上面的加粗文字,說明hash演算法可以解決我們之前的問題。但是可想想下,將無限的資料歸於有限的空間之內,必然會出現碰撞的問題。對於碰撞問題的解決,也有很多方法。下面將介紹redis的dict庫中hash碰撞解決方案,只有弄明白這個方案,才能理解該庫的設計思想。
為了讓我們的例子說明比較簡單,我杜撰出一種hash演算法和限定使用範圍,這樣將複雜的問題簡單化,從而讓我們一窺問題究竟。
我們將key的使用範圍限定於0~4,hash演算法的定義是hash_value = key%5。則我們可以構建乙個陣列儲存key為0~4的資料
但是,當我們認知範圍從0~4擴充套件到0~9,則通過我們上面的hash演算法將產生大量的碰撞。在碰撞無法避免的情況下,只有改變我們的儲存結構,但是我們還想使用陣列,那怎麼辦呢?那我們就對hash的值再hash,再hash的方法是hash_value%3。於是有
上面就是拉鍊解決hash碰撞的思路。它將碰撞的資料通過鍊錶的形式連線在一塊,而通過陣列的形式找到該鍊錶的起始元素。這種方案可以解決碰撞問題,但是相應的效率也會有所下降,但是下降的幅度要視鍊錶的長度來決定。因為通過hash值尋找陣列元素是非常快速的,通過陣列元素定位到鍊錶的時間消耗也是快速的,因為它們都是定址運算。所以可以想象真正消耗時間的是鍊錶中資料的查詢。
對上面的問題,我們該如何優化呢?我們可以想到的最簡單的方法就是適度的擴大陣列的長度。比如我們將陣列長度擴大到5個,則鍊錶長度將縮小,其查詢效率會明顯提公升:
現在再考慮乙個情況,如果我們隨機的去掉大部分元素,僅僅留下元素1和4,那麼我們上面的結構變為
上圖可以看出該結構顯得非常鬆散,也浪費記憶體。這個時候我們可以重新定義再hash演算法,比如讓hash_value%2,則
上面這兩種再hash是針對鍊錶過長或者空間過於零散的場景設計的。如果把這些看明白了,那麼redis的dict的實現思想也就大致清楚了。
redis的dict中最基礎的元素結構是
typedef struct dictentry v;
struct dictentry *next;
} dictentry;
該結構自身內部有乙個指向下乙個該結構物件的指標,可以見得這是鍊錶元素的結構。key欄位是乙個無型別指標,我們可以讓該key指向任意型別,從而支撐dict的key是任意型別的能力。聯合體v則是key對應的value,它可以是uint_64_t、int64_t、double和void*型,void*型是無型別指標,它使得dict可以承載任意型別的value值。
一般乙個dict只能承載一種型別的(key,value)對,而key和value的型別則可以是自定義的。這種開放的能力需要優良架構設計的支援。因為對型別沒有約束,而框架自身無法得知這些型別的一些資訊。但是流程上卻需要得知一些必要資訊,比如key欄位如何進行hash?key和value如何複製和析構?key欄位如何進行等值對比?這些框架無法提前預知的能力只能讓資料型別提供者去提供。redis的dict中通過下面的結構來指定這些資訊
typedef struct dicttype dicttype;
承載dictentry的是下面這個結構,它就是我們之前討論hash碰撞時拉鍊演算法的體現
typedef struct dictht dictht;
table是乙個儲存dicentry指標的陣列;size是陣列的長度;sizemask是用於進行hash再歸類的桶,它的值是size-1;used是元素個數,我們通過乙個圖來解釋
似乎我們可以用這個結構已經可以實現字典了。但是redis在這個基礎上做了一些優化,我們看下它定義的字典結構:
typedef struct dict dict;
type欄位定義了字典處理key和value的相應方法,通過這個欄位該框架開放了處理自定義型別資料的能力。privdata是私有資料,但是一般都傳null。ht是個陣列,它有兩個元素,都是可以用於儲存資料的。這兒有個問題,就是為什麼要兩個dictht物件?我們在講解拉鍊法時拋出過兩個問題,即資料鏈過長時或資料鬆散時如何進行優化?我們採用的是擴大陣列個數和縮小陣列個數,即再hash(rehash)的方案。其實redis就是這樣的方案去做的,只是它處理的比較精細。ht[0]作為主要的資料儲存區域,ht[1]則是用於rehash操作的結果,但是一旦rehash完成,就將ht[1]中的資料賦值給ht[0]。那麼為什麼不讓ht[1]作為rehash操作中乙個棧上臨時變數,而要儲存在字典結構中呢?這是因為如果我們將rehash操作當成乙個原子操作在乙個函式中去做,此時如果有資料插入或者刪除,則需要等到rehash操作完成才可以執行。而當資料量很大時,rehash操作會比較慢,這樣勢必影響其他操作的速度。於是redis在設計時,採用的是一種漸進式的rehash方法。因為漸進式非原子性,所以中間狀態也要儲存在字典結構中以保證資料完整性。這就是為什麼有兩個dictht的原因。rehashidx是rehash操作時ht[0]中正在被rehash操作的陣列下標,如果它是-1則代表沒有在進行rehash操作。iterators是迭代器,我們會在之後講解。
redis0 1原始碼解析之字典
字典也叫雜湊表。看一下redis中的實現。下面是資料結構關係圖。redis中,雜湊表的設計思想是,申請乙個指標陣列,然後每個元素指向乙個鍊錶用來儲存資料 即鏈位址法 申請乙個表示字典的資料結構 dict dictcreate dicttype type,void privdataptr 初始化字典資...
Redis原始碼分析(一) Redis結構解析
從今天起,本人將會展開對redis原始碼的學習,redis的 規模比較小,非常適合學習,是乙份非常不錯的學習資料,數了一下大概100個檔案左右的樣子,用的是c語言寫的。希望最終能把他啃完吧,c語言好久不用,快忘光了。分析原始碼的第一步,先別急著想著從哪開始看起,先瀏覽一下原始碼結構,可以模組式的漸入...
redis原始碼解析之鍊錶結構
typedef struct listnode listnode 雙端鍊錶節點包含2個指標域和1個資料域,注意資料的型別為void 因此其可以承載任意資料型別。typedef struct list list 雙端鍊錶中,使用函式指標來封裝與節點值相關的操作,在後面的使用中較頻繁,並維護乙個len作...