最近在看《Redis的设计与实现》,感觉书还不错,但由于书中讲解的Redis版本较低,有些地方与现版本有出入,所以通过简单浏览源码,加深理解。本文代码使用Redis6.0
SDS,全称Simple Dynamic String,作为Redis中的字符串表示,主要作用于Redis中字符串对象StringObject的底层实现,以及Redis内部作为char*类型的替代品。具体sds源码实现在sds.c和sds.h文件中。
数据结构 查看sds.h文件,不难发现对sds的类型定义,其实就是char *的一个alias。
尽管C语言字符串本质上也是char *,不过与之不同的是,一个sds的完整结构,分为两块部分,sdshdr结构和字符数组。以下为sdshdr结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 struct __attribute__ ((__packed__ )) sdshdr5 { unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr8 { uint8_t len; uint8_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr16 { uint16_t len; uint16_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr32 { uint32_t len; uint32_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr64 { uint64_t len; uint64_t alloc; unsigned char flags; char buf[]; }; #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
sds针对不同的字符串长度,为了节省内存,定义了五种类型的header,其中sdshdr5较为特殊,与其他header结构不同,它并没有alloc和len字段,所以flags字段被分为两部分,低三位作为header类型,高五位记录字符串长度。因为不具备alloc字段,所以sdshdr5不会预分配空间,如果字符串动态增加,则需要重新分配内存,大部分场景不太会使用sdshdr5。其余4个header的结构类似,字段含义如下
len: 字符串真实长度
alloc:字符串当前可使用的最大容量
flags:占一字节,前三位表示header类型
其中结构体中,有几个特殊的点
__attribute__ ((__packed__))
表示编译器不需要对结构体进行内存对齐,从而使字段内存连续
char buf[]
,未指定长度且定义在结构体的最后一个字段,是一个柔性数组,表示flags字段后是一个字符数组,而柔性数组buf[]只作为一个符号地址存在,通过sizeof 返回的结构大小不包括柔性数组的内存大小
对内存对齐与柔性数组的详细解释:内存对齐 与柔性数组
通过源码中定义的宏以及sdslen方法不难看出这两点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define SDS_TYPE_MASK 7 #define SDS_TYPE_BITS 3 #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) static inline size_t sdslen (const sds s) { unsigned char flags = s[-1 ]; switch (flags&SDS_TYPE_MASK) { case SDS_TYPE_5: return SDS_TYPE_5_LEN(flags); case SDS_TYPE_8: return SDS_HDR(8 ,s)->len; case SDS_TYPE_16: return SDS_HDR(16 ,s)->len; case SDS_TYPE_32: return SDS_HDR(32 ,s)->len; case SDS_TYPE_64: return SDS_HDR(64 ,s)->len; } return 0 ; }
”##“ 连接符,预处理运算符,例如SDS_HDR(8,s)宏展开后是((struct sdshdr8 *)((s)-(sizeof(struct sdshdr8))))
其中因为buf[]是柔性数组,sizeof(struct sdshdr8)不会包含数组大小,SDS_HDR即表示字符串向前偏移结构体的大小,从而获得header起始位置的指针。而flags则可以通过s向前偏移一个字节获得,即flags=s[-1]
综上,不难看出sds结构体中的header部分,实际存储在字符数组之前,如下图所示
1 2 3 4 5 +---------------------+-------------------------------+-----------+ | len | alloc | flags | Binary safe C alike string... | Null term | +---------------------+-------------------------------+-----------+ | | `->Header `-> Pointer returned to the user.
SDS的一些操作 创建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 sds sdsnew (const char *init) { size_t initlen = (init == NULL ) ? 0 : strlen (init); return sdsnewlen(init, initlen); } 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 (sh == NULL ) return NULL ; if (init==SDS_NOINIT) init = NULL ; else if (!init) memset (sh, 0 , hdrlen+initlen+1 ); 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 ; } ... } if (initlen && init) memcpy (s, init, initlen); s[initlen] = '\0' ; return s; }
扩容&空间预分配 大多数追加操作中,都会调用到扩容,其中根据新长度的大小,扩容大小也不同,一般为新长度的两倍,在大小为1Mb后,每次扩容变为增加1Mb,从而实现内存预分配内存
1 2 3 4 5 6 #define SDS_MAX_PREALLOC (1024*1024) if (newlen < SDS_MAX_PREALLOC) newlen *= 2 ; else newlen += SDS_MAX_PREALLOC;
完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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; 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); 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 { 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; }
缩容&惰性空间释放 当某些操作后,减少了原有字符串大小,sds并不会立即重新释放空间,重新分配内存,而是继续保留那么多空间,说不定下次就用上了。例如sdsclear
操作
1 2 3 4 5 void sdsclear (sds s) { sdssetlen(s, 0 ); s[0 ] = '\0' ; }
当然必要的时候也可以调用sdsRemoveFreeSpace
,主动缩容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 sds sdsRemoveFreeSpace (sds s) { void *sh, *newsh; char type, oldtype = s[-1 ] & SDS_TYPE_MASK; int hdrlen, oldhdrlen = sdsHdrSize(oldtype); size_t len = sdslen(s); size_t avail = sdsavail(s); sh = (char *)s-oldhdrlen; if (avail == 0 ) return s; type = sdsReqType(len); hdrlen = sdsHdrSize(type); if (oldtype==type || type > SDS_TYPE_8) { newsh = s_realloc(sh, oldhdrlen+len+1 ); if (newsh == NULL ) return NULL ; s = (char *)newsh+oldhdrlen; } else { newsh = s_malloc(hdrlen+len+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, len); return s; }
小结 sds相比于C语言字符串,具有以下几个显著的特点:
结构中存储字符串长度,获取字符长度只需要常量时间复杂度,而原生字符串则需要遍历数组;
二进制安全,可以存储字节数据,因为存储字符串长度,不会提前遇到\0字符而终止;
杜绝缓冲区溢出,C语言字符串本身不记录数组长度,增加操作时,分配内存不足时容易造成缓冲区溢出,而sds因为存在alloc,会在修改时,检查空间大小是否满足;
内存预分配以及惰性删除,减少内存重新分配次数;
兼容C标准库中的部分字符串函数。
参考