「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」前端
前言
應用場景
- 在Redis中有不少場景都是用了字典做爲底層數據結構!咱們使用最多的應該是redis的庫的設置和五種基本數據類型的Hash結構數據!
- 在上一篇【redis前傳】中咱們學習了list數據結構。今天咱們繼續學習主流數據結構Hash。
- 在redis內部有字典結構、hash結構可是這裏的hash和咱們平時熟知的redis基礎數據的hash並非一個意思!咱們簡單的將字典結構、hash結構理解成redis更加底層的一種抽象結構。平時咱們使用的hash基礎數據結構理解成hash工具

- 而今天咱們的主角就是五種數據結構的Hash分析。他的底層使用了字典這個結構。字典結構內部使用的是底層的hash結構。有點繞!好好理解你行的
哈希表

- 上面這張圖詮釋了做爲redis底層結構的Hash。在內部redis稱之爲dictht 。 後面咱們爲何和以前的hash結構衝突咱們都已類名爲準叫作dictht類。
- 在hictht類中有四個屬性分別是table 、 size 、 sizemask 、 used ; 其中table就是一個數組;數組中元素是另一個類叫作dictEntry類。
- dictEntry就是真正存儲數據的。內部是key、value存儲結構。一個簡單的哈希表就如圖所示。數據最終會存儲在table中的dictEntry對象中。
- 至於爲何sizemask = size -1 ; 這個是爲了在計算hash索引時須要用到的。那爲何不直接使用size-1而是經過一個變量來承接呢?這個吧!!!我也不知道。容我去百度百度。
數組節點
- 上面的哈希表是否是很熟悉,這不和咱們Java中的Map數據結構一模一樣嗎。能夠說是也能夠說不是,二者很類似但也有區別的。
- 在上面中咱們提到數據最終是存儲在哈希表裏table數組裏的元素。該元素叫dictEntry 。 下面咱們看看dictEntry結構如何吧!

- 經過左側對dictEntry的定義咱們能夠看出。dictEntry存儲的值能夠是指針、正數、浮點數各類數據類型!相似於Java中的Object屬性。 對於上述的key沒有啥真意的就是一個鍵。
- 既然是數組那麼索引就是固定長度的,那麼在有限的長度中確定會出現經典問題就是【hash衝突】。在Java中咱們是經過鏈表和紅黑樹來解決衝突的問題!在redis中是經過鏈表解決的。在dictEntry中經過next指針將衝突元素鏈接。
- 這裏咱們就能夠和Java中的Map結構進行理解。他們內部非常類似!!!
- 這裏須要注意下在hash衝突時redis的確是經過鏈表進行存儲的,可是因爲哈希表(dictht)中沒有記錄每一個索引未中鏈表的尾部節點只有頭結點信息因此。並且咱們也知道鏈表在查詢上效率不佳,因此當發生哈希衝突時redis是將新加入的節點加入在鏈表的頭部!

字典
多態字典
- 字典是本文開頭提出的結構!也是redis中大量使用的一種底層數據結構。在redis中名叫作dict類。

- 經過圖示咱們明確的看出內部是包含哈希表的。其實從名字上咱們也能夠看出哈希表爲何叫dictht 。 筆者這裏認爲是dicthashcodetable 。 意思就是字典表內部的一個hash相關的數組(僅我的理解)
- 以前也提到過redis內部不少地方會使用到字典!就比如咱們上學是用到【新華字典】、【成語詞典】、【歇後語詞典】等等。雖然名字叫法不同可是內部結構都是一部字典供咱們快速定位。而redis中dict內部就是經過type字段進行區分每一個字典的。而privdata是每部字典須要的特定參數。經過type和privdata就能夠輕鬆實現各類功能不一樣的字典,他有個專有名詞叫~~多態字典~~
rehash
- 除了type 、 privdata之外剩下的就是ht 、 rehashidx了。其中ht是一個長度爲2的數組。數組裏元素就是咱們以前提到了哈希表(dictht) 。 ht爲何長度爲2 這就須要咱們瞭解下redis的rehash過程了。而rehashidx就是記錄rehash的進度!在沒有發生rehash的時候rehashidx=-1;
- 在實際使用過程當中在字典中咱們全部的數據都會存儲在ht[0]對應的哈希表中。ht[1]永遠都是一個空數組。這些都是爲何rehash作準備,在正式開始以前咱們先來了解下redis爲何須要rehash這個動做
- 首先咱們在哈希表中是一個定長數組發生衝突時內部是經過鏈表解決的。理論上一個哈希表能夠存儲足夠的數據,這裏的足夠就是指空間容許的範圍有多少存多少。可是咱們知道鏈表的特色就是新增、刪除很快可是查詢很慢,尤爲是當鏈表很長的時候就會出現查詢效率低下的問題!爲了不鏈表過長redis就會在必定條件下對哈希表中數組長度的擴展從而解決局部鏈表過長的問題!
- 每次數組發生長度變化時,那麼以前的hash值就須要從新經歷一遍hash而後尋址index的過程。這個過程就叫作rehash 。

- 關於rehash和Java中Map的resize是同樣的功能!Java中resize是直接new 出一片內存進行復制的並且他是每次進行2倍擴展。而redis的rehash稍微不一樣基本上咱們也能夠理解成2倍擴展!關於兩塊內存複製有點相似於JVM中垃圾回收有點相似。有時間咱們能夠一塊兒研究下JVM章節。
- 那麼啥時候須要進行rehash呢?這裏和Java的負載因子同樣;可是除了負載因子這個空間考覈之外redis還考慮一個性能的問題。由於在單線程的前提下咱們還要考慮客戶端使用的感知性!單線程的意思就是執行命令是順序執行的。總不能在咱們rehash的過程當中所有阻塞客戶端的使用這對於操做體驗上穩定性來講是不友好的。

- 涉及到上述兩個命令的咱們稱之爲後臺命令結合負載因子產生以下條件



漸進式rehash
-
一直強調redis是單線程。那麼什麼叫單線程模型?就是對於redis服務來講執行命令是線性操做!可是每一個客戶端的命令是無序的,先到的就先進入隊列redis服務從隊列一次取出命令進行執行。除了客戶端的命令還有一些系統生成的命令好比說咱們上面提到的rehash操做!數組
-
①、首先爲了不阻塞客戶端或者說盡可能控制阻塞的時間在客戶端感知範圍內,redis內部的rehash並非一次性操做而是一個按部就班的過程。一次僅複製一部分markdown
-
②、還記得以前咱們提到dict中rehashidx這個屬性嗎,他是記錄rehash的進度。由於哈希表內部是一個數組而rehashidx就是記錄這個數組的索引。從而咱們也能夠知道每次rehash複製的時候是已一個索引完整鏈表爲單元進行復制的。數據結構
-
③、除了新增之外的其餘操做都會同時影響到ht[0]、ht[1] 由於在rehash過程當中兩個數組都是在使用狀態的工具
-
④、新增值的時候就只須要新增到ht[1]中。由於最終的目的就是將全部值同步到ht[1]中。而ht[0]的值會慢慢的變少;不必新增到ht[0]post
-
⑤、在rehash過程當中查找元素時會查找兩個數組中的並集元素。這也就也是了爲何再rehash過程新增元素只須要新增到ht[1]的緣由性能
總結
①、字典表在redis被普遍使用,基於字典表優秀的設計解決redis單線程問題學習
②、字典裏包含哈希表,哈希表內部使用節點負責存儲key、value
③、字典type實現多態字典用於多場景!
④、漸進式rehash解決服務卡頓問題