redis支持多种数据类型,sds(simple dynamic string)是最基本的一种,redis中的字符串类型大多使用sds保存,它支持动态的扩展与压缩,并提供许多工具函数。这篇文章将分析sds在redis中是如何实现的。redis
sds在redis中其实就是一个char*类型的别名,声明以下:app
typedef char *sds;
可是,以sds指向的字符串的存储格式具备必定的规则,即在字符串数据以前存储了相应的头部信息,这些头部信息包含了:1. alloc-分配的内存空间长度。2. len-有效字符串长度。3. flags-头部类型。函数
redis中有sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64这5类头部类型,其声明以下:工具
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[]; };
其它几种类型相似,但len与alloc字段根据头部类型使用不一样的类型,sdshdr16使用uint16_t,sdshdr32使用uint32_t,sdshdr64使用uint64_t。另外,sdshdr5与其它头部类型不一样,没有len与alloc字段,并将字符串实际长度保存在flags字段的高5bits。ui
sdshdr8因为alloc为uint8_t类型,所以能够表示的字符串最长为255字节;sdshdr16因为alloc为uint16_t类型,所以能够表示的字符串最长为65536字节;相似的,redis中选择最合适的头部去存储字符串,节约少量空间(我认为当字符串较短时,使用sdshdr8能够节约空间,但当字符串长度超过了uint8_t的表示范围,使用sdshdr16与sdshdr32,头部长度占所用空间的比例差异不大)。选择该使用何种头部的函数实现以下:this
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 }
sds的类型指向有效的字符串起始位置,头部信息与有效字符串的存储空间是统一分配的,它们的内存空间的连续的,所以将sds向前移动头部长度,便可获得实际分配的内存起始地址。而sds头部长度由头部类型决定,其实现以下:spa
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的头部类型保存在头部的flags的低3bits,头部类型能够经过以下方式得到code
oldtype = s[-1] & SDS_TYPE_MASK; //s为sds类型
实际分配内存地址由以下方式得到blog
sh = (char*)s-sdsHdrSize(oldtype);
所此,sdsfree操做实现以下:内存
void sdsfree(sds s) { if (s == NULL) return; s_free((char*)s-sdsHdrSize(s[-1])); }
新建sds类型的函数声明为:
sds sdsnewlen(const void *init, size_t initlen)
它的大体步骤可描述以下:
函数具体实现以下:
sds sdsnewlen(const void *init, size_t initlen) { void *sh; sds s; char type = sdsReqType(initlen); /* Empty strings are usually created in order to append. Use type 8 * since type 5 is not good at this. */ if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; 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: { 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; }
为已存在sds类型增长存储空间的操做函数声明为:
sds sdsMakeRoomFor(sds s, size_t addlen)
它的操做步骤大体以下:
函数具体实现以下:
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; }
sds头部alloc记录了分配的内存空间,len记录了实际使用的内存空间,当须要释放未使用的内存空间时,根据len记录,可使用s_realloc压缩空间或者使用s_alloc从新分配更小的空间,释放旧的内存。相应函数声明为:
sds sdsRemoveFreeSpace(sds s)
大体操做步骤以下:
sdsRemoveFreeSpace的实现与sds sdsMakeRoomFor大体相同,此处再也不列出。
另外sds还提供了不少工具函数,如cat操做,copy操做,ll2str(整数转字符串),vprintf操做(格式化操做)等。具体实现见源码文件sds.c。