1. 程式人生 > 其它 >如何將不同型別的資料放在一個列表中_阿里架構師剖析:Redis常用資料型別對應的資料結構

如何將不同型別的資料放在一個列表中_阿里架構師剖析:Redis常用資料型別對應的資料結構

技術標籤:如何將不同型別的資料放在一個列表中資料結構經典案例

本文就帶大家一塊兒看下,經典資料庫 Redis 中的常用資料型別,底層都是用哪種資料結構實現的

Redis資料庫介紹

Redis 是一種鍵值(Key-Value)資料庫。相對於關係型資料庫(比如 MySQL),Redis也被叫作非關係型資料庫

像 MySQL 這樣的關係型資料庫,表的結構比較複雜,會包含很多欄位,可以通過 SQL 語句,來實現非常複雜的查詢需求。而 Redis 中只包含“鍵”和“值”兩部分,只能通過“鍵”來查詢“值”。正是因為這樣簡單的儲存結構,也讓 Redis 的讀寫效率非常高。

除此之外,Redis 主要是作為記憶體資料庫來使用,也就是說,資料是儲存在記憶體中的。儘管它經常被用作記憶體資料庫,但是,它也支援將資料儲存在硬碟中。這一點,我們後面會介紹。

Redis 中,鍵的資料型別是字串,但是為了豐富資料儲存的方式,方便開發者使用,值的資料型別有很多,常用的資料型別有這樣幾種,它們分別是字串、列表、字典、集合、有序集合。

“字串(string)”這種資料型別非常簡單,對應到資料結構裡,就是字串。你應該非常熟悉,這裡我就不多介紹了。我們著重看下,其他四種比較複雜點的資料型別,看看它們底層都依賴了哪些資料結構。

列表(list)

我們先來看列表。列表這種資料型別支援儲存一組資料。這種資料型別對應兩種實現方法,一種是壓縮列表(ziplist),另一種是雙向迴圈連結串列。

當列表中儲存的資料量比較小的時候,列表就可以採用壓縮列表的方式實現。具體需要同時滿足下面兩個條件:

  • 列表中儲存的單個數據(有可能是字串型別的)小於 64 位元組;
  • 列表中資料個數少於 512 個。

關於壓縮列表,我這裡稍微解釋一下。它並不是基礎資料結構,而是 Redis 自己設計的一種資料儲存結構。它有點兒類似陣列,通過一片連續的記憶體空間,來儲存資料。不過,它跟陣列不同的一點是,它允許儲存的資料大小不同。具體的儲存結構也非常簡單,你可以看我下面畫的這幅圖。

b81ded92b5d39b4069f5279e06bba8ed.png

現在,我們來看看,壓縮列表中的“壓縮”兩個字該如何理解?

聽到“壓縮”兩個字,直觀的反應就是節省記憶體。之所以說這種儲存結構節省記憶體,是相較於陣列的儲存思路而言的。我們知道,陣列要求每個元素的大小相同,如果我們要儲存不同長度的字串,那我們就需要用最大長度的字串大小作為元素的大小(假設是 20 個位元組)。那當我們儲存小於 20 個位元組長度的字串的時候,便會浪費部分儲存空間。聽起來有點兒拗口,我畫個圖解釋一下。

8ce7b8ae1dfddadf3020bb86986231e0.png

壓縮列表這種儲存結構,一方面比較節省記憶體,另一方面可以支援不同型別資料的儲存。而且,因為資料儲存在一片連續的記憶體空間,通過鍵來獲取值為列表型別的資料,讀取的效率也非常高。

當列表中儲存的資料量比較大的時候,也就是不能同時滿足剛剛講的兩個條件的時候,列表就要通過雙向迴圈連結串列來實現了。

在連結串列裡,我們已經講過雙向迴圈連結串列這種資料結構了,如果不記得了,你可以先回去複習一下。這裡我們著重看一下 Redis 中雙向連結串列的編碼實現方式。

Redis 的這種雙向連結串列的實現方式,非常值得借鑑。它額外定義一個 list 結構體,來組織連結串列的首、尾指標,還有長度等資訊。這樣,在使用的時候就會非常方便。

//以下是C語言程式碼,因為Redis是用C語言實現的。typedef struct listnode {    struct listNode *prev;    struct listNode *next;    void *value;}listNode;typedef struct list {    listNode *head;    listNode *tail;    unsigned long len;    // ....省略其他定義}list;

字典(hash)

字典型別用來儲存一組資料對。每個資料對又包含鍵值兩部分。字典型別也有兩種實現方式。一種是我們剛剛講到的壓縮列表,另一種是散列表。

同樣,只有當儲存的資料量比較小的情況下,Redis 才使用壓縮列表來實現字典型別。具體需要滿足兩個條件:

  • 字典中儲存的鍵和值的大小都要小於 64 位元組;
  • 字典中鍵值對的個數要小於 512 個。

當不能同時滿足上面兩個條件的時候,Redis 就使用散列表來實現字典型別。Redis 使用MurmurHash2這種執行速度快、隨機性好的雜湊演算法作為雜湊函式。對於雜湊衝突問題,Redis 使用連結串列法來解決。除此之外,Redis 還支援散列表的動態擴容、縮容。

