redis支持多种数据类型,sds(simple dynamic string)是最基本的一种,redis中的字符串类型大多使用sds保存,它支持动态的扩展与压缩,并提供许多工具函数。这篇文章将分析sds在redis中是如何实现的。
1. sds类型
sds在redis中其实就是一个char*类型的别名,声明如下:
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。
sdshdr8由于alloc为uint8_t类型,因此可以表示的字符串最长为255字节;sdshdr16由于alloc为uint16_t类型,因此可以表示的字符串最长为65536字节;类似的,redis中选择最合适的头部去存储字符串,节约少许空间(我认为当字符串较短时,使用sdshdr8可以节约空间,但当字符串长度超过了uint8_t的表示范围,使用sdshdr16与sdshdr32,头部长度占所用空间的比例差别不大)。选择该使用何种头部的函数实现如下:
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 }
2. sds操作
2.1 sds类型与实际分配内存之间的转换
sds的类型指向有效的字符串起始位置,头部信息与有效字符串的存储空间是统一分配的,它们的内存空间的连续的,因此将sds向前移动头部长度,即可得到实际分配的内存起始地址。而sds头部长度由头部类型决定,其实现如下:
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,头部类型可以通过如下方式获得
oldtype = s[-1] & SDS_TYPE_MASK; //s为sds类型
实际分配内存地址由如下方式获得
sh = (char*)s-sdsHdrSize(oldtype);
所此,sdsfree操作实现如下:
void sdsfree(sds s) { if (s == NULL) return; s_free((char*)s-sdsHdrSize(s[-1])); }
2.2 为c_str分配sds类型
新建sds类型的函数声明为:
sds sdsnewlen(const void *init, size_t initlen)
它的大致步骤可描述如下:
- 根据c_str的长度选择合适的sds头部类型,这一步由sdsReqType()函数实现
- 分配足够的空间存储头部与有效字符串(sds字符串末尾需要一个字节存储’\0’),这一步由s_malloc()函数实现。
- 设置头部信息,将c_str内容copy到sds中。
函数具体实现如下:
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; }
2.3 为sds增加存储空间
为已存在sds类型增加存储空间的操作函数声明为:
sds sdsMakeRoomFor(sds s, size_t addlen)
它的操作步骤大致如下:
- 查看sds中是否有足够的剩余空间容纳addlen长度的字符串,有则返回,无则继续其它操作。
- 计算需要重新分配的存储空间的长度,包括原sds长度与addlen,另外预备一部分的剩余空间。
- 根据新的长度,得到新的sds头部类型,如果新的头部类型与原类型相同,则使用s_realloc分配更多的空间;如果新的头部类型与原类型不相同,则使用s_alloc重新分配内存,并将原sds内容copy到新分配的空间。
函数具体实现如下:
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; }
2.4 释放sds未使用的存储空间
sds头部alloc记录了分配的内存空间,len记录了实际使用的内存空间,当需要释放未使用的内存空间时,根据len记录,可以使用s_realloc压缩空间或者使用s_alloc重新分配更小的空间,释放旧的内存。相应函数声明为:
sds sdsRemoveFreeSpace(sds s)
大致操作步骤如下:
- 根据len计算新的sds类型与所需内存空间。
- 如果新的类型与原sds类型相同,使用s_realloc压缩原内存空间。
- 如果新的类型与原sds类型不相同, 使用s_alloc重新分配空间,将原内容copy到新分配的sds中,并释放原内存。
sdsRemoveFreeSpace的实现与sds sdsMakeRoomFor大致相同,此处不再列出。
另外sds还提供了很多工具函数,如cat操作,copy操作,ll2str(整数转字符串),vprintf操作(格式化操作)等。具体实现见源码文件sds.c。