Redis源碼學習(1)──字符串

redis 版本:5.0

本文代碼在Redis源碼中的位置:redis/src/sds.c、redis/src/sds.h

源碼整體結構

src:核心實現代碼,用 C 語言編寫

tests:單元測試代碼,用 Tcl 實現

deps:所有依賴庫

字符串存儲結構

Redis 將字符串的實現稱爲 sds(simple dynamic string)。爲了提高存儲空間的利用率,Redis 對不同長度的字符串,採用不同的數據結構。

以下是長度小於32的字符串的存儲結構:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags;
    char buf[];
};

其中,flags:高5位指示字符串真實長度,低3位指示數據類型;buf:柔性數組,指向字符串存儲地址。

:柔性數組是在 C99 及以上標準才支持,其目的是爲了在結構體中能夠“動態”地設定數組的長度。但需要注意的是,對結構體進行 sizeof 時,柔性數組的大小不被計算在內。因此在對結構體分配大小時,需要注意加上柔性數組的大小。柔性數組必須被聲明在結構體的最後,其起始地址與上一字段的末尾地址相連。

參考:https://en.wikipedia.org/wiki/Flexible_array_member

其他長度的字符串的存儲結構與其類似,結構分別如下:

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

其中,len 表示字符串實際長度;alloc 表示申請的字符數組的長度。

可以看出,Redis 最長能存儲長度大約爲 \(1.84* 10^{19}\)\(2^{64}-1\)) 的字符串。

:在聲明結構體時,struct __attribute__ ((__packed__)) 語法用來告訴編譯器採用緊湊的方式存儲數據,即1字節對齊,而不是默認的所有變量大小的最小公倍數做字節對齊。

sdshdr32 爲例,若採用1字節對齊,lenallocflags 一共佔9個字節(4+4+1),而默認會使用4字節對齊,這樣 flags 也會佔用4個字節,一共佔用12個字節。

採用 packed 的好處是明顯的,一來可以減少數據的大小,提高空間利用率;二來這爲地址計算帶來了方便:無論哪種類型(除 sdshdr5 外),使用 buff[-1] 即能獲取到 flag

字符串基本操作

創建

sdsnewlen 方法負責創建字符串,代碼如下:

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp;

    sh = s_malloc(hdrlen+initlen+1);
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

以上代碼比較簡單,但有幾點需要說明:

  • 從第5行代碼可以看出,對於空字符串,Redis 將其做爲 sdshdr8 類型而不是 sdshdr5 類型。其給出的原因是:用戶很有可能會在空字符串後追加新的字符串,因此用一個存儲長度適中的結構。
  • sh 指針指向對應結構體的起始地址,其長度爲結構體大小 + 待存儲字符串的大小 + 1。最後加1是爲了在末尾追加一個 \0 符。
  • s 指針即爲以上提到的柔性數組起始地址。fp 指針地址正好在 s 指針指向地址的前一字節,指向 flags
  • 第19行代碼中,initlen << SDS_TYPE_BITSinitlen 左移3位,正好印證之前提到的 flags 使用高5位存儲字符串長度。之後或上 type ,即把低3位置爲 type 的值(因爲之前左移3位後,低3位值均爲0,這樣或運算的值就是 type)。
  • SDS_HDR_VAR 宏定義爲 #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); 。其中的 ## 表示字符連接的意思。以23行代碼爲例,該行代碼在經編譯器預處理後,變爲 struct sdshdr8 *sh = (void*)((s)-(sizeof(struct sdshdr8))),語義爲sh = s - sizeof(sdshdr8)暫時還沒弄明白這裏爲啥要重算一下 sh。按理說第15行代碼 s = (char*)sh+hdrlen; 中,s 正是由 sh +sizeof(sdshdr8)得到。
  • 注意,最終返回給外部的是 s 指針而不是結構體指針。

其中的sdsReqType 方法用於根據待存儲字符串長度選擇合適的存儲類型,代碼如下:

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

LONG_MAX 表示 long 類型整數最大值,與編譯器有關。通常64位編譯器值爲 \(2^{63} - 1\),32位編譯器爲 \(2^{32}-1\)LLONG_MAX 表示 long long 類型整數最大值,通常32位和64位編譯器值均爲 \(2^{63} - 1\)

參考:

