數據結構 |Redis中數據類型對應的數據結構

 

Redis

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

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

Redis 主要是作爲內存數據庫來使用,也就是說,數據是存儲在內存中的。儘管它經常被用作內存數據庫,但是,它也支持將數據存儲在硬盤中。

Redis 中,鍵的數據類型是字符串,值的數據類型有很多,常用的數據類型有字符串、列表、字典、集合、有序集合。

字符串(string)

“字符串(string)”這種數據類型非常簡單,對應到數據結構裏,就是字符串

列表(list)

列表這種數據類型支持存儲一組數據。這種數據類型對應兩種實現方法,一種是壓縮列表(ziplist),另一種是雙向循環鏈表

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

  • 列表中保存的單個數據(有可能是字符串類型的)小於 64 字節;

  • 列表中數據個數少於 512 個。

關於壓縮列表,它並不是基礎數據結構,而是 Redis 自己設計的一種數據存儲結構。它有點兒類似數組,通過一片連續的內存空間,來存儲數據。不過,它跟數組不同的一點是,它允許存儲的數據大小不同。具

體的存儲結構也非常簡單。

 

壓縮列表中的“壓縮”兩個字該如何理解 ?

聽到“壓縮”兩個字,直觀的反應就是節省內存。之所以說這種存儲結構節省內存,是相較於數組的存儲思路而言的。數組要求每個元素的大小相同,如果要存儲不同長度的字符串,那就需要

用最大長度的字符串大小作爲元素的大小(假設是 20 個字節)。當存儲小於 20 個字節長度的字符串的時候,便會浪費部分存儲空間。

壓縮列表這種存儲結構,一方面比較節省內存,另一方面可以支持不同類型數據的存儲。而且,因爲數據存儲在一片連續的內存空間,通過鍵來獲取值爲列表類型的數據,讀取的效率也非常高。

② 當列表中存儲的數據量比較大的時候,也就是不能同時滿足剛剛講的兩個條件的時候,列表就要通過雙向循環鏈表來實現了。

雙向循環鏈表這種數據結構了見之前。這裏着重看一下 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 中的很多數據類型,比如字典、有序集合等,都是通過多種數據結構來實現的,爲什麼會這樣設計呢?用一種固定的數據結構來實現,不是更加簡單嗎? 

redis的數據結構由多種數據結構來實現,主要是出於時間和空間的考慮,當數據量小的時候通過數組下標訪問最快、佔用內存最小,而壓縮列表只是數組的升級版;

因爲數組需要佔用連續的內存空間,所以當數據量大的時候,就需要使用鏈表了,同時爲了保證速度又需要和數組結合,也就有了散列表。

對於數據的大小和多少採用哪種數據結構,相信redis團隊一定是根據大多數的開發場景而定的。

數據結構持久化有兩種方法。對於二叉查找樹這種數據結構,我們如何將它持久化到磁盤中呢?

 二叉查找樹的存儲,傾向於存儲方式一,通過填充葉子節點形成完全二叉樹,然後以數組的形式存儲到硬盤,數據還原過程也是非常高效的。如果用存儲方式二就比較複雜了。

 

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