1. 程式人生 > >Redis學習之SDS原理分析及源碼解析

Redis學習之SDS原理分析及源碼解析

容易 sig 副本 介紹 二進制安全 版本 技術分享 log 連續

一.SDS的簡單介紹

SDS:簡單動態字符串(simple dynamic string)

1)SDS是Redis默認的字符表示,比如包含字符串值的鍵值對都是在底層由SDS實現的

2)SDS用來保存數據庫中的字符串值

3)SDS被用作緩沖區:比如AOF模塊的AOF緩沖區,以及客戶端狀態中的輸入緩沖區

二.SDS的結構

struct sdshdr {

    // buf 中已占用空間的長度
    int len;

    // buf 中剩余可用空間的長度
    int free;

    // 字節數組
    char buf[];
};

技術分享圖片

分析:

1)free=5:代表空閑空間長度為5

2)len=5:代表已經使用的空間長度為5

三.Redis使用SDS的原因

1)常數復雜度獲取字符串長度:O(1)

C字符串獲取字符串長度時間復雜度為O(N),使用SDS可以確保獲取字符串長度的操作不會成為Redis的性能瓶頸

2)杜絕緩沖區溢出

C字符串不記錄自身長度和空閑空間,容易造成緩沖區溢出,使用SDS則不會,SDS拼接字符串之前會先通過free字段檢測剩余空間能否滿足需求,不能滿足需求的就會擴容

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

使用C字符串的話:

每次對一個C字符串進行增長或縮短操作,長度都需要對這個C字符串數組進行一次內存重分配,比如C字符串的拼接,程序要先進行內存重分配來擴展字符串數組的大小,避免緩沖區溢出,又比如C字符串的縮短操作,程序需要通過內存重分配來釋放不再使用的那部分空間,避免內存泄漏

使用SDS的話:

通過SDS的len屬性和free屬性可以實現兩種內存分配的優化策略:空間預分配和惰性空間釋放

1.針對內存分配的策略:空間預分配

在對SDS的空間進行擴展的時候,程序不僅會為SDS分配修改所必須的空間,還會為SDS分配額外的未使用的空間

這樣可以減少連續執行字符串增長操作所需的內存重分配次數,通過這種預分配的策略,SDS將連續增長N次字符串所需的內存重分配次數從必定N次降低為最多N次,這是個很大的性能提升!

2.針對內存釋放的策略:惰性空間釋放

在對SDS的字符串進行縮短操作的時候,程序並不會立刻使用內存重分配來回收縮短之後多出來的字節,而是使用free屬性將這些字節的數量記錄下來等待將來使用,通過惰性空間釋放策略,SDS避免了縮短字符串時所需的內存重分配次數,並且為將來可能有的增長操作提供了優化!

4)二進制安全

為了確保數據庫可以二進制數據(圖片,視頻等),SDS的API都是二進制安全的,所有的API都會以處理二進制的方式來處理存放在SDS的buf數組裏面的數據,程序不會對其中的數據做任何的限制,過濾,數據存進去是什麽樣子,讀出來就是什麽樣子,這也是buf數組叫做字節數組而不是叫字符數組的原因,以為它是用來保存一系列二進制數據的

通過二進制安全的SDS,Redis不僅可以保存文本數據,還可以保存任意格式是二進制數

四.SDS的主要API及其源碼解析

1)sdsnew函數:創建一個包含給定字符串的SDS

sds sdsnew(const char *init)

/*
 * 根據給定字符串 init ,創建一個包含同樣字符串的 sds
 *
 * 參數
 *  init :如果輸入為 NULL ,那麽創建一個空白 sds
 *         否則,新創建的 sds 中包含和 init 內容相同字符串
 *
 * 返回值
 *  sds :創建成功返回 sdshdr 相對應的 sds
 *        創建失敗返回 NULL
 *
 * 復雜度
 *  T = O(N)
 */
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}

/*
 * 根據給定的初始化字符串 init 和字符串長度 initlen
 * 創建一個新的 sds
 *
 * 參數
 *  init :初始化字符串指針
 *  initlen :初始化字符串的長度
 *
 * 返回值
 *  sds :創建成功返回 sdshdr 相對應的 sds
 *        創建失敗返回 NULL
 *
 * 復雜度
 *  T = O(N)
 */
