Sds (Simple Dynamic String,簡單動態字符串)是 Redis 底層所使用的字符串表示, 幾乎全部的 Redis 模塊中都用了 sds。html
本章將對 sds 的實現、性能和功能等方面進行介紹, 並說明 Redis 使用 sds 而不是傳統 C 字符串的緣由。python
Sds 在 Redis 中的主要做用有如下兩個:redis
char*
類型的替代品;如下兩個小節分別對這兩種用途進行介紹。算法
Redis 是一個鍵值對數據庫(key-value DB), 數據庫的值能夠是字符串、集合、列表等多種類型的對象, 而數據庫的鍵則老是字符串對象。sql
對於那些包含字符串值的字符串對象來講, 每一個字符串對象都包含一個 sds 值。數據庫
「包含字符串值的字符串對象」,這種說法初聽上去可能會有點奇怪, 可是在 Redis 中, 一個字符串對象除了能夠保存字符串值以外, 還能夠保存 long
類型的值, 因此爲了嚴謹起見, 這裏須要強調一下: 當字符串對象保存的是字符串時, 它包含的纔是 sds 值, 不然的話, 它就是一個 long
類型的值。api
舉個例子, 如下命令建立了一個新的數據庫鍵值對, 這個鍵值對的鍵和值都是字符串對象, 它們都包含一個 sds 值:數組
redis> SET book "Mastering C++ in 21 days" OK redis> GET book "Mastering C++ in 21 days"
如下命令建立了另外一個鍵值對, 它的鍵是字符串對象, 而值則是一個集合對象:緩存
redis> SADD nosql "Redis" "MongoDB" "Neo4j" (integer) 3 redis> SMEMBERS nosql 1) "Neo4j" 2) "Redis" 3) "MongoDB"
由於 char*
類型的功能單一, 抽象層次低, 而且不能高效地支持一些 Redis 經常使用的操做(好比追加操做和長度計算操做), 因此在 Redis 程序內部, 絕大部分狀況下都會使用 sds 而不是 char*
來表示字符串。安全
性能問題在稍後介紹 sds 定義的時候就會說到, 由於咱們尚未了解過 Redis 的其餘功能模塊, 因此也沒辦法詳細地舉例說那裏用到了 sds , 不過在後面的章節中, 咱們會常常看到其餘模塊(幾乎每個)都用到了 sds 類型值。
目前來講, 只要記住這個事實便可: 在 Redis 中, 客戶端傳入服務器的協議內容、 aof 緩存、 返回給客戶端的回覆, 等等, 這些重要的內容都是由 sds 類型來保存的。
在 C 語言中,字符串能夠用一個 \0
結尾的 char
數組來表示。
好比說, hello world
在 C 語言中就能夠表示爲 "hello world\0"
。
這種簡單的字符串表示,在大多數狀況下都能知足要求,可是,它並不能高效地支持長度計算和追加(append)這兩種操做:
strlen(s)
)的複雜度爲 θ(N)θ(N) 。realloc
)。在 Redis 內部, 字符串的追加和長度計算很常見, 而 APPEND 和 STRLEN 更是這兩種操做,在 Redis 命令中的直接映射, 這兩個簡單的操做不該該成爲性能的瓶頸。
另外, Redis 除了處理 C 字符串以外, 還須要處理單純的字節數組, 以及服務器協議等內容, 因此爲了方便起見, Redis 的字符串表示還應該是二進制安全的: 程序不該對字符串裏面保存的數據作任何假設, 數據能夠是以 \0
結尾的 C 字符串, 也能夠是單純的字節數組, 或者其餘格式的數據。
考慮到這兩個緣由, Redis 使用 sds 類型替換了 C 語言的默認字符串表示: sds 既可高效地實現追加和長度計算, 同時是二進制安全的。
在前面的內容中, 咱們一直將 sds 做爲一種抽象數據結構來講明, 實際上, 它的實現由如下兩部分組成:
typedef char *sds; struct sdshdr { // buf 已佔用長度 int len; // buf 剩餘可用長度 int free; // 實際保存字符串數據的地方 char buf[]; };
其中,類型 sds
是 char *
的別名(alias),而結構 sdshdr
則保存了 len
、 free
和 buf
三個屬性。
做爲例子,如下是新建立的,一樣保存 hello world
字符串的 sdshdr
結構:
struct sdshdr { len = 11; free = 0; buf = "hello world\0"; // buf 的實際長度爲 len + 1 };
經過 len
屬性, sdshdr
能夠實現複雜度爲 θ(1)θ(1) 的長度計算操做。
另外一方面, 經過對 buf
分配一些額外的空間, 並使用 free
記錄未使用空間的大小, sdshdr
可讓執行追加操做所需的內存重分配次數大大減小, 下一節咱們就會來詳細討論這一點。
固然, sds 也對操做的正確實現提出了要求 —— 全部處理 sdshdr
的函數,都必須正確地更新 len
和 free
屬性,不然就會形成 bug 。
在前面說到過,利用 sdshdr
結構,除了能夠用 θ(1)θ(1) 複雜度獲取字符串的長度以外,還能夠減小追加(append)操做所需的內存重分配次數,如下就來詳細解釋這個優化的原理。
爲了易於理解,咱們用一個 Redis 執行實例做爲例子,解釋一下,當執行如下代碼時, Redis 內部發生了什麼:
redis> SET msg "hello world" OK redis> APPEND msg " again!" (integer) 18 redis> GET msg "hello world again!"
首先, SET
命令建立並保存 hello world
到一個 sdshdr
中,這個 sdshdr
的值以下:
struct sdshdr { len = 11; free = 0; buf = "hello world\0"; }
當執行 APPEND 命令時,相應的 sdshdr
被更新,字符串 " again!"
會被追加到原來的 "hello world"
以後:
struct sdshdr { len = 18; free = 18; buf = "hello world again!\0 "; // 空白的地方爲預分配空間,共 18 + 18 + 1 個字節 }
注意, 當調用 SET
命令建立 sdshdr
時, sdshdr
的 free
屬性爲 0
, Redis 也沒有爲 buf
建立額外的空間 —— 而在執行 APPEND 以後, Redis 爲 buf
建立了多於所需空間一倍的大小。
在這個例子中, 保存 "hello world again!"
共須要 18 + 1
個字節, 但程序卻爲咱們分配了 18 + 18 + 1 = 37
個字節 —— 這樣一來, 若是未來再次對同一個 sdshdr
進行追加操做, 只要追加內容的長度不超過 free
屬性的值, 那麼就不須要對 buf
進行內存重分配。
好比說, 執行如下命令並不會引發 buf
的內存重分配, 由於新追加的字符串長度小於 18
:
redis> APPEND msg " again!" (integer) 25
再次執行 APPEND 命令以後, msg
的值所對應的 sdshdr
結構能夠表示以下:
struct sdshdr { len = 25; free = 11; buf = "hello world again! again!\0 "; // 空白的地方爲預分配空間,共 18 + 18 + 1 個字節 }
sds.c/sdsMakeRoomFor
函數描述了 sdshdr
的這種內存預分配優化策略, 如下是這個函數的僞代碼版本:
def sdsMakeRoomFor(sdshdr, required_len): # 預分配空間足夠,無須再進行空間分配 if (sdshdr.free >= required_len): return sdshdr # 計算新字符串的總長度 newlen = sdshdr.len + required_len # 若是新字符串的總長度小於 SDS_MAX_PREALLOC # 那麼爲字符串分配 2 倍於所需長度的空間 # 不然就分配所需長度加上 SDS_MAX_PREALLOC 數量的空間 if newlen < SDS_MAX_PREALLOC: newlen *= 2 else: newlen += SDS_MAX_PREALLOC # 分配內存 newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1) # 更新 free 屬性 newsh.free = newlen - sdshdr.len # 返回 return newsh
在目前版本的 Redis 中, SDS_MAX_PREALLOC
的值爲 1024 * 1024
, 也就是說, 當大小小於 1MB
的字符串執行追加操做時,sdsMakeRoomFor
就爲它們分配多於所需大小一倍的空間; 當字符串的大小大於 1MB
, 那麼 sdsMakeRoomFor
就爲它們額外多分配 1MB
的空間。
sds 模塊基於 sds
類型和 sdshdr
結構提供瞭如下 API :
函數 | 做用 | 算法複雜度 |
---|---|---|
sdsnewlen |
建立一個指定長度的 sds ,接受一個 C 字符串做爲初始化值 |
O(N)O(N) |
sdsempty |
建立一個只包含空白字符串 "" 的 sds |
O(1)O(1) |
sdsnew |
根據給定 C 字符串,建立一個相應的 sds |
O(N)O(N) |
sdsdup |
複製給定 sds |
O(N)O(N) |
sdsfree |
釋放給定 sds |
O(N)O(N) |
sdsupdatelen |
更新給定 sds 所對應 sdshdr 結構的 free 和 len |
O(N)O(N) |
sdsclear |
清除給定 sds 的內容,將它初始化爲 "" |
O(1)O(1) |
sdsMakeRoomFor |
對 sds 所對應 sdshdr 結構的 buf 進行擴展 |
O(N)O(N) |
sdsRemoveFreeSpace |
在不改動 buf 的狀況下,將 buf 內多餘的空間釋放出去 |
O(N)O(N) |
sdsAllocSize |
計算給定 sds 的 buf 所佔用的內存總數 |
O(1)O(1) |
sdsIncrLen |
對 sds 的 buf 的右端進行擴展(expand)或修剪(trim) |
O(1)O(1) |
sdsgrowzero |
將給定 sds 的 buf 擴展至指定長度,無內容的部分用 \0 來填充 |
O(N)O(N) |
sdscatlen |
按給定長度對 sds 進行擴展,並將一個 C 字符串追加到 sds 的末尾 |
O(N)O(N) |
sdscat |
將一個 C 字符串追加到 sds 末尾 |
O(N)O(N) |
sdscatsds |
將一個 sds 追加到另外一個 sds 末尾 |
O(N)O(N) |
sdscpylen |
將一個 C 字符串的部份內容複製到另外一個 sds 中,須要時對 sds 進行擴展 |
O(N)O(N) |
sdscpy |
將一個 C 字符串複製到 sds |
O(N)O(N) |
sds
還有另外一部分功能性函數, 好比 sdstolower
、 sdstrim
、 sdscmp
, 等等, 基本都是標準 C 字符串庫函數的 sds
版本, 這裏不一一列舉了。
sds
,而不是 C 字符串(以 \0
結尾的 char*
)。sds
有如下特性:
strlen
);append
);sds
會爲追加操做進行優化:加快追加操做的速度,並下降內存分配的次數,代價是多佔用了一些內存,並且這些內存不會被主動釋放。