:static inline 關鍵字是在建議編譯器,以類似於宏定義的方式對待該函數,即在編譯階段,直接將該函數的相關指令插入到調用該函數的地方。這樣做可減少函數調用的開銷。

參考:https://zhuanlan.zhihu.com/p/132726037

sdsHdrSize 函數用於計算對應存儲類型的大小,代碼如下:

static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

其中,SDS_TYPE_MASK 的值爲7,二進制即爲:0000 0111type 和它做與運算,正好得出自己的第3位的值,即類型值。

刪除

刪除字符串的方法有兩種,一種是真正釋放了內存,代碼如下:

void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}

其中 s[-1] 之前也提到過,就是 flags 的值。而 s-sdsHdrSize(s[-1]) 則得到了 sdshdr 結構體的起始地址。

另一種則只是將長度置爲0,並沒有真正釋放內存,這麼做的目的當然是爲了下次存儲字符串時,無需重新申請內存,直接再用即可。

void sdsclear(sds s) {
    sdssetlen(s, 0);
    s[0] = '\0';
}

其中,sdssetlen 函數用來設置字符串長度,代碼如下:

static inline void sdssetlen(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len = newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len = newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len = newlen;
            break;
    }
}

追加(cat)

追加操作用於在一個字符串後面添加另一字符串,其代碼如下:

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

連接後,s 將指向連接後的字符串。

sdscatlen 函數邏輯也很簡單:擴容(若需要) -> 複製追加內容 -> 修改長度。代碼如下:

sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);
    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

其中 sdsMakeRoomFor 爲擴容函數,根據剩餘可用空間大小和待追加的字符串長度決定是否擴容。它保證了其返回的指針指向的內存區域,一定能容納追加的字符串(若內部的內存申請成功的話)。代碼如下:

sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

    type = sdsReqType(newlen);

    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);
    if (oldtype==type) {
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        newsh = s_malloc(hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    sdssetalloc(s, newlen);
    return s;
}

其基本邏輯爲:

  • 若剩餘容量足夠容納待追加的字符串,則無需擴容(第9行)。
  • 否則,算出追加後的字符串的長度 newlen(第13行)。第13行~第17行代碼還對 newlen 做了調整。暫時不知道爲啥要這麼做
  • 之後根據 newlen 決定使用什麼類型進行存儲。若類型還和之前一樣,則繼續沿用之前的內存結構,但需要使用 s_realloc 擴大之前的內存空間(第28行);否則則需要重新申請一塊新的內存(第34行),將之前的數據複製到新內存中(第36行)。

連接(join)

連接操作是將一個字符串數組的所有元素串聯在一起,若給定了分隔符,各元素之間還需插入分隔符。例如,若字符串數組 s = ["a", "b", "c"] ,分隔符 sep = ",",則 join(s, sep) 的結果爲:a,b,c。其代碼如下:

sds sdsjoin(char **argv, int argc, char *sep) {
    sds join = sdsempty();
    int j;

    for (j = 0; j < argc; j++) {
        join = sdscat(join, argv[j]);
        if (j != argc-1) join = sdscat(join,sep);
    }
    return join;
}

代碼很簡單,就不多做說明了。其中,sdsempty() 是構造一個空串,即:sdsnewlen("",0)

分割參數

sdssplitargs 函數用於將一條命令按參數分割成一個字符串數組,代碼如下:

sds *sdssplitargs(const char *line, int *argc) {
    const char *p = line;
    char *current = NULL;
    char **vector = NULL;

    *argc = 0;
    while(1) {
        /* skip blanks */
        while(*p && isspace(*p)) p++;
        if (*p) {
            /* get a token */
            int inq=0;  /* set to 1 if we are in "quotes" */
            int insq=0; /* set to 1 if we are in 'single quotes' */
            int done=0;

            if (current == NULL) current = sdsempty();
            while(!done) {
                if (inq) {
                    if (*p == '\\' && *(p+1) == 'x' &&
                                             is_hex_digit(*(p+2)) &&
                                             is_hex_digit(*(p+3)))
                    {
                        unsigned char byte;

                        byte = (hex_digit_to_int(*(p+2))*16)+
                                hex_digit_to_int(*(p+3));
                        current = sdscatlen(current,(char*)&byte,1);
                        p += 3;
                    } else if (*p == '\\' && *(p+1)) {
                        char c;

                        p++;
                        switch(*p) {
                        case 'n': c = '\n'; break;
                        case 'r': c = '\r'; break;
                        case 't': c = '\t'; break;
                        case 'b': c = '\b'; break;
                        case 'a': c = '\a'; break;
                        default: c = *p; break;
                        }
                        current = sdscatlen(current,&c,1);
                    } else if (*p == '"') {
                        /* closing quote must be followed by a space or
                         * nothing at all. */
                        if (*(p+1) && !isspace(*(p+1))) goto err;
                        done=1;
                    } else if (!*p) {
                        /* unterminated quotes */
                        goto err;
                    } else {
                        current = sdscatlen(current,p,1);
                    }
                } else if (insq) {
                    if (*p == '\\' && *(p+1) == '\'') {
                        p++;
                        current = sdscatlen(current,"'",1);
                    } else if (*p == '\'') {
                        /* closing quote must be followed by a space or
                         * nothing at all. */
                        if (*(p+1) && !isspace(*(p+1))) goto err;
                        done=1;
                    } else if (!*p) {
                        /* unterminated quotes */
                        goto err;
                    } else {
                        current = sdscatlen(current,p,1);
                    }
                } else {
                    switch(*p) {
                    case ' ':
                    case '\n':
                    case '\r':
                    case '\t':
                    case '\0':
                        done=1;
                        break;
                    case '"':
                        inq=1;
                        break;
                    case '\'':
                        insq=1;
                        break;
                    default:
                        current = sdscatlen(current,p,1);
                        break;
                    }
                }
                if (*p) p++;
            }
            /* add the token to the vector */
            vector = s_realloc(vector,((*argc)+1)*sizeof(char*));
            vector[*argc] = current;
            (*argc)++;
            current = NULL;
        } else {
            /* Even on empty input string return something not NULL. */
            if (vector == NULL) vector = s_malloc(sizeof(void*));
            return vector;
        }
    }

err:
    while((*argc)--)
        sdsfree(vector[*argc]);
    s_free(vector);
    if (current) sdsfree(current);
    *argc = 0;
    return NULL;
}

以上代碼總體上是在對字符串 line 逐個字符進行遍歷,指針 p 做爲遊標指向當前訪問的字符。

主體上,代碼由兩層 while 循環構成。外層 while 循環(第7行)條件總是成立,因此該函數要麼出錯結束(第102行~末尾),返回 NULL,要麼從第98行成功結束,此時的條件是 p 指針正好指向字符串末尾的 \0,返回分割出的參數數組 vector。而內層 while 循環執行結束後(第91行),會識別出一個參數,並會將該參數添加到 vector 數組中(第92行)。

再來看內層 while 循環是如何識別一個參數的。這段代碼(第17行到89行)總體結構是 if...else if ... else ...。進入前兩分支的條件是處於雙引號模式或單引號模式下,否則進入 else 分支。每輪循環結束,遊標 p 向後挪動一位(第88行)。

雙、單引號模式由 inqinsq 變量指示,前者表示當前訪問的字符在單引號內(即之前已訪問了起始單引號,但還未訪問到閉合單引號),後者則表示是在雙引號內。進入這兩個模式的時機當然是遇到了雙引號(第77行)或單引號(第80行)。

在非雙、單引號模式下,若遇到一般字符,都會將該字符追加到 current 字符串中(current 在遍歷開始是空串);若遇到 空格'\n''\r''\t''\0',內層循環都將結束,並將 current 做爲識別到的一個參數。

在雙、單引號模式下,若是遇到 "\n""\r""\t""\b""\a"(注意,這些都是兩個字符而不是一個),會將其做爲單字符 '\n''\r''\t''\b''\a' 加入 current 中(第41行);若字符串遍歷完成都沒遇到閉合的雙、單引號,則會報錯(第49行和第64行);閉合的雙、單引號後面不是空格(若字符串後面還有其他字符),也會報錯(第44行和第60行)。

另外,在雙引號模式下,會將格式類似於 "\x41"的子串,做爲16進制數,轉成字符類型後追加到 current 中(第19行~第28行)。該例中,"\x41" 將做爲字母 A

以上便是 Redis 字符串操作最主要的函數,還有一些比如字符串大小寫轉換,比較等函數實現均非常簡單,這裏不再贅述,有興趣可以去看看源碼。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章