Redis设计于实现之字典
字典
簡介
- 字典又稱符號(hào)表,映射或關(guān)聯(lián)數(shù)組,是一種保存鍵值對的抽象數(shù)據(jù)結(jié)構(gòu)。
- Redis數(shù)據(jù)庫的底層也是用字典實(shí)現(xiàn)的,對數(shù)據(jù)庫的增刪改查也是基于對字典的操作之上的。
- 字典還是哈希鍵的底層實(shí)現(xiàn)之一,當(dāng)哈希鍵對比較多或者鍵值對中的元素都是比較長的,Redis就會(huì)使用字典作為底層實(shí)現(xiàn)。
字典實(shí)現(xiàn)
字典的實(shí)現(xiàn)是以哈希表作為它的底層實(shí)現(xiàn),一個(gè)哈希表可以有多個(gè)哈希表節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)保存了字典中的一個(gè)鍵值對。
1.哈希表節(jié)點(diǎn)
key就是鍵,v就是鍵中的值(可以是指針,unit64_t 整數(shù), int64_t s64整數(shù)),next是將另外一個(gè)哈希值相同的鍵值對連接在一起的指針(為了解決沖突)
2.哈希表
table屬性是一個(gè)數(shù)組,數(shù)組中每個(gè)元素都指向一個(gè)哈希表節(jié)點(diǎn) ,每個(gè)哈希表節(jié)點(diǎn)都保存著一個(gè)鍵值對。
size記錄了哈希表的大小,也就是table數(shù)組的大小。
used屬性記錄哈希表目前已有哈希表節(jié)點(diǎn)(鍵值對)的數(shù)量。
sizemask總是等于size-1(這個(gè)屬性和哈希值一起決定一個(gè)鍵應(yīng)該被放到table的那個(gè)索引上)。
3.字典
type和pribdata是配套的,針對不同類型的鍵值對,為創(chuàng)建多態(tài)字典而設(shè)置的。
type指向dictType結(jié)構(gòu)的指針,每一個(gè)dictType里都保存了一簇用于操作特定類型鍵值對的函數(shù)(為用途不同的字典設(shè)置不同的類型特定函數(shù))。
privdata保存了需要傳給那些類型特定函數(shù)的可選參數(shù)(也就是在dictType結(jié)構(gòu)體中的參數(shù))。
ht包含了兩項(xiàng)數(shù)組,每個(gè)項(xiàng)都是dictht哈希表,字典只使用ht[0]哈希表,ht[1]只會(huì)在ht[0]rehash時(shí)使用,除了ht[1]之外另一個(gè)和rehash(重新散列)有關(guān)的就是rehashidx(記錄rehash進(jìn)度,若沒在進(jìn)行rehash則值為-1).
這就是字典實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu),如果要數(shù)據(jù)庫性能好,還是要用一些性能較好的算法,Redis使用的MurmurHash2算法來計(jì)算鍵的哈希值(數(shù)據(jù)庫的底層實(shí)現(xiàn)或者哈希鍵的底層實(shí)現(xiàn))。
哈希算法
在我們需要把一個(gè)新的鍵值對加入到字典里,程序得先根據(jù)鍵值對的鍵計(jì)算出哈希值和索引值,然后根據(jù)索引,將新鍵值對的哈希表節(jié)點(diǎn)方法哈希表數(shù)組的指定索引(位置)上,這都是由哈希算法來完成的。哈希算法的設(shè)計(jì)推理就不寫了,因?yàn)檫@個(gè)是一個(gè)很復(fù)雜的過程。
Redis計(jì)算哈希值和索引值的方法
這種算法的優(yōu)點(diǎn)就是:對于輸入有規(guī)律的鍵仍能給出一個(gè)很好的隨機(jī)分布性而且計(jì)算速度也很快!但是呢,這種算法可能會(huì)出現(xiàn)沖突,因此要避免沖突就得由個(gè)解決沖突的辦法?(在前面提到過)
解決鍵沖突
什么是沖突:因?yàn)樗惴▓?zhí)行時(shí)會(huì)有可能多個(gè)鍵被分配到哈希數(shù)組的同一個(gè)索引上。
Redis中解決鍵沖突采用的是鏈地址法,每個(gè)節(jié)點(diǎn)都有一個(gè)next指針,構(gòu)成沖突的節(jié)點(diǎn)可以用next指針構(gòu)成一個(gè)單鏈表來共同占有同一個(gè)索引。這個(gè)解決方法也是解決沖突比較經(jīng)典的方法,也是比較簡單的方法。
rehash
rehash是什么?為什么要rehash?
rehash是重新散列的意思,因?yàn)樵诓粩嗟膱?zhí)行中,哈希表保存的鍵值對在逐漸增多或減少,為了讓哈希表的負(fù)載因子(load factor)維持在合理范圍內(nèi),當(dāng)哈希表中的鍵值對過多或者過少時(shí),需要對表的大小進(jìn)行擴(kuò)展或收縮。
哈希表執(zhí)行rehash的步驟:
1.為字典的ht[1]分配空間,此哈希表的大小取決于要執(zhí)行的操作和h[0]包含的鍵值對的數(shù)量(ht[0].used)
- 擴(kuò)展操作:h[1]的大小等于第一個(gè)大于ht[0].used*2的2的n次方。
- 收縮操作:h[1]的大小等于第一個(gè)大于ht[0].used的2的n次方。
2.將保存在ht[0]中的鍵值對到ht[1]上:重新計(jì)算哈希值和索引,然后將鍵值對放到ht[1]哈希表的指定位置上。
3. ht[0]遷移到ht[1]后,ht[0]變?yōu)榭毡砣缓筢尫诺?#xff0c;然后再將ht[1]設(shè)置為ht[0],并再ht[1]位置上新建一個(gè)空白的哈希表,供下一次rehash使用。
哈希表的自動(dòng)擴(kuò)展與收縮
如果在負(fù)載因子不合理時(shí)沒有進(jìn)行手動(dòng)的rehash的話,那系統(tǒng)會(huì)在某些條件成立下自動(dòng)進(jìn)行擴(kuò)展或收縮。
哈希表的負(fù)載因子求法:負(fù)載因子=以保存節(jié)點(diǎn)數(shù)量/哈希表大小
- ?
- 當(dāng)系統(tǒng)滿足以下的任意一個(gè)條件程序就會(huì)自動(dòng)開始對哈希表執(zhí)行擴(kuò)展操作:
1.服務(wù)器目前沒在執(zhí)行BGSAVE命令或者BGREWRITEAOF命令并且哈希表的負(fù)載因子大于1
2.服務(wù)器目前正執(zhí)行BGSAVE命令或者BGREWRITEAOF命令并且負(fù)載因子大于5 - 當(dāng)系統(tǒng)滿足負(fù)載因子小于0.1,就會(huì)自動(dòng)進(jìn)行收縮操作。
漸進(jìn)式rehash
其實(shí)在擴(kuò)展或者收縮哈希表的時(shí)候并不是一次性,集中性的執(zhí)行的,而是分多次,漸進(jìn)式地完成的。
漸進(jìn)式的詳細(xì)步驟:
1.為ht[1]分配空間,此時(shí)字典同時(shí)有ht[0]和ht[1]兩個(gè)哈希表。
2.在字典中維持一個(gè)索引計(jì)數(shù)器變量rehashidx,并設(shè)為0,表示rehash正式開始。
3.在rehash期間,每次對字典進(jìn)行添加,刪除,查找,更新時(shí),程序除了執(zhí)行指定操作以外,還會(huì)順帶將ht[0]哈希表在rehashidx索引上的所有鍵值對rehash到ht[1]上,rehash完成,然后rehashidx+1。
4.直到全部內(nèi)移到ht[1],這時(shí)rehashidx屬性的值設(shè)為-1,表示rehash操作完成。
采取分而治之的方式,將rehash鍵值對所需的工作均攤到對字典的增刪改查上,避免了集中式rehash帶來的龐大計(jì)算量。
漸進(jìn)式rehash執(zhí)行期間的哈希表操作
因?yàn)樵趓ehash期間字典會(huì)同時(shí)使用ht[0]和ht[1],因此,增刪改查會(huì)在兩個(gè)哈希表上進(jìn)行,比如查詢操作,先對0表掃描如果沒找到,就再從1表里找。注意的是,如果在此期間進(jìn)行插入操作的話,那就會(huì)插入到1表,而不是0表。因?yàn)椴迦氲?表就沒意義了等于浪費(fèi)體力。也保證了0表只減不增。
總結(jié)
以上是生活随笔為你收集整理的Redis设计于实现之字典的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis设计与实现之SDS和链表
- 下一篇: Redis的设计与实现之跳表