sds sdsnewlen(const void *init, size_t initlen) {

    struct sdshdr *sh;

    // 根據是否有初始化內容,選擇適當的內存分配方式
    // T = O(N)
    if (init) {
        // zmalloc 不初始化所分配的內存
        sh = zmalloc(sizeof(struct sdshdr) + initlen + 1);
    }
    else {
        // zcalloc 將分配的內存全部初始化為 0
        sh = zcalloc(sizeof(struct sdshdr) + initlen + 1);
    }

    // 內存分配失敗,返回
    if (sh == NULL) return NULL;

    // 設置初始化長度
    sh->len = initlen;
    // 新 sds 不預留任何空間
    sh->free = 0;
    // 如果有指定初始化內容,將它們復制到 sdshdr 的 buf 中
    // T = O(N)
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    // 以 \0 結尾
    sh->buf[initlen] = \0;

    // 返回 buf 部分,而不是整個 sdshdr,因為sds是char指針類型的別名
    return (char*)sh->buf;
}

2)sdsempty函數:創建一個不包含任何內容的SDS

sds sdsempty(void)

/*
 * 創建並返回一個只保存了空字符串 "" 的 sds
 *
 * 返回值
 *  sds :創建成功返回 sdshdr 相對應的 sds
 *        創建失敗返回 NULL
 *
 * 復雜度
 *  T = O(1)
 */
sds sdsempty(void) {
    return sdsnewlen("", 0);
}

3)sdsfree函數:釋放給定的SDS

/*
 * 釋放給定的 sds
 *
 * 復雜度
 *  T = O(N)
 */
void sdsfree(sds s) {
    if (s == NULL) return;
    zfree(s - sizeof(struct sdshdr));
}

ps:zfree函數為內存管理模塊中的函數,我們在這裏先不探究,只需要知道它可以釋放指定的空間就可以了

分析:s - sizeof(struct sdshdr)到底返回的是什麽呢?

首先我們知道SDS的buf數組是柔性數組,也就是這個數組是不占據內存大小的,所以sizeof(struct sdshdr)為8

還有SDS數據類型為char *類型,所以假如存在這樣一條語句:

struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)))

其結構的內存結構圖為:

技術分享圖片

所以s-(sizeof(struct sdshdr))就是指向sds的頭部的!!

4)sdslen函數:返回SDS的已使用的空間字節數

/*
 * 返回 sds 已經使用的空間字節數
 *
 * T = O(1)
 */
static inline size_t sdslen(const sds s)
{
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
    return sh->len;
}

static inline修飾的函數是內聯函數,目的是解決函數在多次調用時候的效率問題!

size_t是無符號整數,是sizeof操作符返回的結構類型

const代表變量只能讀,不能被修改

5)sdsavail函數:返回SDS的未使用的空間字節數

/*
 * 返回 sds 為使用的空間字節數
 *
 * T = O(1)
 */
static inline size_t sdsavail(const sds s)
{
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
    return sh->free;
}

跟sdslen函數的區別就在於返回的屬性不同

6)sdsup函數:創建一個給定SDS的副本(copy)

/*
 * 復制給定 sds 的副本
 *
 * 返回值
 *  sds :創建成功返回輸入 sds 的副本
 *        創建失敗返回 NULL
 *
 * 復雜度
 *  T = O(N)
 */
sds sdsdup(const sds s) {
    return sdsnewlen(s, sdslen(s));
}

調用了sdsnewlen函數和sdslen函數,這兩個函數在上面都有介紹

7)sdsclear函數:清空SDS保存的字符串內容

/*
 * 在不釋放 SDS 的字符串空間的情況下,
 * 重置 SDS 所保存的字符串為空字符串。
 *
 * 復雜度
 *  T = O(1)
 */
void sdsclear(sds s) {

    // 取出 sdshdr
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));

    // 重新計算屬性
    sh->free += sh->len;
    sh->len = 0;

    // 將結束符放到最前面(相當於惰性地刪除 buf 中的內容)
    sh->buf[0] = \0;
}

