Redis數據結構探究

1、與其他數據庫的對比

最近系統中引入了Redis,在應用中發現Reids具有關係型數據庫或其他緩存服務器所不具備的優點。
與關係型數據庫如Mysql相比,Reids屬於非關係型數據庫,類似於Nosql,不同數據之間不需要有關聯關係。
memcache也可以用來存儲鍵值映射,同是對內存操作,和Redis性能差別不大,但是Redis具備以兩種形式將數據寫入硬盤的能力,並且除了存儲普通的字符串鍵值外,還可以存儲其他四種數據結構,而memcache只能存儲字符串鍵。所以Redis可以用來解決更廣泛的問題,而且不只可以用來當做緩存數據庫,更可以用作主數據庫使用。

2、Redis的五種數據類型

Redis可以存儲五種鍵和數據類型間的映射,分別是STRING、LIST、SET、HASH、ZSET,其中STRING、LIST、SET、HASH在大多數編程語言中都存在,在實現和作用上也比較相似。另外ZSET是特有的一種數據結構,是一種有序集合。
以下是五種數據類型之間的對比和特點。

結構類型 結構存儲的值 結構操作
STRING 字符串、整數、浮點數 可以對字符串或字符串一部分執行操作,對整數和浮點數進行++ –操作
LIST 鏈表,鏈表的每個節點包含一個字符串 可以在鏈表兩端進行push pull(起到了棧的作用),根據偏移量進行trim,讀取單個或多個元素,根據值查找或刪除元素
SET 無序集合 添加/獲取/移除元素,判斷是否存在,交差並集合,隨機獲取元素
HASH 無序散列表 添加、移除、獲取鍵值對,獲取所有鍵值對
ZSET 有序集合,由字符串和分數組成,元素排列順序根據分值 添加、獲取、刪除元素,根據分值獲取元素

2.1 STRING

Redis的字符串映射的鍵和值都是字符串類型,可以存儲字符串、整數或浮點數。和memcache的鍵值對作用相同。除了普通字符串類型,在存儲一些複雜結構時,比如本次在系統中存儲一個java對象,就是將對象序列化爲字符串進行儲存,另外,也可以將對象序列化爲json或xml進行存儲。
一般STRING類型有以下應用場景:

  • 緩存,存儲基礎數據信息(文章、用戶、記錄等)。僅當update/insert時訪問數據庫並更新緩存,其他時候只訪問緩存,可以顯著減輕數據庫壓力。
  • 用作session服務器,一般服務器集羣中共享session會存儲在數據庫、session服務器、redis中,存取較快。
  • 計數器等操作頻繁的數據請求,因爲STRING類型可以存儲整數或浮點數並可以自增,所以用作計數器、pingback等非常方便。

2.2 LIST

LIST就是我們經常用到的數據結構中的隊列,Redis的LIST是雙端隊列,兩端都可以進行Push和pull操作,所以也可以當做棧來使用,另外還可以獲取一定範圍內的列表。
LIST有以下應用場景:

  • 分佈式系統中服務間調用可以使用LIST實現消息隊列,實現異步調用,使應用間解耦。
  • 高併發場景下可以使用LIST實現限流。
  • 文章分頁或者是有序的列表數據可以使用LIST,因爲可以在某個範圍內存取元素。

2.3 HASH

上文中提到了STRING可以用來存儲序列化的對象,但是如果這個對象需要進行操作的話,需要先解析再修改再序列化保存,無法直接修改。而HASH類型可以解決這個問題,在Redis中,HASH結構可以解決這個問題,它的鍵也是一個鍵值對,用戶可以直接修改HASH結構中某個屬性的值。所以適合存儲對象。
HASH有以下應用場景:

  • 存儲用戶數據,用戶基礎數據是請求頻率較高的,但是屬性較多且經常需要修改,所以可以存儲在HASH,修改時update某個屬性就可以。

2.4 SET

SET是數據結構中的集合,與列表不同的是它不保存重複數據,並且元素無序,所以不能獲取一定範圍內的元素,也不能根據索引獲取。Redis支持對集合進行交併差集計算,在很多場景下都可以發揮作用。
SET有以下應用場景:

  • 數據排重,比如記錄今天訪問的用戶,可以使用集合。
  • 標籤操作,系統中有個功能是打標籤,每打一個標籤,可以將該數據保存在以標籤id爲key的set中,展示時直接展示該set。
  • 用戶關係操作,比如計算用戶的共同好友等。

