來自于《 Rdis 設(shè)計與實現(xiàn)》一書
Redis 沒有使用 C 語言傳統(tǒng)的字符串表示,而是構(gòu)建了一種名為簡單動態(tài)字符串(simple dynamic string, SDS)的抽象類型。我將其理解為一個 struct 結(jié)構(gòu)體。
struct sdshdr {
int len; # 記錄 buf 數(shù)組中已使用字節(jié)的數(shù)量
int free; #記錄 buf 數(shù)組中未使用字節(jié)的數(shù)量
char buf[]; # 字節(jié)數(shù)組,用于保存字符串
}
順便說一點,這與 Python 很像,因為 Python 的 len 就是直接在結(jié)構(gòu)體中去取這么一個變量。
為什么要使用這種結(jié)構(gòu)?
- C 語言的字符串在執(zhí)行拼接操作時,有可能出現(xiàn)緩沖區(qū)溢出危險行為。
- 如果需要進(jìn)行縮短字符串,那么有可能出現(xiàn)內(nèi)存泄漏,即忘記釋放字符串不需要的那部分空間。
通過未使用空間,SDS 實現(xiàn)了空間預(yù)分配和惰性空間釋放兩種優(yōu)化策略。
- 空間預(yù)分配
- 當(dāng)進(jìn)行字符串?dāng)U容時,有一個公式
如果修改之后 SDS 長度(即 len)小于 1 MB,那么程序間分配和 len 屬性同樣大小的未使用空間,即 len 屬性值和 free 屬性值相等。例如,進(jìn)行修改之后,SDS len 變?yōu)?10 字節(jié)后,那么 free 也會為 10 字節(jié)。buff 總長度為 10+10+1 字節(jié)。1字節(jié)用來保存空字符。
如果 SDS 長度大于等于 1 MB,那么程序會分配 1MB 的未使用空間。例如,如果修改之后 len 變?yōu)?10 MB,那么 free 會變?yōu)?1MB,buf 數(shù)組的實際長度為 10MB + 1MB + 1byte。
通過這種預(yù)分配策略,SDS 將連續(xù)增長N次字符串所需的內(nèi)存重分配次數(shù)從必定N次 降低為最多 N 次。
- 惰性空間釋放
惰性空間釋放用于優(yōu)化 SDS 的字符串縮短操作:當(dāng) SDS 的 API 需要縮短 SDS 保存的 字符串時,程序并不立即使用內(nèi)存重分配來回收縮短后多出來的字節(jié),而是使用free屬性 將這些字節(jié)的數(shù)量記錄起來,并等待將來使用。
通過惰性空間釋放策略,SDS避免了縮短字符串時所需的內(nèi)存重分配操作,并為將來可能有的增長操作提供了優(yōu)化。
但是它也有 API 在有需要時直接釋放這些未使用空間。
二進(jìn)制安全
我們知道 C 字符串以 \0 結(jié)尾,這就使得字符串里面不能包含空字符。而 Redis 的 SDS 的 API 都是二進(jìn)制安全的。它不會對數(shù)據(jù)做任何限制,過濾,換句話說,讀取的就是寫入的東西。所以 buf 稱為字節(jié)數(shù)組,用它來保存一系列二進(jìn)制數(shù)據(jù)。
這樣使得 Redis 不僅可以保存文本數(shù)據(jù),而且可以保存任意格式的二進(jìn)制數(shù)據(jù)。
兼容部分 C 字符串函數(shù)。SDS 的 API 遵循 C 字符串以空字符結(jié)尾的慣例。這是為了讓 SDS 用來保存文本數(shù)據(jù)時可以重用 <string.h> 庫定義的函數(shù)。比如對比兩個文本字符串
strcasecmp和strcat等
C 字符串和 SDS 之間的區(qū)別.
| C 字符串 | SDS |
|---|---|
| 獲取字符串長度復(fù)雜度 O(N) | O(1) |
| API 是不安全的,可能會造成緩沖區(qū)溢出 | 安全,不會造成溢出 |
| 修改字符串長度N次必然需要執(zhí)行 N 次內(nèi)存重分配 | 修改N次最多需要執(zhí)行 N 次內(nèi)存重分配 |
| 只能保存文本數(shù)據(jù) | 可以保存文本或者二進(jìn)制數(shù)據(jù) |
| 可以使用所有<string.h>庫中的函數(shù) | 可以使用一部分<string.h>庫中的函數(shù) |