前言
-
相信你一定使用過新華字典吧!小時候不會讀的字都是通過字典去查詢的。在
Redis
中也存在相同功能叫做字典又稱為符號表!是一種儲存鍵值對的抽象資料結構 -
本篇仍然定位在【redis前傳】系列中,因為本篇仍然是在解析redis資料結構!當你嘗試去了解redis時才能明白其中原理!才能明白為什麼redis被大家吹捧速度快,而不是被告知redis很快!
應用場景
- 在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並不是一次性操作而是一個循序漸進的過程。一次僅複製一部分
-
②、還記得之前我們提到dict中rehashidx這個屬性嗎,他是記錄rehash的進度。因為雜湊表內部是一個陣列而rehashidx就是記錄這個陣列的索引。從而我們也可以知道每次rehash複製的時候是已一個索引完整連結串列為單元進行復制的。
-
③、除了新增以外的其他操作都會同時影響到ht[0]、ht[1] 因為在rehash過程中兩個陣列都是在使用狀態的
-
④、新增值的時候就只需要新增到ht[1]中。因為最終的目的就是將所有值同步到ht[1]中。而ht[0]的值會慢慢的變少;沒必要新增到ht[0]
-
⑤、在rehash過程中查詢元素時會查詢兩個陣列中的並集元素。這也就也是了為什麼再rehash過程新增元素只需要新增到ht[1]的原因
總結
①、字典表在redis被廣泛使用,基於字典表優秀的設計解決redis單執行緒問題
②、字典裡包含雜湊表,雜湊表內部使用節點負責儲存key、value
③、字典type實現多型字典用於多場景!
④、漸進式rehash解決服務卡頓問題