【Redis基本數據結構】動態字符串實現


Redis 不直接使用原始 C 字符串,而是本身構建了一種字符串類型,叫作 SDS(simple dynamic string), 並將 SDS 做爲 Redis 的默認字符串表示.git

SDS 的定義

看一下 SDS 的定義:github

// file : sds.h
struct sdshdr {
    // 字符串當前長度,
    //等於 buf 數組中已使用字節數
    unsigned int len;
    // 剩餘可用長度
    unsigned int free;
    // 字符數組(具體存放字符串的地方)
    char buf[];
};

SDS buf 數組仍是遵循 C 字符串以空字符結尾的慣例,這樣能夠直接重用一部分 C 字符串函數庫裏函數算法

SDS 與 C 字符串的區別

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

由於 C 字符串並不記錄自身的長度信息,因此獲取一個 C 字符串的長度,必須遍歷整個字符串,這個操做的的複雜度爲 $O(N)$.
SDS 在 len 屬性中記錄了 SDS 的 len 屬性,獲取一個 SDS 長度的複雜度僅爲$O(1)$數據庫

杜絕緩衝區溢出

除了獲取字符串長度的複雜度高以外, C 字符串不記錄自身長度帶來的另外一個問題是容易形成緩衝區溢出.
看看字符串拼接函數 strcat 的調用:數組

char *strcat( char *dest, const char *src );

在執行 strcat 時假定 dest 分配了足夠多的內存,能夠容納 src 字符串中的內容,而一旦這個假定不成立,就會產生緩衝區溢出.安全

舉個例子,假設程序中有兩個在內存中緊鄰着的 C 字符串 s1 和 s2,執行 strcat(s1, s2). 如圖所示:函數

此處輸入圖片的描述

若是在執行以前忘了給 s1 分配足夠的空間,在執行以後, s1的數據將溢出到 s2 的空間之中,致使 s2 的內容被意外更改.性能

與 C 字符串不一樣, SDS 的 空間分配徹底杜絕了發生緩衝區溢出的可能性,當 Redis 須要對 SDS 進行修改時,API 會首先檢查 SDS 的空間是否知足修改的要求,若是不知足, API 會自動將 SDS 空間擴展至執行修改所需的大小優化

舉個例子, Redis 裏也有一個執行拼接的函數: sdscat, 假如執行 sdscat(s," WORLD");, 若是檢查發現 s 的空間不足以拼接, sdscat 會先擴展s 的空間,在執行拼接.
拼接操做先後如圖所示:編碼

此處輸入圖片的描述

注意上圖中的 SDS, sdscat 不只進行了拼接操做,還額外分配了11 字節的未使用空間,剛好等於拼接以後的字符串長度, 這並非巧合,它是 SDS 的空間分配策略,下面會講到

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

對於 C 字符串,每次增加或 縮短一個 C 字符串,程序總要多保存這個 C 字符串的數組進行一次內存重分配操做:

  • 若是執行的增加字符串的操做,在執行以前,程序要先經過內存重分配來擴展空間的大小,若是忘了可能會產生緩衝區溢出

  • 若是執行的是縮短字符串的操做,在執行以後,程序要經過內存重分配來釋放再也不使用的部分空間,若是忘了,可能會產生內存泄露

內存重分配涉及到複雜的算法,而且可能須要執行系統調用.

  • 在通常程序中,若是修改字符串長度的狀況不太常出現,那麼每次修改都執行一次內存重分配是能夠接受的

  • 可是 Redis 做爲數據庫,常常用於對於速度要求嚴苛,數據被頻繁修改的場合,若是每次修改都執行一次內存重分配的話,光是執行內存重分配就會佔去修改字符串所用時間的一大部分,若果修改頻繁發生,可能會對系統性能形成影響

爲了不 C 字符串的這種缺陷, SDS 使用了未使用空間這一律念, 在 SDS 中, buf 數組的長度不必定是字符數量加上一個結束符,數組裏面還包含未使用的字節,這些字節的數量由 SDS 的 free 屬性記錄.

經過未使用空間, SDS 實現了空間預分配和惰性空間釋放兩種優化策略.

空間預分配
空間預分配用於優化 SDS 的字符串增加操做: 當 SDS 的 API 對一個 SDS 進行修改,而且須要空間擴展時, 程序不只會爲 SDS 分配修改所需的空間,還會爲 SDS 分配額外的未使用空間.

額外的未使用空間分配規則以下:

  • 若是對 SDS 修改後,字符串的長度( len 的值) 小於 1 MB, 那麼程序分配和 len 同樣大小的未使用空間,此時 free=len.
    在上節實例中, s="HELLO" + " WORLD", len = 11,程序會分配11字節的未使用空間, SDS的 buf 數組的實際長度變成 11+11+1 = 23 字節(額外的一字節保存空字符)

  • 若是對 SDS 修改後, 字符串的長度大於 1 MB, 那麼程序會分配1 MB 的未使用空間.舉個例子,若是進行修改以後, SDS 的len 變成 10 MB, 那麼程序會分配 1MB 的未使用空間(free=1 MB),SDS 的 buf數組實際長度將變爲 10 MB + 1 MB + 1 byte.

經過預分配策略, Redis
能夠減小連續執行字符串增加操做所需的內存重分配次數.

惰性空間釋放

惰性空間釋放用於優化SDS 的字符串縮短操做: 當須要縮短 SDS 保存的字符串時,程序並不當即回收縮短以後多出來的字節,而是使用 free 屬性將這些字節的數量記錄起來,等待未來使用.

舉個例子, sdstrim 函數接受一個 SDS 和一個 C 字符串,從 SDS 字符串兩側刪除在 C 中出現的字符,對於某個SDS中 buf保存着 `s = "xxabcyy", 執行

sdsstrim(s, "xy");

再執行:

sdscat(s, "Redis");

SDS 結構變化以下圖:
此處輸入圖片的描述

注意執行 sdstrim 以後 SDS 並無釋放多出來的 6 字節,而是將這6 字節做爲未使用空間保留在了 SDS 裏面,在以後的 sdscat 操做中沒必要爲了拼接從新分配空間.

SDS 也提供了相應的 API, 在有須要時,真正地釋放 SDS 未使用空間,不用擔憂惰性空間策略形成的空間浪費.

二進制安全

C 字符串中的字符必須符合某種編碼 ,除了末尾以外,其餘位置不能包含空字符,不然最早被讀入的空字符會被誤認爲字符串結束標誌,這些限制使得 C 字符串只能保存文本數據, 而不能保存像圖片,音頻,視頻這樣的二進制文件.

維基百科的Null-terminated string 詞條給出了空字符結尾字符串的定義,說明了這種表示的來源)

數據庫保存二進制數據的場景並很多見,爲了確保 Redis 可使用各類不一樣的場景, SDS 中的 API 都是二進制安全的,全部的 SDS API 會以處理二進制的方式來處理 buf 中數據,程序不會對其中的數據作任何假設,數據在寫入時什麼,在讀出時就是什麼樣.

個人博客: http://ygmyth.github.io

相關文章
相關標籤/搜索