Redis 數據結構——SDS

簡單動態字符串(sds)

定義

Redis沒有直接使用C語言傳統的字符串表示(以空字符結尾的字符數組),而是自己構建了一種名爲簡單動態字符串(simple dynamic string,SDS)的抽象類型,並將SDS用作Redis的默認字符串表示。

struct sdshdr {
    //等於SDS所保存字符串的長度
    int len;
    //記錄buf數組中未使用字節的數量
    int free;
    //字節數組,用於保存字符串
    char buf[];
};

 

  • free屬性的值爲0,表示這個SDS沒有分配任何未使用空間。

  • len屬性的值爲5,表示這個SDS保存了一個五字節長的字符串。

  • buf屬性是一個char類型的數組,數組的前五個字節分別保存了'R'、'e'、'd'、'i'、's'五個 字符,而最後一個字節則保存了空字符'\0'。

SDS遵循C字符串以空字符結尾的慣例,保存空字符的1字節空間不計算在SDS的len屬性裏面,並且爲空字符分配額外的1字節空間,以及添加空字符到字符串末尾等操作,都是由SDS函數自動完成的,所以這個空字符對於SDS的使用者來說是完全透明的。遵循空字符結尾這一慣例的好處是,SDS可以直接重用一部分C字符串函數庫裏面的函數。

SDS於C字符串的區別

常數複雜度獲取字符串長度

因爲C字符串並不記錄自身的長度信息,所以爲了獲取一個C字符串的長度,程序必須遍歷整個字符串,對遇到的每個字符進行計數,直到遇到代表字符串結尾的空字符爲止,這個操作的複雜度爲O(N)。

和C字符串不同,因爲SDS在len屬性中記錄了SDS本身的長度,所以獲取一個SDS長度的複雜度僅爲O(1)。

設置和更新SDS長度的工作是由SDS的API在執行時自動完成的,使用SDS無須進行任何手動修改長度的工作。

杜絕緩衝區溢出

C字符串不記錄自身長度帶來的一個問題是容易造成緩衝區溢出(buffer overflow)。

兩個字符串物理相鄰存儲,第一個字符串內容修改長度比之前長,就發生了緩衝區溢出,導致第二個字符串出錯。

SDS的空間分配策略完全杜絕了發生緩衝區溢出的可能性:當SDS API需要對SDS進行修改時,API會先檢查SDS的空間是否滿足修改所需的要求,如果不滿足的話,API會自動將SDS的空間擴展至執行修改所需的大小,然後才執行實際的修改操作,所以使用SDS既不需要手動修改SDS的空間大小,也不會出現前面所說的緩衝區溢出問題。

減少修改字符串時帶來的內存重分配次數

SDS通過未使用空間解除了字符串長度和底層數組長度之間的關聯:在SDS中,buf數組的長度不一定就是字符數量加一,數組裏面可以包含未使用的字節,而這些字節的數量就由SDS的free屬性記錄。

空間預分配

空間預分配用於優化SDS的字符串增長操作:當SDS的API對一個SDS進行修改,並且需要對SDS進行空間擴展的時候,程序不僅會爲SDS分配修改所必須要的空間,還會爲SDS分配額外的未使用空間。

  • 如果對SDS進行修改之後,SDS的長度(也即是len屬性的值)將小於1MB,那麼程序分 配和len屬性同樣大小的未使用空間,這時SDS len屬性的值將和free屬性的值相同。舉個例 子,如果進行修改之後,SDS的len將變成13字節,那麼程序也會分配13字節的未使用空間, SDS的buf數組的實際長度將變成13+13+1=27字節(額外的一字節用於保存空字符)。

  • 如果對SDS進行修改之後,SDS的長度將大於等於1MB,那麼程序會分配1MB的未使用 空間。舉個例子,如果進行修改之後,SDS的len將變成30MB,那麼程序會分配1MB的未使 用空間,SDS的buf數組的實際長度將爲30MB+1MB+1byte。

惰性空間釋放

惰性空間釋放用於優化SDS的字符串縮短操作:當SDS的API需要縮短SDS保存的字符串時,程序並不立即使用內存重分配來回收縮短後多出來的字節,而是使用free屬性將這些字節的數量記錄起來,並等待將來使用。

