Redis底層數據結構.md

1.Redis 概述

Redis 數據庫裏面的每個鍵值對(key-value) 都是由對象(object)組成的:
數據庫鍵總是一個字符串對象(string object);
數據庫的值則可以是字符串對象、列表對象(list)、哈希對象(hash)、集合對象(set)、有序集合(sort set)對象這五種對象中的其中一種。

2.Redis 底層數據結構

有以下數據類型:
簡單動態字符串(SDS), 鏈表, 字典, 跳躍表, 整數集合, 壓縮列表

2.1 SDS

Redis構建了一種名爲簡單動態字符串的抽象類型,作爲默認字符串表示
結構:

/*  
 * 保存字符串對象的結構  
 */  
struct sdshdr {  
    // buf 中已佔用空間的長度  
    int len;  
  
    // buf 中剩餘可用空間的長度  
    int free;  
  
    // 數據空間  
    char buf[];  
};

SDS 與C字符串的區別:

1.獲取字符串長度(SDS O(1)/C 字符串 O(n))
傳統的C 字符串 使用長度爲N+1 的字符串數組來表示長度爲N 的字符串,所以爲了獲取一個長度爲C字符串的長度,必須遍歷整個字符串。
和C 字符串不同,SDS 的數據結構中,有專門用於保存字符串長度的變量,我們可以通過獲取len 屬性的值,直接知道字符串長度

2.杜絕緩衝區溢出
C 字符串 不記錄字符串長度,除了獲取的時候複雜度高以外,還容易導致緩衝區溢出。
假設程序中有兩個在內存中緊鄰着的 字符串 s1 和 s2,其中s1 保存了字符串“redis”,二s2 則保存了字符串“MongoDb”。將s1 的內容修改爲redis cluster,但是又忘了重新爲s1 分配足夠的空間。那麼s2原來的內容會被覆蓋掉。
Redis 中SDS 的空間分配策略完全杜絕了發生緩衝區溢出的可能性:
當我們需要對一個SDS 進行修改的時候,redis 會在執行拼接操作之前,預先檢查給定SDS 空間是否足夠,如果不夠,會先拓展SDS 的空間,然後再執行拼接操作。

3.減少修改字符串時帶來的內存重分配次數   
C語言字符串在進行字符串的擴充和收縮的時候,都會面臨着內存空間的重新分配問題。
SDS在拓展時會進行預分配策略, 通過這種預分配策略,SDS將連續增長N次字符串所需的內存重分配次數從必定N次降低爲最多N次。

4.惰性釋放空間
SDS有free屬性可以記錄剩餘空間的,當對字符串進行收縮的時候,redis只記錄free的值,避免下次修改時,對字符串空間進行拓展。
SDS提供了相應的API,在需要的時候,自行釋放SDS的空餘空間。

5.二進制安全
C 字符串中的字符必須符合某種編碼,並且除了字符串的末尾之外,字符串裏面不能包含空字符,否則最先被程序讀入的空字符將被誤認爲是字符串結尾,這些限制使得C字符串只能保存文本數據,而不能保存想圖片,音頻,視頻,壓縮文件這樣的二進制數據。
但是在Redis中,不是靠空字符來判斷字符串的結束的,而是通過len這個屬性。那麼,即便是中間出現了空字符對於SDS來說,讀取該字符仍然是可以的。

3.鏈表

鏈表提供了高效的節點重排能力,以及順序性的節點訪問方式,並且可以通過增刪節點來靈活地調整鏈表的長度。
鏈表在Redis 中的應用非常廣泛,比如列表鍵的底層實現之一就是鏈表。當一個列表鍵包含了數量較多的元素,又或者列表中包含的元素都是比較長的字符串時,Redis 就會使用鏈表作爲列表鍵的底層實現。
1.鏈表的數據結構ListNode(雙向鏈表):

typedef struct listNode{
      struct listNode *prev;
      struct listNode * next;
      void * value;  
}

一般用list操作鏈表:

typedef struct list{
    //表頭節點
    listNode * head;
    //表尾節點
    listNode * tail;
    //鏈表長度
    unsigned long len;
    //節點值複製函數
    void *(*dup) (void *ptr);
    //節點值釋放函數
    void (*free) (void *ptr);
    //節點值對比函數
    int (*match)(void *ptr, void *key);
}

3.鏈表的特性
雙端:鏈表節點帶有prev 和next 指針,獲取某個節點的前置節點和後置節點的時間複雜度都是O(N)
無環:表頭節點的 prev 指針和表尾節點的next 都指向NULL,對立案表的訪問時以NULL爲截止
表頭和表尾:因爲鏈表帶有head指針和tail 指針,程序獲取鏈表頭結點和尾節點的時間複雜度爲O(1)
長度計數器:鏈表中存有記錄鏈表長度的屬性 len
多態:鏈表節點使用 void* 指針來保存節點值,並且可以通過list 結構的dup 、 free、 match三個屬性爲節點值設置類型特定函數。

4.字典

是一種用於保存鍵值對的抽象數據結構。 
在字典中,一個鍵(key)可以和一個值(value)進行關聯,字典中的每個鍵都是獨一無二的。在C語言中,並沒有這種數據結構,但是Redis 中構建了自己的字典實現。

1.字典的定義