以上采用了惰性空間釋放的策略,其實buf中的內容並沒有被“真正的刪除”,只是len屬性和free屬性變了,結束符移動到buf數組最前面了而已

8)sdscat函數:將給定的C字符串拼接到SDS字符串的末尾

/*
 * 將給定字符串 t 追加到 sds 的末尾
 *
 * 返回值
 *  sds :追加成功返回新 sds ,失敗返回 NULL
 *
 * 復雜度
 *  T = O(N)
 */
sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
}
/*
 * 將長度為 len 的字符串 t 追加到 sds 的字符串末尾
 *
 * 返回值
 *  sds :追加成功返回新 sds ,失敗返回 NULL
 *
 * 復雜度
 *  T = O(N)
 */
sds sdscatlen(sds s, const void *t, size_t len) {

    struct sdshdr *sh;

    // 原有字符串長度
    size_t curlen = sdslen(s);

    // 擴展 sds 空間
    // T = O(N)
    s = sdsMakeRoomFor(s, len);

    // 內存不足?直接返回
    if (s == NULL) return NULL;

    // 復制 t 中的內容到字符串後部
    // T = O(N)
    sh = (void*)(s - (sizeof(struct sdshdr)));
    memcpy(s + curlen, t, len);

    // 更新屬性
    sh->len = curlen + len;
    sh->free = sh->free - len;

    // 添加新結尾符號
    s[curlen + len] = \0;

    // 返回新 sds
    return s;
}
/*
 * 對 sds 中 buf 的長度進行擴展,確保在函數執行之後,
 * buf 至少會有 addlen + 1 長度的空余空間
 * (額外的 1 字節是為 \0 準備的)
 *
 * 返回值
 *  sds :擴展成功返回擴展後的 sds
 *        擴展失敗返回 NULL
 *
 * 復雜度
 *  T = O(N)
 */
sds sdsMakeRoomFor(sds s, size_t addlen) {

    struct sdshdr *sh, *newsh;

    // 獲取 s 目前的空余空間長度
    size_t free = sdsavail(s);

    size_t len, newlen;

    // s 目前的空余空間已經足夠,無須再進行擴展,直接返回
    if (free >= addlen) return s;

    // 獲取 s 目前已占用空間的長度
    len = sdslen(s);
    sh = (void*)(s - (sizeof(struct sdshdr)));

    // s 最少需要的長度
    newlen = (len + addlen);

    // 根據新長度,為 s 分配新空間所需的大小
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新長度小於 SDS_MAX_PREALLOC 最大預先分配長度
        // 那麽為它分配兩倍於所需長度的空間 空間預分配策略
        newlen *= 2;
    else
        // 否則,分配長度為目前長度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
    // T = O(N)
    newsh = zrealloc(sh, sizeof(struct sdshdr) + newlen + 1);

    // 內存不足,分配失敗,返回
    if (newsh == NULL) return NULL;

    // 更新 sds 的空余長度
    newsh->free = newlen - len;

    // 返回 sds
    return newsh->buf;
}

sdscat函數調用了sdscatlen函數,sdscatlen函數調用sdsMakeRoomFor函數

我們可以發現sdsMakeRoomFor函數采用了空間預分配的策略,確保在對buf進行長度擴展的時候至少為有addlen+1長度的空余空間,addlen為buf後面拼接的C字符串長度

sdsMakeRoomFor函數中有個宏定義的常量SDS_MAX_PREALLOC ,為最大預先分配長度,是為空間預分配策略服務的

sdsMakeRoomFor函數中調用了zrealloc函數,zrealloc函數的作用是分配指定內存大小空間給sds,此函數屬於內存分配模塊,我們先不探究

9)sdscatsds函數:將給定的SDS字符串拼接到另一個SDS字符串的末尾

/*
 * 將另一個 sds 追加到一個 sds 的末尾
 *
 * 返回值
 *  sds :追加成功返回新 sds ,失敗返回 NULL
 *
 * 復雜度
 *  T = O(N)
 */

sds sdscatsds(sds s, const sds t) {
    return sdscatlen(s, t, sdslen(t));
}