當資料動態增加之後,散列表的裝載因子會不停地變大。為了避免散列表效能的下降,當裝載因子大於 1 的時候,Redis 會觸發擴容,將散列表擴大為原來大小的 2 倍左右。

當資料動態減少之後,為了節省記憶體,當裝載因子小於 0.1 的時候,Redis 就會觸發縮容,縮小為字典中資料個數的大約 2 倍大小。

我們前面講過,擴容縮容要做大量的資料搬移和雜湊值的重新計算,所以比較耗時。針對這個問題,Redis 使用我們在散列表(中)講的漸進式擴容縮容策略,將資料的搬移分批進行,避免了大量資料一次性搬移導致的服務停頓。

集合(set)

集合這種資料型別用來儲存一組不重複的資料。這種資料型別也有兩種實現方法,一種是基於有序陣列,另一種是基於散列表。

當要儲存的資料,同時滿足下面這樣兩個條件的時候,Redis 就採用有序陣列,來實現集合這種資料型別。

  • 儲存的資料都是整數;
  • 儲存的資料元素個數不超過 512 個。

當不能同時滿足這兩個條件的時候,Redis 就使用散列表來儲存集合中的資料。

有序集合(sortedset)

有序集合這種資料型別,我們在跳錶裡已經詳細講過了。它用來儲存一組資料,並且每個資料會附帶一個得分。通過得分的大小,我們將資料組織成跳錶這樣的資料結構,以支援快速地按照得分值、得分割槽間獲取資料。

實際上,跟 Redis 的其他資料型別一樣,有序集合也並不僅僅只有跳錶這一種實現方式。當資料量比較小的時候,Redis 會用壓縮列表來實現有序集合。具體點說就是,使用壓縮列表來實現有序集合的前提,有這樣兩個:

  • 所有資料的大小都要小於 64 位元組;
  • 元素個數要小於 128 個。

資料結構持久化

儘管 Redis 經常會被用作記憶體資料庫,但是,它也支援資料落盤,也就是將記憶體中的資料儲存到硬碟中。這樣,當機器斷電的時候,儲存在 Redis 中的資料也不會丟失。在機器重新啟動之後,Redis 只需要再將儲存在硬碟中的資料,重新讀取到記憶體,就可以繼續工作了。

剛剛我們講到,Redis 的資料格式由“鍵”和“值”兩部分組成。而“值”又支援很多資料型別,比如字串、列表、字典、集合、有序集合。像字典、集合等型別,底層用到了散列表,散列表中有指標的概念,而指標指向的是記憶體中的儲存地址。 那 Redis 是如何將這樣一個跟具體記憶體地址有關的資料結構儲存到磁碟中的呢?

實際上,Redis 遇到的這個問題並不特殊,很多場景中都會遇到。我們把它叫作資料結構的持久化問題,或者物件的持久化問題。這裡的“持久化”,你可以籠統地可以理解為“儲存到磁碟”。

如何將資料結構持久化到硬碟?我們主要有兩種解決思路。

第一種是清除原有的儲存結構,只將資料儲存到磁碟中。當我們需要從磁碟還原資料到記憶體的時候,再重新將資料組織成原來的資料結構。實際上,Redis 採用的就是這種持久化思路。

不過,這種方式也有一定的弊端。那就是資料從硬碟還原到記憶體的過程,會耗用比較多的時間。比如,我們現在要將散列表中的資料儲存到磁碟。當我們從磁碟中,取出資料重新構建散列表的時候,需要重新計算每個資料的雜湊值。如果磁碟中儲存的是幾 GB 的資料,那重構資料結構的耗時就不可忽視了。

第二種方式是保留原來的儲存格式,將資料按照原有的格式儲存在磁碟中。我們拿散列表這樣的資料結構來舉例。我們可以將散列表的大小、每個資料被雜湊到的槽的編號等資訊,都儲存在磁碟中。有了這些資訊,我們從磁碟中將資料還原到記憶體中的時候,就可以避免重新計算雜湊值。

總結

本文主要介紹了 Redis 中常用資料型別底層依賴的資料結構,總結一下大概有這五種:

壓縮列表(可以看作一種特殊的陣列)、有序陣列、連結串列、散列表、跳錶。實際上,Redis就是這些常用資料結構的封裝。

你有沒有發現,有了資料結構和演算法的基礎之後,再去閱讀 Redis 的原始碼,理解起來就容易多了?很多原來覺得很深奧的設計思想,是不是就都會覺得順理成章了呢?

還是那句話,夯實基礎很重要。同樣是看原始碼,有些人只能看個熱鬧,瞭解一些皮毛,無法形成自己的知識結構,不能化為己用,過不幾天就忘了。而有些人基礎很好,不但能知其然,還能知其所以然,從而真正理解作者設計的動機。這樣不但能有助於我們理解所用的開源軟體,還能為我們自己創新添磚加瓦。

推薦閱讀

盤點:2020年最新、最全、最實用的Java崗面試真題,已收錄GitHub

絕對乾貨,掌握這27個知識點,輕鬆拿下80%的技術面試(Java崗)

一線大廠為什麼面試必問分散式?

在一次又一次的失敗中,我總結了這份萬字的《MySQL效能調優筆記》

併發程式設計詳解:十三個工具類,十大設計模式,從理論基礎到案例實戰

如何高效部署分散式訊息佇列?這份《RabbitMQ實戰》絕對可以幫到你