介绍
在redis数据结构中,字符串类型没有直接使用c语言中的字符串,而是使用了sds(simple dynamic string,简单动态字符串)。sds在redis中运用十分广泛,例如,key-value中的key就是以sds为基础。我们先来看一下sds的定义,再来说明使用sds的原因。
sds的定义
在介绍sds之前,我们来简单回顾下c语言中字符串的结构,例如,下图为字符串”Redis“在内存中的存储方式
有以下两个特点:
- 除了分配必要的内存空间外,还要额外分配一个字节用来存储’\n’,因为在c语言以’\n’作为字符串的终止符
- 字符串没有维护自身的长度信息,必须要遍历整个字符串才能知道它的长度
再来看sds的定义,每个sds都包含了len、alloc、flags和buf这4个部分
- len:sds中c字的长度(不包括’\n’)
- alloc:已分配的空间(包括header和c字符串末尾的’\n’)
- flags:低3位记录sds类型,高5位未使用
- buf:存储c字符串
sds类型
上面介绍flags时提到”sds类型“,在redis中sds总共有5中类型,分别是
-
SDS_TYPE_5
-
SDS_TYPE_8
-
SDS_TYPE_16
-
SDS_TYPE_32
-
SDS_TYPE_64
不同类型在redis中的定义
#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,redis为其分配的内存空间大小也不相同,其目的是为了节省内存。以下是不同类型的sds header的定义
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类型
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 (string_size < 1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
}
相关函数
- 创建一个指定长度的sds并保存对应的值
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);//根据长度决定sds的类型
// 空字符串一般用作追加来使用,这里优化了下,使用SDS_TYPE_8
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp;//指向flags的指针
sh = s_malloc(hdrlen+initlen+1);// 申请内存,header长度 + 初始长度 + 1(结束位\0)
if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;//申请内存失败,返回NULL
s = (char*)sh+hdrlen;//c字符串开始位置位置的指针
fp = ((unsigned char*)s)-1;//因为flags长度固定为1个字节,s向左偏移1位,就能得到flag的指针
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);//如果init中有内容,则将其保存到新创建的sds中
s[initlen] = '\0';
return s;//返回sds
}
- 更新sds长度信息
void sdsupdatelen(sds s) {
int reallen = strlen(s);
sdssetlen(s, reallen);
}
这个函数是为了防止在某些情况下操作sds后,sds的len信息不正确的情况。例如,我手动修改了sds中c字符串的内容,修改后c字符串的长度应该为2,而不再是之前的6,因此需要调用sdsupdatelen函数更正len信息。
s = sdsnew("foobar");
s[2] = '\0';
sdsupdatelen(s);
printf("%d\n", sdslen(s));
- sds空间扩充
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; // 一般会在字符串追加操作的时候调用这个函数,保证sds有足够的空间来存储修改后的字符串,如果剩余空间可以满足的话,直接返回即可,无需扩容操作
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC) // 如果目标长度小于1M,则将sds的容量扩充为之前的2倍
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC; // 如果目标长度小于大于1M,则在原来的基础上扩容1M的空间
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) {
//前面提到过,对于不同长度的字符串,redis使用的不同的的sds类型及不同大小的header,如果扩容后类型不变的情况下,只需要调用s_realloc函数对内存空间进行调整
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 */
// 如果sds类型改变的情况下,需要调用s_malloc函数重新分配内存并移动字符串
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当前剩余空间
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
return 0;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
return sh->alloc - sh->len;//已分配的空间-已使用的空间
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
return sh->alloc - sh->len;
}
}
return 0;
}
- sds缩容
既然有sds扩容的函数,也会有对应的缩容函数,否则字符串占用的空间只会越来越多,内存空间不能及时释放。
sds sdsRemoveFreeSpace(sds s) {
void *sh, *newsh;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
size_t len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
type = sdsReqType(len);
hdrlen = sdsHdrSize(type);
if (oldtype==type) {// 如果缩容后的type和之前一样,则调用s_realloc函数重新分配空间即可
newsh = s_realloc(sh, hdrlen+len+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else { // 如果缩容后的type和之前不一样,还需要改变header类型
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;
}