sdscatsds函數調用的兩個函數:sdscatlen和sdslen

已經在上面解析過了,這裏不再解析

10)sdscpy函數:將給定的C字符串復制到SDS裏面,覆蓋SDS原有的字符串

/*
 * 將字符串復制到 sds 當中,
 * 覆蓋原有的字符。
 *
 * 如果 sds 的長度少於字符串的長度,那麽擴展 sds 。
 *
 * 復雜度
 *  T = O(N)
 *
 * 返回值
 *  sds :復制成功返回新的 sds ,否則返回 NULL
 */

sds sdscpy(sds s, const char *t) {
    return sdscpylen(s, t, strlen(t));
}
/*
 * 將字符串 t 的前 len 個字符復制到 sds s 當中,
 * 並在字符串的最後添加終結符。
 *
 * 如果 sds 的長度少於 len 個字符,那麽擴展 sds
 *
 * 復雜度
 *  T = O(N)
 *
 * 返回值
 *  sds :復制成功返回新的 sds ,否則返回 NULL
 */

sds sdscpylen(sds s, const char *t, size_t len) {

    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));

    // sds 現有 buf 的長度
    size_t totlen = sh->free + sh->len;

    // 如果 s 的 buf 長度不滿足 len ,那麽擴展它
    if (totlen < len) {
        // T = O(N)
        s = sdsMakeRoomFor(s, len - sh->len);

        //擴展失敗,返回NULL
        if (s == NULL) return NULL;
        
        //擴展成功
        sh = (void*)(s - (sizeof(struct sdshdr)));
        totlen = sh->free + sh->len;
    }

    // 復制內容
    // T = O(N)
    memcpy(s, t, len);

    // 添加終結符號
    s[len] = \0;

    // 更新屬性
    sh->len = len;
    sh->free = totlen - len;

    // 返回新的 sds
    return s;
}

sdscpy函數調用了sdscpylen函數,sdscpylen函數又調用了sdsMakeRoomFor函數

代碼分析看註釋就好

11)sdsgrowzero函數:用空字符將SDS擴展至給定長度

/*
 * 將 sds 擴充至指定長度,未使用的空間以 0 字節填充。
 *
 * 返回值
 *  sds :擴充成功返回新 sds ,失敗返回 NULL
 *
 * 復雜度:
 *  T = O(N)
 */
sds sdsgrowzero(sds s, size_t len) {

    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));//*sh指向s的頭部

    size_t totlen, curlen = sh->len;//總長度 已經使用的長度

    // 如果 len 比字符串的現有長度小,
    // 那麽直接返回,不做動作
    if (len <= curlen) return s;

    // 擴展 sds
    // T = O(N)
    s = sdsMakeRoomFor(s, len - curlen);

    // 如果內存不足,直接返回
    if (s == NULL) return NULL;

    // 將新分配的空間用 0 填充,防止出現垃圾內容
    // T = O(N)
    sh = (void*)(s - (sizeof(struct sdshdr)));
    memset(s + curlen, 0, (len - curlen + 1)); // also set trailing \0 byte

    // 更新屬性
    totlen = sh->len + sh->free;
    sh->len = len;
    sh->free = totlen - sh->len;

    // 返回新的 sds
    return s;
}

沒有判斷len-curlen和free的關系,就算free>=len-curlen也會進行一次擴容操作

12.sdsrange函數:保留SDS給定區間內的數據,不在區間內的數據會被覆蓋或者清除

/*
 * 按索引對截取 sds 字符串的其中一段
 * start 和 end 都是閉區間(包含在內)
 *
 * 索引從 0 開始,最大為 sdslen(s) - 1
 * 索引可以是負數, sdslen(s) - 1 == -1
 *
 * 復雜度
 *  T = O(N)
 */
