熊浩含redis
/* 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
好比:函數
> set a ttt
從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外,沒有什麼其它特別之處了。編碼
問題中的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"
#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
光看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。
我的猜測是鍵不更新而值會更新,故鍵用盡量小的結構存;值更新會引發擴容,索性直接用大些的結構存。
我的猜測是爲了編碼方便。不一樣命令的參數個數都不相同,一開始分不清哪一個位置是key哪一個位置是value,索性統一處理,在具體場景下,再單獨優化。
筆者給Redis做者發了一封郵件去確認下,還未收到回信。