【Redis源碼分析】一個對SDSHDR5是否使用的疑問

熊浩含redis

問題提出

  • 一、在Redis源碼中有一句註釋,是對sdshdr5的解釋:
/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};

那麼sdshdr5真的不使用了嗎bash

  • 二、在Redis5中,執行如下命令,key和value最終是用哪一種sds存放?

好比:函數

> set a ttt

sds基礎回顧

從Redis3.2開始,sds就有了5種類型,5種類型分別存放不一樣大小的字符串。在建立字符串時,sds會根據字符串的長度選擇不一樣的類型。最終由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;//爲空時強制用sdshdr8
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */

    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: {
            ...
        }
        case SDS_TYPE_32: {
            ...
        }
        case SDS_TYPE_64: {
            ...
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

除了建立空字符串時會強轉爲SDS_TYPE_8外,沒有什麼其它特別之處了。編碼

gdb結果

問題中的key和value都是長度短於32的字符串,彷佛應該都用sdshdr5來存。但gdb打印後發現,key確實是用sdshdr5存儲的,但value倒是用sdshdr8存儲的。
在getCommand函數處打斷點,打印c-db->dict中的相關內容:spa

分別打印key和val的值,其中key是sds,val是robj。結果以下:code

(gdb) p    (sds)0x7f09d2009830
$117 = 0x7f09d2009830 "\ba"
(gdb) p *(robj*)0x7f09d2029830
$118 = {type = 0, encoding = 8,    lru = 1536715, refcount = 1, ptr = 0x7f09d2029843}
(gdb) p    (sds)0x7f09d2029842
$119 = 0x7f09d2029842 "\001ttt"
  • ttt前的001,表明flags是00000001(二進制),低三位表類型,意味着存ttt所用的類型爲SDS_TYPE_8
  • a前的b,表明flags是00001000(二進制),低三位表類型,意味着存a所用類型爲SDS_TYPE_5
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

set命令流程

光看sdsnewlen沒法解釋問題,執行blog

>set a ttt

入口函數是setcommand,咱們從setcommand命令入口看起:rem

void setCommand(client *c) {
    ...
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

最終調setGenericCommand,c->argv[1],c->argv[2]是兩個robj,存放着key和value,打印結果以下:字符串

(gdb) p (sds)((*c->argv[1])->ptr-1)
$125 = 0x7f09d2029aca "\001a"
(gdb) p (sds)((*c->argv[2])->ptr-1)
$126 = 0x7f09d202988a "\001ttt"

能夠看到,__兩個robj底層的sds_type都是sdshdr8__。爲何是兩個sdshdr8呢?argv應該是在命令解析的時候生成的,繼續跟源碼。命令解析的源頭在readQueryFromClient,從readQueryFromClient一直往下跟,調用鏈以下:

最終走到了createStringObject:

robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)//OBJ_ENCODING_EMBSTR_SIZE_LIMIT = 44
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

redis在存儲命令參數時,根據參數長度選不一樣的結構。有意思的是,參數長度小於44時,走createEmbeddedStringObject分支,但createEmbeddedStringObject中又強制用sdshdr8來存字符串:

robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);//指定sdshdr8
    ...
    return o;
}

而當參數長度大於44時,走通常流程。此時建立的字符串長度既然大於44,更大於32了,天然也不可能用sdshdr5。換而言之,__從Buffer中解析出的命令參數,redis統一用大於sdshdr5的結構存,這跟以前gdb的現象是一致的__。
那何時key變回由sdshdr5存儲了呢?回過頭繼續跟setGenericCommand,調用鏈以下:

setGenericCommand-->setKey-->dbAdd

在dbAdd函數中,能夠看到,redis對待存入的key作了一次複製,__正是此次複製將key由以前的sdshdr8轉成了sdshdr5__:

void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr);
    int retval = dictAdd(db->dict, copy, val);
    ...
}

sdsdup複製只看字符串內容,根據字符串內容建立新的sds,因爲key->ptr指向的字符串是"a",故copy這個robj底層是個sdshdr5。最終調用dictAdd時,鍵的robj底層是sdshdr5,而值的robj底層是sdshdr8。

總結

最終能夠確認,長度小於32的鍵值對,鍵的底層是sdshdr5,而值的robj底層是sdshdr8。

  • Q1:爲何用sdshdr5存key能夠,存value不行?

    我的猜測是鍵不更新而值會更新,故鍵用盡量小的結構存;值更新會引發擴容,索性直接用大些的結構存。

  • Q2:爲何解析參數時,Redis又拋棄了小的sdshdr5?

    我的猜測是爲了編碼方便。不一樣命令的參數個數都不相同,一開始分不清哪一個位置是key哪一個位置是value,索性統一處理,在具體場景下,再單獨優化。

  • Q3:源碼裏面的註釋是否是錯了呢?

    筆者給Redis做者發了一封郵件去確認下,還未收到回信。

相關文章
相關標籤/搜索