/*
* s = sdsnew("Hello World");
* sdsrange(s,1,-1); => "ello World"
*/
void sdsrange(sds s, int start, int end) {
    
    //sh指向s頭部
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
    
    size_t newlen, len = sdslen(s);

    //沒有可以截取的字符串,直接返回
    if (len == 0) return;
    
    //start參數規則
    if (start < 0) {
        start = len + start;
        if (start < 0) start = 0;
    }
    
    //end參數規則
    if (end < 0) {
        end = len + end;
        if (end < 0) end = 0;
    }
    
    //len取決於start和end的關系
    newlen = (start > end) ? 0 : (end - start) + 1;
    
    //新的sds的len!=0
    if (newlen != 0) {
        
        //需要截取的起點大於等於有符號的len 那麽新的sds的len=0
        if (start >= (signed)len) {
            newlen = 0;
        }
    
        //終點超出了有符號的len 終點就是len-1
        else if (end >= (signed)len) {
            end = len - 1;
            
            //重新計算len
            newlen = (start > end) ? 0 : (end - start) + 1;
        }
    }
    else {
        start = 0;
    }

    // 如果有需要,對字符串進行移動
    // T = O(N)
    if (start && newlen) memmove(sh->buf, sh->buf + start, newlen);

    // 添加終結符
    sh->buf[newlen] = 0;

    // 更新屬性
    sh->free = sh->free + (sh->len - newlen);
    sh->len = newlen;
}

註意start和end參數的設置,以及有時候需要對字符串進行移動操作

13.sdstrim函數:接受一個SDS和一個C字符串作為參數,從SDS左右兩端分別移除所有在C字符串中出現過的字符

/*
 * 對 sds 左右兩端進行修剪,清除其中 cset 指定的所有字符
 *
 * 在頭部遇到某個字符不屬於cset,則頭部清除停下
 * 在尾部遇到某個字符不屬於cset,則尾部清除停下
 *
 * 比如 sdsstrim(xxyayabcycyxy, "xy") 將返回 "ayabcyc"
 *
 * 復雜性:
 *  T = O(M*N),M 為 SDS 長度, N 為 cset 長度。
 * Example:
 *
 * s = sdsnew("AA...AA.a.aa.aHelloWorld     :::");
 * s = sdstrim(s,"A. :");
 * printf("%s\n", s);
 *
 * Output will be just "a.aa.aHello World".
 */
sds sdstrim(sds s, const char *cset) {
    struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
    char *start, *end, *sp, *ep;
    size_t len;

    // 設置和記錄指針
    sp = start = s;
    ep = end = s + sdslen(s) - 1;

    // 修剪, T = O(N^2)
    while (sp <= end && strchr(cset, *sp)) sp++; //從頭部開始清除,遇到第一個不屬於cset裏面的字符則停止頭部清除工作
    while (ep > start && strchr(cset, *ep)) ep--;//從尾部開始清除,遇到第一個不屬於cset裏面的字符則停止尾部清除工作

    // 計算 trim 完畢之後剩余的字符串長度
    len = (sp > ep) ? 0 : ((ep - sp) + 1);

    // 如果有需要,前移字符串內容
    // T = O(N)
    if (sh->buf != sp) memmove(sh->buf, sp, len);

    // 添加終結符
    sh->buf[len] = \0;

    // 更新屬性
    sh->free = sh->free + (sh->len - len);
    sh->len = len;

    // 返回修剪後的 sds
    return s;
}

註意sdstrim函數不是移除所有在子串中出現過的字符,而是移除頭部和尾部在子串中出現的字符

舉個例子:比如 sdsstrim(xxyayabcycyxy, "xy") 將返回 "ayabcyc"

14)sdscmp函數:比較兩個SDS字符串是否相等

/*
 * 對比兩個 sds , strcmp 的 sds 版本
 *
 * 返回值
 *  int :相等返回 0 ,s1 較大返回正數, s2 較大返回負數
 *
 * T = O(N)
 */
int sdscmp(const sds s1, const sds s2) {
    size_t l1, l2, minlen;
    int cmp;

    //s1長度
    l1 = sdslen(s1);
    
    //s2長度
    l2 = sdslen(s2);
    
    //s1和s2最小長度
    minlen = (l1 < l2) ? l1 : l2;
    
    //比較
    cmp = memcmp(s1, s2, minlen);

    //s1較大返回正數,s2較大返回負數,相等返回0
    if (cmp == 0) return l1 - l2;

    return cmp;
}

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

ovre!

Redis學習之SDS原理分析及源碼解析