你不知道的Redis八-Redis底層數據結構解析

目錄

一、Redis存儲的數據的數據結構

二、Redis中鍵和值得數據結構

1、redis鍵值的數據結構

2、hash衝突

3、rehash阻塞

4、漸進式rehash

二、壓縮列表

三、跳錶

四、rdis使用建議


一、Redis存儲的數據的數據結構

我們都只到Redis常用的數據結構爲String,List,Hash,Set,Sorted Set。但這只是我們在用的時候鍵值對的表現形式,他們底層真正使用的數據結構爲簡單動態字符串,雙向鏈表,壓縮列表,哈希表,調錶和整數數組

 

可以看到,String 類型的底層實現只有一種數據結構,也就是簡單動態字符串。而 List、Hash、Set 和 Sorted Set 這四種數據類型,都有兩種底層實現結構。通常情況下,我們會把這四種類型稱爲集合類型,它們的特點是一個鍵對應了一個集合的數據

二、Redis中鍵和值得數據結構

1、redis鍵值的數據結構

因爲redis本身要求獲取速度快,那麼時間複雜度肯定是O1,爲了實現從鍵到值的快速訪問,Redis 使用了一個哈希表來保存所有鍵值對。數據結構如下

 

redis是由一個全局hash表存儲所有鍵和值得關係。值存的爲對象的地址。

2、hash衝突

既然使用hash表,那麼就肯定會有hash衝突。Redis 解決哈希衝突的方式,和JAVA中的hash表解決方式一樣,也是鏈式哈希。鏈式哈希也很容易理解,就是指同一個哈希桶中的多個元素用一個鏈表來保存,它們之間依次用指針連接。

 

因此當Key特別大的時候,存在着鏈式查找的一個時間消耗。特別是當我們存在幾千萬key的時候,那這個時間消耗也是非常多的

 

3、rehash阻塞

爲了解決hash衝突帶來的鏈表長度太長,因此redis引入對hash的rehash操作

rehash 也就是增加現有的哈希桶數量,讓逐漸增多的 entry 元素能在更多的桶之間分散保存,減少單個桶中的元素數量,從而減少單個桶中的衝突。

具體操作

Redis 默認使用了兩個全局哈希表:哈希表 1 和哈希表 2。一開始,當你剛插入數據時,默認使用哈希表 1,此時的哈希表 2 並沒有被分配空間。隨着數據逐步增多,Redis 開始執行 rehash,這個過程分爲三步:

1、給哈希表 2 分配更大的空間,例如是當前哈希表 1 大小的兩倍;

2、把哈希表 1 中的數據重新進行打散映射到hash表2中;

3、釋放哈希表 1 的空間。

我們就可以從哈希表 1 切換到哈希表 2,用增大的哈希表 2 保存更多數據,而原來的哈希表 1 留作下一次 rehash 擴容備用

這個過程看似簡單,但是第二步涉及大量的數據拷貝,如果一次性把哈希表 1 中的數據都遷移完,會造成 Redis 線程阻塞,無法服務其他請求。此時,Redis 就無法快速訪問數據了。

爲了避免這個問題,Redis 採用了漸進式 rehash

4、漸進式rehash

簡單來說就是在第二步拷貝數據時,Redis 仍然正常處理客戶端請求,每處理一個請求時,從哈希表 1 中的第一個索引位置開始,順帶着將這個索引位置上的所有 entries 拷貝到哈希表 2 中;等處理下一個請求時,再順帶拷貝哈希表 1 中的下一個索引位置的 entries。如下圖所示:

這樣就巧妙地把一次性大量拷貝的開銷,分攤到了多次處理請求的過程中,避免了耗時操作,保證了數據的快速訪問。

二、壓縮列表

之前也說了,集合類型的底層數據結構主要有 5 種:整數數組、雙向鏈表、哈希表、壓縮列表和跳錶。其中,哈希表剛纔已經講過,整數數組、雙向鏈表也比較常見。那麼我們具體看下壓縮列表和跳錶

zlbytes:記錄壓縮列表佔用的內存字節數,對壓縮列表進行內存重分配或者計算zlen位置時使用,佔用4個字節

Zltail:記錄也鎖列表未節點距離壓縮列表的起始節點有多少字節,通過這個偏移量,程序無需便利整個壓縮列表就可以確定表尾節點的位置 佔用4個字節

Zllen:記錄列表包含的節點數量,佔用2個字節

Zllend:記錄壓縮列表的末端,佔用1個字節

entry:壓縮列表保存的數據,主要有以下屬性

  • prev_len,表示前一個 entry 的長度。prev_len 有兩種取值情況:1 字節或 5 字節。取值 1 字節時,表示上一個 entry 的長度小於 254 字節。雖然 1 字節的值能表示的數值範圍是 0 到 255,但是壓縮列表中 zlend 的取值默認是 255,因此,就默認用 255 表示整個壓縮列表的結束,其他表示長度的地方就不能再用 255 這個值了。所以,當上一個 entry 長度小於 254 字節時,prev_len 取值爲 1 字節,否則,就取值爲 5 字節。
  • len:表示自身長度,4 字節;
  • encoding:表示編碼方式,1 字節;
  • content:保存實際數據。

如果我們要查找定位第一個元素和最後一個元素,可以通過表頭三個字段的長度直接定位,複雜度是 O(1)。而查找其他元素時,就沒有這麼高效了,只能逐個查找,此時的複雜度就是 O(N) 了。

三、跳錶

有序鏈表只能逐一查找元素,導致操作起來非常緩慢,於是就出現了跳錶。具體來說,跳錶在鏈表的基礎上,增加了多級索引,通過索引位置的幾個跳轉,實現數據的快速定位。

如果我們使用上圖所示的跳躍表,就可以減少查找所需時間爲O(n/2),因爲我們可以先通過每個節點的最上面的指針先進行查找,這樣子就能跳過一半的節點。

比如我們想查找50,首先和20比較,大於20之後,在和40進行比較,然後在和70進行比較,發現70大於50,說明查找的點在40和50之間,從這個過程中,我們可以看出,查找的時候跳過了30。

跳躍表其實也是一種通過“空間來換取時間”的一個算法,令鏈表的每個結點不僅記錄next結點位置,還可以按照level層級分別記錄後繼第level個結點。此法使用的就是“先大步查找確定範圍,再逐漸縮小迫近”的思想進行的查找。跳躍表在算法效率上很接近紅黑樹。

四、Redis數據結構時間複雜度

 

五、Redis使用建議

第一,對於單元素操作,對於redis的使用盡量使用單元素操作。這種效率一般都比較高。

第二,範圍操作,是指集合類型中的遍歷操作,可以返回集合中的所有數據,這類操作一般都比較耗時,儘量避免

第三,統計操作,是指集合類型對集合中所有元素個數的記錄,因爲這些數據結構本身記錄所有元素個數,時間複雜度爲O(1)所以效率很高

第四,例外情況,是指某些數據結構的特殊記錄,例如壓縮列表和雙向鏈表都會記錄表頭和表尾的偏移量。對於頭插和尾插增刪元素可以通過偏移量直接定位,所以時間複雜度也是O(1)。實現快速操

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