# 1.哈希表
typedef struct dictht {
   //哈希表數組
   dictEntry **table;
   //哈希表大小
   unsigned long size;

   //哈希表大小掩碼,用於計算索引值
   unsigned long sizemask;
   //該哈希表已有節點的數量
   unsigned long used;
}
# 2.哈希表節點
typeof struct dictEntry{
   //鍵
   void *key;
   //值
   union{
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;
}
# 3.字典
typedef struct dict {
    // 類型特定函數
    dictType *type;
    // 私有數據
    void *privedata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    in trehashidx;

}

1.哈希表 我們可以看到,在結構中存有指向dictEntry 數組的指針,而我們用來存儲數據的空間既是dictEntry
2.哈希表節點 在數據結構中,我們清楚key 是唯一的,但是我們存入裏面的key 並不是直接的字符串,而是一個hash 值,通過hash 算法,將字符串轉換成對應的hash 值,然後在dictEntry 中找到對應的位置。
這時候我們會發現一個問題,如果出現hash 值相同的情況怎麼辦?Redis 採用了鏈地址法(類比於HashMap中的桶):
在這裏插入圖片描述

3.字典 type 屬性 和privdata 屬性是針對不同類型的鍵值對,爲創建多態字典而設置的。
ht 屬性是一個包含兩個項(兩個哈希表)的數組
在這裏插入圖片描述

2.Rehash

隨着對哈希表的不斷操作,哈希表保存的鍵值對會逐漸的發生改變,爲了讓哈希表的負載因子維持在一個合理的範圍之內,我們需要對哈希表的大小進行相應的擴展或者壓縮,這時候,我們可以通過 rehash(重新散列)操作來完成

5.跳錶

跳躍表(skiplist)是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。跳躍表是一種隨機化的數據,跳躍表以有序的方式在層次化的鏈表中保存元素,效率和平衡樹媲美 ——查找、刪除、添加等操作都可以在對數期望時間下完成,並且比起平衡樹來說,跳躍表的實現要簡單直觀得多。

Redis 只在兩個地方用到了跳躍表,一個是實現有序集合鍵,另外一個是在集羣節點中用作內部數據結構。
Redis 的跳躍表 主要由兩部分組成:zskiplist(鏈表)和zskiplistNode (節點)

typedef struct zskiplistNode{
   //層
     struct zskiplistLevel{
     //前進指針
        struct zskiplistNode *forward;
    //跨度
        unsigned int span;
    } level[];
  //後退指針
    struct zskiplistNode *backward;
  //分值
    double score;
  //成員對象
    robj *obj;
}
typedef struct zskiplist {
     //表頭節點和表尾節點
     structz skiplistNode *header,*tail;
     //表中節點數量
     unsigned long length;
     //表中層數最大的節點的層數
     int level;
}zskiplist;

1、層:level 數組可以包含多個元素,每個元素都包含一個指向其他節點的指針。
2、前進指針:用於指向表尾方向的前進指針
3、跨度:用於記錄兩個節點之間的距離
4、後退指針:用於從表尾向表頭方向訪問節點
5、分值和成員:跳躍表中的所有節點都按分值從小到大排序。成員對象指向一個字符串,這個字符串對象保存着一個SDS值
在這裏插入圖片描述

從結構圖中我們可以清晰的看到,header,tail分別指向跳躍表的頭結點和尾節點。level 用於記錄最大的層數,length 用於記錄我們的節點數量。
總結:
 - 跳躍表是有序集合的底層實現之一

  • 主要有zskiplist 和zskiplistNode兩個結構組成
  • 每個跳躍表節點的層高都是1至32之間的隨機數
  • 在同一個跳躍表中,多個節點可以包含相同的分值,但每個節點的對象必須是唯一的
  • 節點按照分值的大小從大到小排序,如果分值相同,則按成員對象大小排序

6.整數集合(Intset)

其實就是一個特殊的集合,裏面存儲的數據只能夠是整數,並且數據量不能過大。

typedef struct intset{
    //編碼方式
    uint32_t enconding;
   // 集合包含的元素數量
    uint32_t length;
    //保存元素的數組    
    int8_t contents[];

}

整數集合的升級

在上述數據結構圖中我們可以看到,intset 在默認情況下會幫我們設定整數集合中的編碼方式,但是當我們存入的整數不符合整數集合中的編碼格式時,就需要使用到Redis 中的升級策略來解決
Intset 中升級整數集合並添加新元素共分爲三步進行:
1、根據新元素的類型,擴展整數集合底層數組的空間大小,併爲新元素分配空間
2、將底層數組現有的所有元素都轉換成新的編碼格式,重新分配空間
3、將新元素加入到底層數組中

總結

  • 整數集合的底層實現爲數組,這個數組以有序,無重複的範式保存集合元素,在有需要時,程序會根據新添加的元素類型改變這個數組的類型
  • 升級操作爲整數集合帶來了操作上的靈活性,並且儘可能地節約了內存
  • 整數集合只支持升級操作,不支持降級操作

7.壓縮列表

壓縮列表是列表鍵和哈希鍵的底層實現之一。當一個列表鍵只把汗少量列表項,並且每個列表項要麼就是小整數,要麼就是長度比較短的字符串,那麼Redis 就會使用壓縮列表來做列表鍵的底層實現。

參考:http://www.cnblogs.com/jaycekon/p/6227442.html

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