Redis存儲數據類型的底層實現原理

Redis字符串(key-string)的底層實現:Redis雖然是用C語言寫的,但卻沒有直接使用C語言的字符串,而是自己實現了一套字符串。目的就是爲了提升速度,提升性能。Redis構建了一個叫做簡單動態字符串(simple dynamic string),簡稱SDS。結構可如下圖表示:在這裏插入圖片描述

struct sdshdr{
	int len;	//記錄已使用長度
	int free;	//記錄未使用長度
	char[] buf;	//字符數組 個人理解爲指向一個字符數組的指針char* buf
};

Redis的字符串也會遵守C語言的字符串的實現規則,即最後一個字符爲空字符。然而這個空字符不會被計算在len裏頭。SDS的最厲害最奇妙之處在於它的Dynamic。動態變化長度。在進行擴容時會進行如下步驟:

  • 計算出所需的大小是否足夠
  • 開闢空間至滿足所需大小
  • 開闢已使用大小len相同長度的空閒free空間(如果len < 1m),開啓1m長度的空閒free空間(如果len>=1m)。

Redis 字符串的性能優勢:

  • 快速獲取字符串長度
  • 避免緩衝區溢出
  • 降低空間分配次數提升內存使用效率
    • 空間預分配:對於追加操作來說,Redis不僅會開闢空間至夠用而且還會預分配未使用的空間(free)來用於下一次操作。至於未使用的空間(free)的大小則由修改後的字符串長度決定。當修改後的字符串長度len < 1M,則會分配與len相同長度的未使用的空間(free) 當修改後的字符串長度len >= 1M,則會分配1M長度的未使用的空間(free)有了這個預分配策略之後會減少內存分配次數,因爲分配之前會檢查已有的free空間是否夠,如果夠則不開闢了。
    • 惰性空間回收:與上面情況相反,惰性空間回收適用於字符串縮減操作。比如有個字符串s1=“hello world”,對s1進行sdstrim(s1," world")操作,執行完該操作之後Redis不會立即回收減少的部分,而是會分配給下一個需要內存的程序。當然,Redis也提供了回收內存的api,可以自己手動調用來回收縮減部分的內存。

hash是一個鍵值對(key =>value)集合。

  • Redis hash是一個string(key)類型的field和val(value)值的映射表 ,hash結構特別適合用於存儲對象。Redis hash中的value的內部實現爲一個hashmap,並提供了直接存取這個map成員的接口。在hashmap中的key我們統一爲field,值統一爲val。在對數據的修改和存取時都可以直接通過其內部map的field, 也就是通過 key(存儲的數據集的key) + field(屬性標籤) 就可以操作對應屬性數據了,既不需要重複存儲數據,也不會帶來序列化和併發修改控制的問題,很好的解決了問題。
//哈希表結構定義
typedef struct dictht{
     dictEntry **table;//哈希表數組
     unsigned long size;//哈希表大小
     unsigned long sizemask;//哈希表大小掩碼,用於計算索引值,總是等於 size-1
     unsigned long used;//該哈希表已有節點的數量
}dictht

//哈希表是由數組 table 組成,table 中每個元素都是指向 dict.h/dictEntry 結構,dictEntry 結構定義如下:
typedef struct dictEntry{
     void *key;	//鍵  
     union{		//值
          void *val;
          uint64_tu64;
          int64_ts64;
     }v;
     struct dictEntry *next;//指向下一個哈希表節點,形成鏈表
}dictEntry
//key用來保存鍵,val用來保存值,這個值可以是一個指針也可以是uint64_t整數,也可以是int64_t整數
  • 總結Redis hash的結構是一個數組,數組元素指向一個鏈表,鏈表每一個節點是一個hash表,結構如下圖:
    在這裏插入圖片描述
  • Redis hash對應value內部實際就是一個hashmap(也可稱之爲字典),實際這裏會有2種不同實現,這個hash的成員比較少時Redis爲了節省內存會採用類似一維數組的方式來緊湊存儲(ziplist),而不會採用真正的hashmap結構,對應的value redisObject的encoding爲zipmap,當成員數量增大時會自動轉成真正的hashmap,此時encoding爲ht。即在數據量比較少的時候Redis hash的內部實現爲一個hash數組,當數據量較大的時候才採用hashmap的方式進行實現。
  • 壓縮列表(ziplist)是Redis爲了節省內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構,一個壓縮列表可以包含任意多個節點(entry),每個節點可以保存一個字節數組或者一個整數值。
    在這裏插入圖片描述
  • 注意這裏還有一個指向下一個哈希表節點的指針,我們知道哈希表最大的問題是存在哈希衝突,如何解決哈希衝突,有開放地址法和鏈地址法。Redis hash底層採用的是鏈地址法,通過next這個指針可以將多個哈希值相同的鍵值對連接在一起,用來解決哈希衝突
  • 擴容和收縮:當哈希表保存的鍵值對太多或者太少時,就要通過 rerehash(重新散列)來對哈希表進行相應的擴展或者收縮。具體步驟:
    • 如果執行擴容操作,會基於原hash表已使用的空間擴大一倍創建另一個hash表;同理,如果是收縮則會根據已使用的空間縮小一倍創建一個hash表。
    • 利用hash算法重新計算索引值,然後將鍵值對重新放到對應的位置上。
    • 所有鍵值對都同步完之後,釋放原hash表的內存空間。
  • 觸發擴容的條件:
    • 服務器目前沒有執行 BGSAVE 命令或者 BGREWRITEAOF 命令,並且負載因子大於等於1。
    • 服務器目前正在執行 BGSAVE 命令或者 BGREWRITEAOF 命令,並且負載因子大於等於5。
    • ps:負載因子 = 哈希表已保存節點數量 / 哈希表大小。
  • 漸近式 rehash:什麼叫漸進式 rehash?也就是說擴容和收縮操作不是一次性、集中式完成的,而是分多次、漸進式完成的。如果保存在Redis中的鍵值對只有幾個幾十個,那麼 rehash 操作可以瞬間完成,但是如果鍵值對有幾百萬,幾千萬甚至幾億,那麼要一次性的進行 rehash,勢必會造成Redis一段時間內不能進行別的操作。所以Redis採用漸進式 rehash,這樣在進行漸進式rehash期間,字典的刪除查找更新等操作可能會在兩個哈希表上進行,第一個哈希表沒有找到,就會去第二個哈希表上進行查找。但是進行 增加操作,一定是在新的哈希表上進行的。

