簡單動態字串(simple dynamic string)SDS

a1322674015發表於2019-12-06

Redis 沒有直接使用C語言傳統的字串表示,而是自己構建了一種名為簡單動態字串(simple dynamic string SDS)的抽象型別,並將SDS用作Redis 的預設字串表示:

10.143.128.165:6379> SET msg "hello world"
OK

設定一個key= msg,value = hello world 的新鍵值對,

鍵(key)是一個字串物件,物件的底層實現是一個儲存著字串“msg” 的SDS;

值(value)也是一個字串物件,物件的底層實現是一個儲存著字串“hello world” 的SDS

SDS除了用來儲存字串以外,SDS還被用作緩衝區(buffer)AOF模組中的 AOF緩衝區

SDS 的定義

Redis 中定義動態字串的結構:

/*  
 * 儲存字串物件的結構  
 */  
struct sdshdr {       
    int len;// buf 中已佔用空間的長度      
    int free;// buf 中剩餘可用空間的長度    
    char buf[];// 資料空間  
};

1、len 變數,用於記錄buf 中已經使用的空間長度(這裡指出Redis 的長度為5)

2、free 變數,用於記錄buf 中還空餘的空間( 初次分配空間,一般沒有空餘,在對字串修改的時候,會有剩餘空間出現

3、buf 字元陣列,用於記錄我們的字串(記錄Redis)

SDS 與 C 字串的區別

 

C 字串 SDS
獲取字串長度的複雜度為O(N) 獲取字串長度的複雜度為O(1)
API 是不安全的,可能會造成緩衝區溢位 API 是安全的,不會造成緩衝區溢位
修改字串長度N次必然需要執行N次記憶體重分配 修改字串長度N次最多執行N次記憶體重分配
只能儲存文字資料 可以儲存二進位制資料和文字文資料
可以使用所有<String.h>庫中的函式 可以使用一部分<string.h>庫中的函式

1 獲取字串長度(SDS O(1)/C 字串 O(n))

傳統的C 字串 使用長度為N+1 的字串陣列來表示長度為N 的字串,所以為了獲取一個長度為C字串的長度,必須遍歷整個字串。

SDS 的資料結構中,有專門用於儲存字串長度的變數,可以透過獲取len 屬性的值,直接知道字串長度。

2 杜絕緩衝區溢位

C 字串 不記錄字串長度,除了獲取的時候複雜度高以外,還容易導致緩衝區溢位。

假設程式中有兩個在記憶體中緊鄰著的 字串 s1 和 s2,其中s1 儲存了字串“redis”,二s2 則儲存了字串“MongoDb”:

如果我們現在將s1 的內容修改為 redis cluster,但是又忘了重新為s1 分配足夠的空間,這時候就會出現以下問題:

原本s2 中的內容已經被S1的內容給佔領了,s2 現在為 cluster,而不是“Mongodb”。

 

當需要對一個SDS 進行修改的時候,redis 會在執行拼接操作之前,預先檢查給定SDS 空間是否足夠,如果不夠,會先擴充SDS 的空間,然後再執行拼接操作:

3 減少修改字串時帶來的記憶體重分配次數

C語言字串在進行字串的擴充和收縮的時候,都會面臨著記憶體空間的重新分配問題。

1. 字串拼接會產生字串的記憶體空間的擴充,在拼接的過程中,原來的字串的大小很可能小於拼接後的字串的大小,那麼這樣的話,就會導致一旦忘記申請分配空間,就會導致記憶體的溢位。

2. 字串在進行收縮的時候,記憶體空間會相應的收縮,而如果在進行字串的切割的時候,沒有對記憶體的空間進行一個重新分配,那麼這部分多出來的空間就成為了記憶體洩露。

我們需要對下面的SDS進行擴充,則需要進行空間的擴充,這時候redis 會將SDS的長度修改為13位元組,並且將未使用空間同樣修改為1位元組

 

因為在上一次修改字串的時候已經擴充了空間,再次進行修改字串的時候會發現空間足夠使用,因此無須進行空間擴充

透過這種預分配策略,SDS將連續增長N次字串所需的記憶體重分配次數從必定N次降低為最多N次

4 惰性空間釋放

SDS 的free 屬性,是用於記錄空餘空間的。除了在擴充字串的時候會使用到free 來進行記錄空餘空間以外,在對字串進行收縮的時候,也可以使用free 屬性來進行記錄剩餘空間,避免下次對字串進行再次修改的時候,需要對字串的空間進行擴充。

SDS 提供了相應的API,可以在有需要的時候,自行釋放SDS 的空餘空間。

透過惰性空間釋放,SDS 避免了縮短字串時所需的記憶體重分配操作,並未將來可能有的增長操作提供了最佳化

5 二進位制安全

C 字串中的字元必須符合某種編碼,並且除了字串的末尾之外,字串裡面不能包含空字元,否則最先被程式讀入的空字元將被誤認為是字串結尾,這些限制使得C字串只能儲存文字資料,而不能儲存想圖片,音訊,影片,壓縮檔案這樣的二進位制資料。

Redis 不是靠空字元來判斷字串的結束的,而是透過len這個屬性

6 相容部分C字串函式

雖然SDS 的API 都是二進位制安全的,但一樣遵循C字串 以空字串結尾的慣例。

========================================================================

連結串列

連結串列提供了高效的節點重排能力,以及順序性的節點訪問方式,並且可以透過增刪節點來靈活地調整連結串列的長度。

Redis 中 列表鍵的底層實現之一就是連結串列。當一個列表鍵包含了數量較多的元素,又或者列表中包含的元素都是比較長的字串時,Redis 就會使用連結串列作為列表鍵的底層實現。

每個連結串列節點使用一個  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);//節點值對比函式
}