2.5 ZSET

有序集合是一種特殊的集合,它既具備集合不重複元素的特點,又可以進行排序。但它和list不同的是,它不是根據下標排序,所以排序不固定,而是它的value中包含一個分值,分值可以是分數、訪問量等等,根據自定義類別進行排序。
ZSET有以下應用場景:

  • 排行榜,排序操作最常用的應用就是排行榜,比如考試分數、訪問量、點贊量。考慮的一個應用場景是在wiki中的精選文章中,對文章進行排序。因爲精選文章相對固定但需要實時變動,應用ZSET比較合適。

3、底層數據結構的實現

我們知道Redis是使用ANSI C實現的,那它是怎麼實現的集合等複雜數據結構,比較令人好奇,在閱讀了《Redis設計與實現》這本書後,得到了比較準確的答案。首先我們需要了解一下Redis中使用到的數據結構。

3.1 簡單動態字符串

最開始以爲Redis是使用c++中的char數組來存儲字符串類型,其實並不是這樣,它定義了一個名爲simple dynamic string(SDS)的結構體用來保存字符串類型,結構如下:

struct sdshdr{
    int len;
    int free;
    int buf[];
}

其中:

  • len:buf中已用長度
  • free:bug剩餘可用長度
  • buf[] : 字符數組

我們存儲的字符串就是存儲在buf數組中,這樣做的原因是char[]並不能滿足Redis的操作需求,或是會帶來較大的性能消耗,比如append,獲取長度等等。像一些高級語言,也普遍有這種實現方式,像之前在閱讀PHP數據結構源碼時,string類型也是由數組和一個標誌長度的int值實現。這樣獲取長度的複雜度就是O(1)。另外還有一些好處:

  • 防止緩衝區溢出
    字符串長度增加時,如果內存相鄰地址已有內容,則會發生緩衝區溢出的現象,而Redis在擴展字符串時,會先檢查free長度,如果不夠時,會先拓展空間。
  • 減少內存分配次數
    SDS的擴展策略是小於1MB時,每次擴展到之前的二倍大小,大於1MB時,每次擴展1MB,所以不需要每次變化都重新分配內存。另外由於釋放空間時採用惰性空間釋放,減少了內存分配次數。
  • 惰性空間釋放
    SDS釋放空間時並不真正釋放內存空間,而是修改free的值,既能避免內存泄露,又減少內存分配次數。
  • 二進制安全
    c字符串末尾默認是空字符,所以在首次讀入空字符時會被認爲字符串結束。而SDS記錄了len長度,可以通過長度獲取內容,有空字符也不影響,所以可以用來存儲二進制數據。

簡單動態字符串是Redis最重要的數據結構,鍵值等字符串類型是使用它,另外還有AOF模塊中的AOF緩衝區,以及客戶端狀態中的輸入緩衝區也是由它實現。

3.2 鏈表

Redis中的LIST實現之一就是鏈表的。由鏈表和節點兩種數據結構組成,節點用來存儲數據和指針,鏈表結構封裝了一些複製和刪除節點的操作。
下面是節點的結構體:

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

普通鏈表節點一般只有一個指針指向下一個節點,而這裏面有pre和next兩個指針,實現了一個雙向鏈表。
下面是鏈表結構體:

typedef struct list{
    listNode *head;
    listNode *tail;
    unsigned long len;
    void *(*dup) (void *ptr)
    void *(*free)(void *ptr)
    int (*match)(void *ptr,void *key);
}list;

list結構體中除了包含head頭結點,tail尾節點,len長度外,還封裝了複製節點、刪除節點、匹配節點的方法。

3.3 hashtable

Redis中HASH的實現方式之一是hashtable,由dict和dictht兩種數據結構實現。
其中字典dict的數據結構:

typedef struct dict {  
    dictType *type;  
    void *privdata;  
    dictht ht[2];  
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */  
    int iterators; /* number of iterators currently running */  
} dict; 

type 屬性 和privdata 屬性是針對不同類型的鍵值對,爲創建多態字典而設置的。ht 屬性是一個包含兩個項(兩個哈希表)的數組。指向了兩個哈希表dictht。

