Redis源码解析——字典结构
? ? ? ? C++語言中有標準的字典庫,我們可以通過pair(key,value)的形式存儲數據。但是C語言中沒有這種的庫,于是就需要自己實現。本文講解的就是Redis源碼中的字典庫的實現方法。(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 一般情況下,我們談到字典,難免要談到紅黑樹。但是Redis這套字典庫并沒有使用該方案去實現,而是使用的是鏈表,且整個代碼行數在1000行以內。所以這塊邏輯還是非常好分析的。
? ? ? ? 我們可以想象下,如果使用普通的鏈表去實現字典,那么是不是整個數據都在一條鏈表結構上呢?如果是這么設計,插入和刪除操作是非常方便的,但是查找操作可能就非常耗時——需要從前向后一個個遍歷對比。很顯然不能采用這種方案。于是有一種替代性的方案,就是使用數組去存儲,然后通過下標去訪問。因為下標操作就是指針的移動,所以查找元素變得非常快。相應的問題便是如何將數據的Key轉換成數組下標?
? ? ? ? 一種比較容易想到的就是使用Key對應的二進制碼作為下標。比如我們要保存pair(1,"String1"),則使用其Key的值1對應的二進值1作為下標;再比如pair('A',"stringA"),則使用A字符對應的編碼十進制值65作為下標。這種設計方法固然簡單,但是有個非常現實的問題——到底要分配多大的數組?上面兩個例子還比較簡單,我們看個稍微復雜的例子,比如要保存pair("AAAA","StringAAA"),則AAAA的二進制編碼對應的十進制是65656565,難道我們要分配那么大的數組?想想也不可能,因為我們往往需要保存的數據比上面這些例子還要復雜很多。如果這么設計,我們的內存可能是否不夠分配的,且其使用率也非常低。那怎么解決呢?于是我們就要提到Hash算法了。
? ? ? ? 我們看下Hash中文定義:Hash,一般翻譯做“散列”,也有直接音譯為“哈希”的,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小于輸入的空間,不同的輸入可能會散列成相同的輸出,所以不可能從散列值來唯一的確定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。(源自百度百科)
? ? ? ? 上面的加粗文字,說明Hash算法可以解決我們之前的問題。但是可想想下,將無限的數據歸于有限的空間之內,必然會出現碰撞的問題。對于碰撞問題的解決,也有很多方法。下面將介紹Redis的Dict庫中Hash碰撞解決方案,只有弄明白這個方案,才能理解該庫的設計思想。
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的實現思想也就大致清楚了。
Dict的基礎結構
? ? ? ? Redis的Dict中最基礎的元素結構是
typedef struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} 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 {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的是下面這個結構,它就是我們之前討論Hash碰撞時拉鏈算法的體現
typedef struct dictht {dictEntry **table;unsigned long size;unsigned long sizemask;unsigned long used;
} dictht;
? ? ? ? table是一個保存dicEntry指針的數組;size是數組的長度;sizemask是用于進行hash再歸類的桶,它的值是size-1;used是元素個數,我們通過一個圖來解釋
? ? ? ??
? ? ? ? 似乎我們可以用這個結構已經可以實現字典了。但是Redis在這個基礎上做了一些優化,我們看下它定義的字典結構:
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的相應方法,通過這個字段該框架開放了處理自定義類型數據的能力。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是迭代器,我們會在之后講解。
總結
以上是生活随笔為你收集整理的Redis源码解析——字典结构的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis源码解析——内存管理
- 下一篇: Redis源码解析——字典基本操作