简单动态字符串(SDS)
简单动态字符串(Simple Dynamic Strings,SDS)是Redis的基本数据结构之一,用于存储字符串 和整型数据 。SDS兼容C语言标准字符串处理函数 ,且在此基础上保证了二进制安全。本章将详细讲解SDS的实现,为读者理解Redis的原理和各种命令的实现打下基础。
什么是二进制安全?字符串的二进制安全是指,字符串能够存储和处理任意二进制数据。C语言中的字符串就不是二进制安全的,因为它要求以
'\0'作为字符串的结尾,那么0本身就无法作为字符串的数据来存储。
SDS的数据结构
在Redis中,自定了SDS来代替C语言字符串,其通过显式记录字符串的长度来保证二进制安全。
c
struct sds {
int len; // buf 中已占用字节数
int free; // buf 中剩余可用字节数
char buf[]; // 柔性数组存储实际数据
};
什么是柔性数组?柔性数组成员(flexible array member),只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存,即该数组的大小取决于你为该结构体分配了多少内存。
在上面简化版本的SDS结构体定义中,使用两个int类型的变量len和free来分别记录当前buf中,使用了多少字节和剩余多少字节。看起来似乎没啥问题,对吧。
但是!!!Redis还想进一步压榨存储空间,根据存储的字符串长度的不同,设计了五种不同的sds结构体:
- 首先需要一个
flag字段来标识当前SDS采用的是五个SDS结构体中的哪一个,至少需要3个bits,因此使用一个unsigned char来存储。 len和alloc字段分别采用uint8_t、uint16_t、uint32_t和uint64_t类型,来存储不同长度的字符串的总容量和占用情况- 其中有一个例外,
sdshdr5没有len和alloc字段。该SDS结构体用于存储长度为1~31的字符串,其长度使用flags中空闲的5个bits来表示,如下图所示(榨干了...):

c
/* SDS类型 */
#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结构体头部 */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
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[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
在继续介绍SDS的操作之前,需要提一嘴,SDS指针指向的是buf,而不是结构体的首地址:

这样做的目的是让SDS字符串能够兼容C语言字符串,并且我们可以通过查看sds指针之前flags字段来快速找到SDS结构体的首地址。
SDS的操作
获取sds字符串的长度:
- 先通过
sds[-1]获取到flags的值 - 然后将使用与
SDS_TYPE_MASK(0b0111)按位求与,得到s的类型 - 每种类型从SDS结构体首地址到buf字段的距离都是固定的,因此可以很方便地找到SDS结构体首地址,返回长度即可
- 注意
SDS_TYPE_5进行了特殊操作,因为其长度存储在flags字段的高5位
c
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 字符串:
- 参数
init,值为NULL,将分配的内存初始化为0;值为SDS_NOINIT就不进行初始化 - 参数
initlen,即要创建的字符串的长度 - 通过
sdsReqType宏函数可以获得长度为initlen的字符串对应的类型 - 通过
sdsHdrSize宏函数可以获得类型对应的首部长度 - 分配内存,大小为 首部长度 + 字符串长度 + 1(为了兼容C字符串,存储末尾位置
'\0') - 之后就是根据SDS类型,设置各个字段,不多叙
- 值得注意的是,最后返回的是指向
buf的指针s,而不是SDS结构体的首地址(还是为了兼容C字符串)
c
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen); /* 根据字符串长度获取SDS类型 */
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); /* 分配内存,+1是为'\0'留位置,兼容C字符串 */
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen; /* s指向buf */
fp = ((unsigned char*)s)-1; /* fp指向flags */
switch(type) {
case SDS_TYPE_5: { /* sdshdr5类型,将长度放在flags中 */
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s); /* SDS_HDR_VAR用于根据类型的到首部的首地址 */
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; /* 返回的是s,而不是首部的首地址 */
}
释放 sds 字符串:
- 老套路,先通过
sds[-1]获取到flags的值 - 然后将其使用
sdsHdrSize()函数,根据flags中的类型信息(第3位)获取对应的SDS首部长度 - 最后通过
char*指针的减法运算,得到SDS结构体的首地址,调用s_free释放掉该SDS字符串
c
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}
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 字符串:
- 委托给
sdscatlen()实现拼接 - 在
sdscatlen()中,首先确保源字符串的内存足够存储拼接结果,然后进行拷贝、设置len字段等
c
sds sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
memcpy(s+curlen, t, len);
sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}
扩容策略:
- 简单的按需扩容,不会预分配更多的内存。
- 新旧SDS类型相同时,无需修改其他字段,使用realloc就地分配所需内存
- 新旧SDS类型不同时,则重新分配+拷贝,并重新设置flags中的类型信息
c
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s); /* alloc - len = 剩余的容量 */
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) { /* 新旧SDS类型不变,只需稍微增加一点内存 */
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else { /* 新旧SDS类型改变,需要修改flags中的类型信息 */
/* 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的基本结构和操作方法,其余的SDS操作函数就不用多介绍了,大差不差的。
核心问题
- SDS字符串如何实现二进制安全?
显式地使用alloc和len字段记录当前SDS字符串的容量和长度,支持任何数据存储在SDS中。
- SDS字符串是如何兼容C语言的?
这就是SDS比较有趣的地方,一个sds指针(char*)指向的是柔性数组buf,而不是SDS结构体首地址,并且在SDS字符串的末尾处还额外添加了一个\0字符。因此将SDS字符串完全可以被当作C字符串传给C字符串库中的函数进行处理。
- sdshdr5的特殊之处是什么?
为了最大程度地压榨内存,使用sdshdr5存储长度为[1, 32)的字符串,该结构体不包含alloc和len字段,而是利用flags字段中剩余的5个bits来表示字符串的长度。