typedef struct dictht {  
    dictEntry **table;  
    unsigned long size;  
    unsigned long sizemask;  
    unsigned long used;  
} dictht; 

其中dictht中的table是用來存儲kv元素的,每個dictEntry包含一對kv。
最後,哈希表節點dictEntry結構定義爲:

typeof struct dictEntry{
   void *key;
   union{
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;
}

其中的next指針是爲了解決hash衝突時,使用了鏈地址法組成鏈表。在此不對如何解決hash衝突和使用的散列算法進行深入討論。
如圖所示:
這裏寫圖片描述
圖片來自引用
HASH的另一種實現方式是ziplist。

3.4 intset

Redis中SET的實現方式有兩種,其中一種是hashtable,在上文中已經有過了解。另外一種是intset,這是一個整數集合,裏面存的爲某種同一類型的整數,支持如下三種長度的整數:

#define INTSET_ENC_INT16 (sizeof(int16_t))  
#define INTSET_ENC_INT32 (sizeof(int32_t))  
#define INTSET_ENC_INT64 (sizeof(int64_t))  

intset結構體爲:

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

其中裏面的數據按照從大到小的元素排列。並且存儲的類型由encoding決定。在集合中查找元素的複雜度爲O(logN)。但插入時設計到從16位升級到32位或64位,所以複雜度不一定。並且升級後不能再降級。

3.5 skiplist

Redis的ZSET實現方式之一是跳躍表,另一種是ziplist。

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

跳躍表主要由鏈表和節點組成,下面是鏈表zskiplist的數據結構。

typedef struct zskiplist {  
    struct zskiplistNode *header, *tail;
    unsigned long length;  
    //最大節點成熟
    int level;  
} zskiplist;  

可以看到,和普通鏈表不同的是,多了一個level用來記錄表中層數最大的節點的層數。
下面是節點的結構體:

typedef struct zskiplistNode {  
    // member  
    robj *obj;  
    // 分值  
    double score;  
    // 後退指針  
    struct zskiplistNode *backward;  
    // 層  
    struct zskiplistLevel {  
        // 前進指針  
        struct zskiplistNode *forward;  
        // 這個層跨越的節點數量  
        unsigned int span;  
    } level[];  
} zskiplistNode;  

其中除了value和分數score外,還包含了一個level[]數組,這就是每個節點裏的分層指針數組。
如圖所示:
這裏寫圖片描述
圖片來自引用
其中每個節點的層數都是1到32的隨機數,節點間是按照分值大小來進行排序。

3.6 ziplist

這是一種壓縮列表,在LIST和HASH中使用,因爲它保存在連續的內存空間中,所以比價節省內存空間,但在插入時每次都需要重新分配空間。
如圖所示:
這裏寫圖片描述
圖片來自引用
其中,包含如下屬性:

  • zlbytes:用於記錄整個壓縮列表佔用的內存字節數
  • zltail:記錄要列表尾節點距離壓縮列表的起始地址有多少字節
  • zllen:記錄了壓縮列表包含的節點數量。
  • entryX:要說列表包含的各個節點
  • zlend:用於標記壓縮列表的末端

3.7 Redis對象的實現

上面講解了Reids實現中使用到的數據結構,其中Redis中的每個對象都不是由固定的數據結構實現,而是會根據數據類型大小選擇不同的實現方式,以下爲對應表。

Redis對象 實現方式
STRING int 實現
embstr編碼的簡單動態字符串(SDS)實現
SDS實現
LIST ziplist壓縮列表實現
鏈表實現
HASH ziplist實現
字典hashtable實現
SET intset整數集合實現
hashtable實現
ZSET ziplist實現
跳錶skiplist、hashtable實現

總結

通過閱讀《Redis設計與實現》前兩章,對Redis的底層實現有了初步的瞭解。後續會繼續研究Redis兩種持久化方式和線程架構方面。


參考來源:
1. 《Redis設計與實現》
2. 《Redis實戰》
3. http://www.cnblogs.com/jaycekon/p/6227442.html
4. https://blog.csdn.net/u011531613/article/details/70193720?locationNum=7&fps=1
5. https://blog.csdn.net/wcf373722432/article/details/78678504
6. https://www.cnblogs.com/ysocean/p/9080942.html

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