簡單動態字符串(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字符串函數。