redis字典快速映射+hash釜底抽薪+漸進式rehash | redis爲什麼那麼快

前言

  • 相信你一定使用過新華字典吧!小時候不會讀的字都是通過字典去查找的。在Redis中也存在相同功能叫做字典又稱爲符號表!是一種保存鍵值對的抽象數據結構

  • 本篇仍然定位在【redis前傳】系列中,因爲本篇仍然是在解析redis數據結構!當你嘗試去了解redis時才能明白其中原理!才能明白爲什麼redis被大家吹捧速度快,而不是被告知redis很快!

應用場景

  • 在Redis中有很多場景都是用了字典作爲底層數據結構!我們使用最多的應該是redis的庫的設置和五種基本數據類型的Hash結構數據!
  • 在上一篇【redis前傳】中我們學習了list數據結構。今天我們繼續學習主流數據結構Hash。
  • 在redis內部有字典結構、hash結構但是這裏的hash和我們平時熟知的redis基礎數據的hash並不是一個意思!我們簡單的將字典結構、hash結構理解成redis更加底層的一種抽象結構。平時我們使用的hash基礎數據結構理解成hash工具

image-20210624161020745

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

哈希表

image-20210624164553947

  • 上面這張圖詮釋了作爲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結構如何吧!

image-20210624165611646

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

image-20210625113012772

字典

多態字典

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

image-20210625110556458

  • 通過圖示我們明確的看出內部是包含哈希表的。其實從名字上我們也可以看出哈希表爲什麼叫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

image-20210625133555602

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

image-20210625140300363

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

image-20210625140528097

image-20210625142224557

image-20210625142326375

漸進式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解決服務卡頓問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章