通過惰性空間釋放策略,SDS避免了縮短字符串時所需的內存重分配操作,併爲將來可能有的增長操作提供了優化。

二進制安全

C字符串中的字符必須符合某種編碼(比如ASCII),並且除了字符串的末尾之外,字符串裏面不能包含空字符,否則最先被程序讀入的空字符將被誤認爲是字符串結尾,這些限制使得C字符串只能保存文本數據,而不能保存像圖片、音頻、視頻、壓縮文件這樣的二進制數據。

爲了確保Redis可以適用於各種不同的使用場景,SDS的API都是二進制安全的 (binary-safe),所有SDS API都會以處理二進制的方式來處理SDS存放在buf數組裏的數據,程序不會對其中的數據做任何限制、過濾、或者假設,數據在寫入時是什麼樣的,它被讀取時就是什麼樣。

Redis不是用數組來保存字符,而是用一系列二進制數據來保存。

兼容部分C字符串函數

雖然SDS的API都是二進制安全的,但它們一樣遵循C字符串以空字符結尾的慣例:這些API總會將SDS保存的數據的末尾設置爲空字符,並且總會在爲buf數組分配空間時多分配一個字節來容納這個空字符,這是爲了讓那些保存文本數據的SDS可以重用一部分<string.h>庫定義的函數。

strcasecmp(sds->buf, "hello world");
strcat(c_string, sds->buf);

總結

C字符串 SDS
獲取字符串長度的複雜度O(N) 獲取字符串長度的複雜度O(1)
API是不安全的,可能造成緩衝區溢出 API是安全的,不會造成緩衝區溢出
修改字符串長度N次必然需要執行N次內存重分配 修改字符串長度N次最多需要執行N次內存重分配
只能保存文本數據 可以保存文本或者二進制數據
可以使用所有<string.h>庫函數 可以部分所有<string.h>庫函數

SDS API

函數 作用 時間複雜度
sdsnew 創建一個包含給定 C 字符串的 SDS 。 O(N)N 爲給定 C 字符串的長度。
sdsempty 創建一個不包含任何內容的空 SDS 。 O(1)
sdsfree 釋放給定的 SDS 。 O(1)
sdslen 返回 SDS 的已使用空間字節數。 這個值可以通過讀取 SDS 的 len 屬性來直接獲得, 複雜度爲
sdsavail 返回 SDS 的未使用空間字節數。 這個值可以通過讀取 SDS 的 free 屬性來直接獲得, 複雜度爲 O(1)
sdsdup 創建一個給定 SDS 的副本(copy)。 O(N)N 爲給定 SDS 的長度。
sdsclear 清空 SDS 保存的字符串內容。 因爲惰性空間釋放策略,複雜度爲 O(1)
sdscat 將給定 C 字符串拼接到 SDS 字符串的末尾。 O(N)N 爲被拼接 C 字符串的長度。
sdscatsds 將給定 SDS 字符串拼接到另一個 SDS 字符串的末尾。 O(N)N 爲被拼接 SDS 字符串的長度。
sdscpy 將給定的 C 字符串複製到 SDS 裏面, 覆蓋 SDS 原有的字符串。 O(N)N 爲被複制 C 字符串的長度。
sdsgrowzero 用空字符將 SDS 擴展至給定長度。 O(N)N 爲擴展新增的字節數。
sdsrange 保留 SDS 給定區間內的數據, 不在區間內的數據會被覆蓋或清除。 O(N)N 爲被保留數據的字節數。
sdstrim 接受一個 SDS 和一個 C 字符串作爲參數, 從 SDS 左右兩端分別移除所有在 C 字符串中出現過的字符。 O(M * N)M 爲 SDS 的長度, N 爲給定 C 字符串的長度。
sdscmp 對比兩個 SDS 字符串是否相同。 O(N)N 爲兩個 SDS 中較短的那個 SDS 的長度。

比起C字符串,SDS具有以下優點:

1)常數複雜度獲取字符串長度。

2)杜絕緩衝區溢出。

3)減少修改字符串長度時所需的內存重分配次數。

4)二進制安全。

5)兼容部分C字符串函數。

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