@本文首發於 https://yeqown.github.iohtml
最近一直在看《redis設計與實現》,其中講了redis中使用到的數據結構如:sds
, ziplist
, skiplist
, hashtable
, intset
, linkedlist
等。讀完第一部分以後,再結合github上的源碼redis,本着好記性不如爛筆頭的理念,便準備動手擼一遍。git
murmur2
哈希函數。rehash
,rehash
過程並非一步到位,而是在get/set/del 等操做中,穿插着完成。hashtable:是根據Key直接訪問在內存存儲位置的數據結構。如何根據key獲得內存中的位置,就須要使用hash函數來從旁協助了。hash函數:是一種從任何一種數據中建立小的數字「指紋」的方法。簡單的說:hash(input) = 1122334455。github
這裏選擇了golang來實現;murmur3 hash算法;golang
一圖以蔽之:redis
// 對外暴露的hashtable type LinkedDict struct { // 2個存儲桶,0號正常使用,1號在rehash過程當中使用;rehash完成以後,1號賦值給0號而後重置1號。 ht [2]*hashtable // 初始值 -1,表示沒有在rehash rehashIdx int } // 存儲桶 type hashtable struct { // 底層「數組」 table []*dictEntry size int sizemask int used int } // hashtable 元素定義 type dictEntry struct { key string value interface{} next *dictEntry }
hashtable經常使用的方法有 GET/SET/DELETE/ITER,接下來會在SET和DEL中介紹rehash的詳細過程。算法
func (d *LinkedDict) Set(key string, value interface{}) { if d.ht[0].table == nil { d.ht[0].init(InitTableSize) } // isRehashing 斷定:`rehashIdx != -1` // needRehash 斷定: 裝載因子 used / size > 1.0 時觸發擴容rehash if !d.isRehashing() && d.needRehash() { // rehashGrowup 表示本次rehash是須要擴容,在配置ht[1].table時會擴展爲當前的2倍 // 反之則會縮減內存空間 // startrehash 會設置 rehashIdx = 0, 標誌rehash的進度 d.startrehash(rehashGrowup) } if d.isRehashing() { // 若是在rehash過程當中,則完成一部分任務: // 根據rehash的進度rehashIdx,選擇搬移 d.ht[0].table[rehashIdx]部分的數據,添加到d.ht[1]中 d.steprehash() } // 上述工做完成以後,就能夠考慮新增數據了 hashkey := d.hashkey(key) if d.isRehashing() { // 若是在rehash過程當中,毋庸考慮,直接新增到d.ht[1]中 d.ht[1].insert(hashkey, newDictEntry(key, value, nil)) return } // 反之,不在rehash過程當中,則直接新增到d.ht[0]中 d.ht[0].insert(hashkey, newDictEntry(key, value, nil)) return } // 漸進式rehash,根據rehashIdx肯定,要搬移那一部分的數據。 func (d *LinkedDict) steprehash() { entry := d.ht[0].table[d.rehashIdx] // 若是rehashIdx指向的側鏈爲空,則rehashIdx自增,直到找到有數據的側鏈或者數據均搬移完成 for entry == nil { d.rehashIdx++ if d.rehashIdx > d.ht[0].sizemask { d.finishrehash() return } entry = d.ht[0].table[d.rehashIdx] } // 開始搬移動做 // 遍歷鏈表,將全部數據,新增到d.ht[1]中 next := entry.next for entry != nil { entry.next = nil d.ht[1].insert(d.hashkey(entry.key), entry) if next == nil { break } entry = next next = next.next } // 釋放d.ht[0].table[rehashIdx]鏈表:避免干擾查詢;釋放內存 d.ht[0].table[d.rehashIdx] = nil d.rehashIdx++ if d.rehashIdx > d.ht[0].sizemask { // 是否已經結束,若是結束則: // d.ht[0] = d.ht[1] // d.ht[1] = newHashTable() // rehashIdx = -1 d.finishrehash() } } // 新增一個元素到到存儲桶: // 根據hash函數的結果(hashkey)對存儲桶大小(size)取模獲得結果(pos);ht.table[pos]完成對鏈表的新增工做。 func (ht *hashtable) insert(hashkey uint64, item *dictEntry) { pos := hashkey % uint64(ht.size) entry := ht.table[pos] last := entry if entry == nil { ht.used++ ht.table[pos] = item return } for entry != nil { if ht.keyCompare(entry.key, item.key) { // 若是key已經存在則覆蓋舊值 entry.value = item.value return } last = entry entry = entry.next } ht.used++ last.next = item }
總結:數組
func (d *LinkedDict) Get(key string) (v interface{}, ok bool) { // 同上SET,不過多贅述 if d.isRehashing() { d.steprehash() } hashkey := d.hashkey(key) v, ok = d.ht[0].lookup(hashkey, key) if !d.isRehashing() { // 若是不在rehash過程當中 d.ht[0]中檢索的結果即是最終結果 return } else if ok { // 若是在rehash過程當中且命中,也返回結果 return v, ok } // 反之 rehash過程當中,但在d.ht[0]中沒找到,卻不表明該key真的不存在, 還須要在d.ht[1]中肯定 v, ok = d.ht[1].lookup(hashkey, key) return }
總結:安全
// Del to delete an item in hashtable. func (d *LinkedDict) Del(key string) { if d.ht[0].used == 0 && d.ht[1].used == 0 { return } // 這裏相比Set,區別在於:斷定的內容不是是否須要擴容而是縮容 // 縮容斷定:d.ht[0]的內存空間大於初始值4且「填充率」少於 10% // d.ht[0].size > 4 && (d.ht[0].used*100/d.ht[0].size) < 10 if !d.isRehashing() && d.needShrink() { d.startrehash(rehashShrink) } if d.isRehashing() { d.steprehash() } hashkey := d.hashkey(key) d.ht[0].del(hashkey, key) if d.isRehashing() { d.ht[1].del(hashkey, key) } }
總結:數據結構
這裏我使用了golang內置的Map作了對比測試,結果以下:併發
builtinMap_1000 cost: 0ms builtinLinkedDict_1000 cost: 0ms getMap_1000 cost: 0ms getLinkedDict_1000 cost: 0ms builtinMap_10000 cost: 4ms builtinLinkedDict_10000 cost: 6ms getMap_10000 cost: 2ms getLinkedDict_10000 cost: 5ms builtinMap_100000 cost: 76ms builtinLinkedDict_100000 cost: 108ms getMap_100000 cost: 56ms getLinkedDict_100000 cost: 131ms builtinMap_1000000 cost: 1053ms builtinLinkedDict_1000000 cost: 1230ms getMap_1000000 cost: 581ms getLinkedDict_1000000 cost: 915ms builtinMap_10000000 cost: 13520ms builtinLinkedDict_10000000 cost: 17137ms getMap_10000000 cost: 8663ms getLinkedDict_10000000 cost: 14271ms
可見差距仍是很是大的,這裏大膽分析下致使這些差距的緣由:
hashtable.keyCompare
上花費了較多時間,雖然已經經過strings.Compare
來加速orz;相比下golang內置的Map使用了unsafe.Pointer
pointer to unsafe.ArbitraryType (int)做爲key,並針對不一樣的key類型來設計哈希算法。low bits & bucketMask
定位buckets和high 8bits
找到對應的位置,效率更高;map
的實現會容易不少orz,僅類似部分;map比上述hashtable的實現要複雜得多;