Redis set是string類型的無序集合。底層也是採用hashtable(與上例中的hashmap結構類似)來進行實現的。集合中的數據都是無序的,所以進行插入、刪除、查詢操作的時間複雜度都是O(1)。

  • Redis set對外提供的功能與list類似,是一個列表功能,特殊之處在於set是可以進行自動排重的,當你需要存儲一個列表數據,又不希望出現重複時,set是一個很好的選擇,並且set還提供了一個檢驗成員是否存在於集合內的接口。

list是簡單的字符串列表,按插入的順序進行排序。Redis list的數據結構如下:

//節點結構
typedef  struct listNode{
       struct listNode *prev;//前置節點
       struct listNode *next;//後置節點
       void *value; //節點的值 
}listNode

//redis 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);//節點值對比函數
}list;

從結構來看,Redis list的底層實現是通過一個雙向鏈表(或一個ziplist),如圖:
在這裏插入圖片描述
Redis list的特性:

  • 雙端:鏈表具有指向前置節點和後置節點的指針,獲取這兩個節點的時間複雜度都爲O(1)
  • 無環:表頭節點的前驅指針和尾節點的後續指針都指向null,遍歷鏈表是也是以null作爲結束標記
  • 帶鏈表長度計數器:可以再O(1)的時間複雜度通過len屬性獲取鏈表的長度
  • 支持多種數據類型存儲:鏈表節點使用 void* 指針來保存節點值,可以保存各種不同類型的值

Redis zset和Redis set一樣也是一個string類型元素的集合,且不允許重複的成員,但zset可以通過用戶提供的一個優先級參數來爲集合內的成員進行排序。zset的內部實現爲一個跳躍表(skiplist)和map(上例中的hashmap),跳躍表結構如下圖:
在這裏插入圖片描述

  • 由圖可以看出skiplist是一種有序的數據結構,他通過每個節點中維持多個指向下一個節點的指針從而達到快速訪問節點的目的具有如下性質:
    • 由多層結構組成,每一層都是一個有序鏈表,排列順序由高層到低層,每層至少包含兩個鏈表節點,分別是前面的head與後面的nil節點。
    • 最底層的鏈表包含了所有元素。
    • 如果一個節點出現在該層,那麼該層之下的所有層都包含有該節點
  • Redis zset的結構定義如下:
//跳躍表
typedef struct zskiplistNode {
     struct zskiplistLevel{				//層
           struct zskiplistNode *forward;//前進指針
           unsigned int span;			//跨度
     }level[];
     struct zskiplistNode *backward;	//後退指針
     double score;						//分值
     robj *obj;							//成員對象
} zskiplistNode;

//跳躍表節點
typedef struct zskiplist{
     structz skiplistNode *header, *tail;//表頭節點和表尾節點
     unsigned long length;	//表中節點的數量
     int level;				//表中層數最大的節點的層數
}zskiplist;
  • 可以抽象爲如下結構:
    在這裏插入圖片描述
  • 搜索:從最高層的鏈表節點開始,如果比當前節點要大和比當前層的下一個節點要小,那麼則往下找,也就是和當前層的下一層的節點的下一個節點進行比較,以此類推,一直找到最底層的最後一個節點,如果找到則返回,反之則返回空。
  • 插入:首先確定插入的層數,有一種方法是假設拋一枚硬幣,如果是正面就累加,直到遇見反面爲止,最後記錄正面的次數作爲插入的層數。當確定插入的層數k後,則需要將新元素插入到從底層到k層。
  • 刪除:在各個層中找到包含指定值的節點,然後將節點從鏈表中刪除即可,如果刪除以後只剩下頭尾兩個節點,則刪除這一層。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章