Redis 数据结构之哈希表
Redis 的字典底層使用哈希表實現(xiàn),說到哈希表大家應(yīng)該能聯(lián)想到 HashMap 或者是 Hashtable,也應(yīng)該能聯(lián)想到 key、value 的存儲形式,以及哈希表擴(kuò)容,哈希算法等知識點。那么 Redis 字典是否也是通過這種形式實現(xiàn)的呢?帶著這些疑問下面我們就來了解一下 Redis 中的哈希表。
一、 哈希表結(jié)構(gòu)
- table:用于存儲鍵值對
- size:表示哈希表的數(shù)組大小
- used:表示哈希表中已經(jīng)存儲的鍵值對的個數(shù)
- sizemask:大小永遠(yuǎn)為 size - 1,該屬性用于計算哈希值
二、 字典結(jié)構(gòu)
上面我們說過字典是基于哈希表實現(xiàn)的,通過上圖我們可以看出字典包含了 2 個哈希表,還有一些其他屬性,比如 rehashindex,type 等。
為什么字典使用 2 個哈希表作為底層實現(xiàn)呢?原因是與 rehash 相關(guān),與 rehash 相關(guān)的還有 rehashindex 屬性,下面我們會具體看到字典 rehash 的過程。
三、哈希算法
當(dāng)我們把一個新的鍵值對添加到字典里時,會先根據(jù)鍵的哈希值計算其對應(yīng)的索引值,然后根據(jù)索引值,將新的鍵值對放到哈希表數(shù)組指定的索引上面。
如果當(dāng) 2 個或 2 個以上的鍵值對被分配同一個索引上面時,就發(fā)生了哈希沖突。我們聯(lián)想下 HashMap 中時如何解決哈希沖突的呢?HassMap 會通過鏈地址法將新的鍵值對通過鏈表的形式追加在上一個鍵值對后面。Redis 中的字典也是通過鏈地址法解決哈希沖突的,不過有一點不同的是 Redis 會將新添加的鍵值對放在鏈表的頭節(jié)點位置上。
四、rehash
4.1 負(fù)載因子
我們再來想一下 HashMap 中的 rehash,HashMap 中有一個 threshold 字段,這個字段在作為擴(kuò)容閾值時默認(rèn)情況下為 0.75 * capacity,意思是當(dāng)哈希表中鍵值對的數(shù)量達(dá)到哈希表容量的 0.75 倍時就需要對哈希表進(jìn)行擴(kuò)容。
Redis 哈希表的負(fù)載因子通過下面的公式計算:
# 負(fù)載因子 = 哈希表已保存節(jié)點數(shù)量 / 哈希表大小load_factor = ht[0].used / ht[0].size4.2 rehash 條件
Redis 哈希表不僅提供了擴(kuò)容還提供了收縮機(jī)制,擴(kuò)容與收縮都是通過 rehash 完成的。與 HashMap 一樣,Redis 中的哈希表想要執(zhí)行 rehash 擴(kuò)容操作也是需要一定條件的,主要為以下 2 個:
- 服務(wù)器目前沒有執(zhí)行 BGREWRITEAOF 或者 BGSAVE 命令,切哈希表的負(fù)載因子大于等于 1
- 服務(wù)器目前正在執(zhí)行 BGSAVE 或者 BGREWRITEAOF 命令, 并且哈希表的負(fù)載因子大于等于 5
下面是收縮 rehash 的條件:
- 哈希表的負(fù)載因子小于 0.1 時, 程序自動開始對哈希表執(zhí)行收縮操作
上面擴(kuò)容時根據(jù) BGREWRITEAOF 或者 BGSAVE 命令是否執(zhí)行分了兩種情況,為什么要這么做呢?原因如下:
在執(zhí)行 BGREWRITEAOF 或者 BGSAVE 命令 時,Redis 會為當(dāng)前服務(wù)器進(jìn)程創(chuàng)建一個子進(jìn)程,所以在子進(jìn)程存在期間,會提高執(zhí)行擴(kuò)容的負(fù)載因子,因為這樣可以避免在子進(jìn)程存在期間進(jìn)行哈希表的擴(kuò)容操作,從而避免不必要的內(nèi)存寫入操作,最大限度的節(jié)約內(nèi)存,提高效率。
4.3 rehash 擴(kuò)容過程
Redis 字典 rehash 過程比較有意思的是它通過 2 個哈希表實現(xiàn),當(dāng)沒有在 rehash 時:rehashidx 的值為 -1,且使用哈希表 0 存儲鍵值對,哈希表 1 什么也不存儲。
rehash 過程:
- 為字典的 ht[1] 哈希表分配空間,分配的大小如下
- 擴(kuò)容:ht[1] 的大小為第一個大于等于 ht[0].used * 2 的 2^n
- 收縮:ht[1] 的大小為第一個大于等于 ht[0].used 的 2^n
- 將保存在 ht[0] 中的所有鍵值對 rehash 到 ht[1] 上,這個過程會重新計算鍵的哈希值和索引值, 然后將鍵值對放置到 ht[1] 哈希表的指定位置上
- 當(dāng) ht[0] 包含的所有鍵值對都遷移到了 ht[1] 之后 (ht[0] 變?yōu)榭毡?#xff09;, 釋放 ht[0] , 將 ht[1] 設(shè)置為 ht[0] , 并在 ht[1] 新創(chuàng)建一個空白哈希表, 為下一次 rehash 做準(zhǔn)備
下面是 rehash 前后的一個對比
上面只給出了最終的結(jié)果對比,其實在 rehash 的過程中,每當(dāng)一個鍵值對被 rehash 到 ht[1]
上時,對應(yīng)的 rehashidx 屬性就會加 1。
4.4 漸進(jìn)式 rehash
上面我們提到 Redis 字典的 rehash 過程,其實 rehash 并不是一次性,集中式的完成的,而是分多次,漸進(jìn)式完成的。原因是 Redis 的字典字典有可能存儲上百萬個鍵值對,如果一次性完成的話,那么 Redis 可能會在一段時間內(nèi)停止服務(wù),為了保證 Redis 的高性能,這么做肯定是不允許的。
4.5 擴(kuò)展問題分析
上面我們已經(jīng)知道 rehash 的過程,現(xiàn)在我們來思考一個問題,在 rehash 的過程中,如果我們對字典進(jìn)行了增刪改查,那么會操作哪個哈希表呢?是舊的還是新的,下面我們就來分析下:
其實不管執(zhí)行任何操作,都不會允許有鍵值有丟失或者不一致的情況,有了這個前提后再進(jìn)行分析就比較簡單了。在新增鍵值對的時候肯定會添加到新的哈希表中去,因為添加到舊的哈希表的話,最終還是被 rehash 到新的哈希表,就沒有必要進(jìn)行一次浪費的 rehash 了。
刪改查操作在保證一致性的前提下,一定會先操作舊的哈希表,如果在舊的哈希表中沒有操作成功,會繼續(xù)操作新的哈希表。我們想一下,如果刪改查先操作新的哈希表再操作舊的哈希表的話,那么在操作的過程中可能有一部分?jǐn)?shù)據(jù)被 rehash 到新的哈希表中去,這些數(shù)據(jù)有可能因為重新哈希的原因而被忽略。
五、參考資料
《Redis 設(shè)計與實現(xiàn)》 黃健宏著
總結(jié)
以上是生活随笔為你收集整理的Redis 数据结构之哈希表的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何选择适合自己的烹饪锅具?
- 下一篇: linux cmake编译源码,linu