連結串列的特性

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

========================================================================

字典

字典,又稱為符號表(symbol table)、關聯陣列(associative array)或對映(map),是一種用於儲存鍵值對的抽象資料結構。 

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

10.143.128.165:6379> SET msg "hello world"
OK

字典的定義

1 雜湊表

Redis 字典所使用的雜湊表由 dict.h/dictht 結構定義:

typedef struct dictht {  
   dictEntry **table; //雜湊表陣列  
   unsigned long size;//雜湊表大小   
   unsigned long sizemask;//雜湊表大小掩碼,用於計算索引值  
   unsigned long used;//該雜湊表已有節點的數量
}

一個空的字典的結構圖如下:

在結構中存有指向dictEntry 陣列的指標,而我們用來儲存資料的空間就是 dictEntry

 2. 雜湊表節點( dictEntry )

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

在資料結構中,key 是唯一的,但是存入裡面的key 並不是直接的字串,而是一個hash 值,透過hash 演算法,將字串轉換成對應的hash 值,然後在dictEntry 中找到對應的位置。

如果出現hash 值相同的情況,Redis 採用了 鏈地址法:

當k1 和k0 的hash 值相同時,將k1中的next 指向k0 形成一個連結串列。

3 字典

typedef struct dict {  
    dictType *type;  // 型別特定函式    
    void *privedata; // 私有資料   
    dictht  ht[2];  // 雜湊表  
    in trehashidx; // rehash 索引
}

type 屬性 和privdata 屬性是針對不同型別的鍵值對,為建立多型字典而設定的。

ht 屬性是一個包含兩個項(兩個雜湊表)的陣列

普通狀態下的字典:

解決雜湊衝突

在插入一條新的資料時,會進行雜湊值的計算,如果出現了hash值相同的情況,Redis 中採用了連地址法(separate chaining)來解決鍵衝突。每個雜湊表節點都有一個next 指標,多個雜湊表節點可以使用next 構成一個單向連結串列,被分配到同一個索引上的多個節點可以使用這個單向連結串列連線起來解決hash值衝突的問題。

 現在要插入k2,透過hash 演算法計算到k2 的hash 值為2,即需要將k2 插入到dictEntry[2]中:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946034/viewspace-2667355/,如需轉載,請註明出處,否則將追究法律責任。

相關文章