【redis前傳】redis整數集為什麼不能降級

煙花散盡13141發表於2021-07-21

前言

整數集合相信有的同學沒有聽說過,因為redis對外提供的只有封裝的五大物件!而我們本系列主旨是學習redis內部結構。內部結構是redis五大結構重要支撐!

前面我們分別從redis內部結構分析了redis的List、Hash、Zset三種資料結構了。今天我們再來分析set資料結構內部是如何儲存的

基本結構

  • 在src/t_set.c中我們發現這樣一段程式碼

image-20210706105819274

  • 由此我們可知在set中是由兩種資料結構構成的: hashtable+intset 。關於redis內部其他的結構我專門在【redis專欄中有介紹】。hashtable不是我們今天的主角,我們今天先分析intset俗稱整數集合。

image-20210706110151754

  • 從上圖中我們可以看出,我構造了兩個set集合分別為【commonset】、【cs】。兩個集合前者儲存字串、後者專門儲存數字。

  • 我們在通過object encoding key 來檢視下兩個集合的底層資料結構,發現一個是hashtable 一個是intset 。這也驗證了我們上面對set基本結構的描述。

  • 在redis中對外提供五大型別實際上都是redis的一個抽象物件叫做redisobject。在內部對映了我們redis內部的資料結構

image-20210706111432308

  • 針對commonset和cs兩個集合在內部資料結構大概可以這麼理解

image-20210706112349968

何時使用intset

  • 你可以單純的認為只要是數字就會使用intset結構來儲存,我恐怕要給你當頭一棒了。實際上並不是這樣

  • 需要同時滿足以下兩個條件:

image-20210706113636870

image-20210706113647350

intset

image-20210706133736601

  • 圖中表示的很清楚了,在intset中的encoding有三種取值分別代表contents儲存資料型別。這裡有人可能會有疑問了contents的型別不就是int8_t嗎?為什麼還需要encoding呢?這裡通過原始碼跟蹤內部的確跟int8_t沒啥關係。而且資料的預設型別就是int16_t 。關於length這裡無需太多解釋,記住一點表示contents元素的個數並非表示contents陣列的長度!
  • 瞭解intset的同學都知道在encoding三種取值範圍中涉及了升級的操作!在講升級之前我們先來了解下C、C++中int的取值範圍是如何定義的
  • int8_t的取值範圍是【-128,127】 。 類似於java中byte佔1個位元組也就是8位。他的取值範圍是

\[-2^{7} \sim 2^{7}-1 \\ 即 \\ -128 \sim 127 \]

image-20210706135925132

新增元素

sadd juejin -123
sadd juejin -6
sadd juejin 12
sadd juejin 56
sadd juejin 321	
  • juejin這個key內部就是intset 。

image-20210706162521929

  • 上面我們新增了5個元素且這五個元素的長度都在16之內!所以當前的intset的encoding=INTSET_ENC_INT16。-123在contents中佔前16位。

  • 所以當前五個元素佔contents的長度是16*5=80 ;

  • 注意set在儲存int型別資料時,內部是按照從小到大的順序儲存的。

型別變動

image-20210706164922957

  • 上面的問題不知道你有沒有考慮過,或者說有沒有遇到過!intset預設是int16位,正如我們上面新增的五個元素。加入此時我們新增第6個元素是65535(32位)。那麼此時16位的長度就不夠儲存了這個時候intset會怎麼做!
  • 另外當我們新增第6個元素後又將65535刪除了之後,結構和新增之前是否一樣!下面我們帶著這兩個問題來一探究竟!!!

升級

  • 首先我們針對第一問題來看看。原來五個元素都是16位就可以滿足了,這個時候新增的65535是32位長度的。那麼是不是可以直接追加32位分配給65535呢?
  • 答案是肯定不行,首先直接追加無法保證陣列元素的大小順序!其次如果前五個分別是16位,第6個是32位那麼在intset結構中沒有多餘的欄位來進行標記。也就是說在解析的時候就無法判斷應該解析16位還是32位了.
  • redis為了方便解析所以在有高長度加入時會將整個contents進行升級。意思就是將整個contents先進行擴容,然後在重新填充資料

image-20210706171505334

加入65535

  • 首先根據length可以確定擴容後元素個數為6 , 每個佔位32,所以contents長度為32*6=192 。 此時前80位內容保持不變

image-20210706171605386

舊資料移位

  • 開闢了足夠的空間後,我們就可以對舊資料進行移位了這裡我們從原陣列的末尾開始移動,在移動之前需要明確在新陣列中的排序位置。
  • 此時我們首先將321進行比對確定在新陣列中他的排名是第五名,那麼他將佔用新contents中128~159區間。

image-20210706172455270

  • 最終前5 個元素就會被移動好 。

image-20210706172652958

  • 最後將新加入的元素填充進去。當發生升級時肯定是因為新元素的長度大於原有長度了。那麼他的值一定會是在新陣列的兩端。負數在最左側,正數在最右側

image-20210706172836896

降級

  • 接下來就是第二個問題當新加入的65535又被刪除了redis該怎麼辦,這個時候元素長度實際16位就可以滿足了,但是此時encoding卻是32位的。按照我的看法應該在實現降級!
  • 但是遺憾的是redis並沒有,那麼請思考為什麼沒有?如果讓你實現你將如何實現

為什麼不實現降級

  • 當加入元素超過當前長度我們很容易就知道此時需要進行升級操作,但是當我們刪除一個資料時我們如何判斷是否需要降級卻很困難,我們需要重新遍歷一遍剩下的元素是否小於當前長度,實現複雜度O(N) 。這就是為什麼不進行降級原因之一
  • 你可能會說重新遍歷一遍很快的反正在記憶體中,那麼你有沒有想過如果降級之後又遇到升級情況,這樣來回的升級降級就降低了我們程式的效能了。我們知道升級是必須的所以這裡降級redis採取的是忽略的策略

小結

image-20210707135328472

參考資料:記憶體升級優化記憶體降級

相關文章