1 简单动态字符串(Simple Dynamic Strings, SDS) 是Redis的基本数据结构之一,用于存储字符串和整型数据。
2 sds的结构体,大体有这4个变量:
- len:表示buf中已占用字节数
- alloc:表示buf中已分配字节数,即buf分配的总长度
- flags:标识当前结构体的类型,flags用来标示是属于5种中哪个字符串类型结构体
- buf:柔性数组,真正存储字符串的数据空间
如上所示,redis提供了5种字符串类型结构体(1字节,2字节,4字节,8字节和小于1字节)。变量
我猜这就是为什么称为简单动态字符串。它会根据内容采用对应的字符串类型来存储。比如用如下结构来存储长度小于32的短字符串
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
注:3 lsb of type, and 5 msb of string length 表示 低3位存储类型,高5位存储长度
3 源码中的__attribute__ ((__packed__))的作用:一般情况下,结构体会按其所有变量大小的最小公倍数做字节对齐。而用packed修饰后,结构体则变成按1字节对齐。
4 sds返回给上层的,不是结构体首地址,而是指向内容的buf指针。因为此时按1字节对齐,故sds创建成功后,无论是sdshdr8,sdshdr16还是sdshdr32,都能通过(char*)sh + hdrlen得到buf指针地址(其中hdrlen是结构体长度,通过sizeof计算得到)。
所以修饰后,无论sdshdr8,sdshdr16还是sdshdr32,都能通过buf[-1]找到flags
5 创建sds:Redis 通过sdsnewlen函数创建sds。sdsnewlen是hi_sdsnewlen函数的别名。在函数中会根据字符串长度选择合适的类型。
hisds hi_sdsnewlen(const void *init, size_t initlen) {
void *sh;
hisds s;
char type = hi_sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == HI_SDS_TYPE_5 && initlen == 0) type = HI_SDS_TYPE_8;
int hdrlen = hi_sdsHdrSize(type);
unsigned char *fp; /* 指向flags的指针 */
sh = hi_s_malloc(hdrlen+initlen+1); // +1是为了结束符'\0'
if (sh == NULL) return NULL;
if (!init)
memset(sh, 0, hdrlen+initlen+1);
s = (char*)sh+hdrlen; // s是指向buf的指针
fp = ((unsigned char*)s)-1; //
switch(type) {
case HI_SDS_TYPE_5: {
*fp = type | (initlen << HI_SDS_TYPE_BITS);
break;
}
case HI_SDS_TYPE_8: {
HI_SDS_HDR_VAR(8,s);
...
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
6 sdshdr5类型基本被舍弃了,因为这种可能会频繁更新而引起扩容,所以直接就创建为sdshdr8
7 释放sds:sdsfree函数,直接释放内存的函数,通过对s的偏移,可定位到sds结构体的首部,然后调用s_free释放内存
/* Free an hisds string. No operation is performed if 's' is NULL. */
void hi_sdsfree(hisds s) {
if (s == NULL) return;
hi_s_free((char*)s-hi_sdsHdrSize(s[-1]));
}
为了优化性能,sds提供了不直接释放内存,而是通过重置统计值来达到清空目的的方法 - sdsclear
void hi_sdsclear(hisds s) {
hi_sdssetlen(s, 0);
s[0] = '\0';
}
8 拼接字符串: sdscatsds函数,里头真正干活的是hi_sdscatlen函数
hisds hi_sdscatsds(hisds s, const hisds t) {
return hi_sdscatlen(s, t, hi_sdslen(t));
}
hisds hi_sdscatlen(hisds s, const void *t, size_t len) {
size_t curlen = hi_sdslen(s);
s = hi_sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
memcpy(s+curlen, t, len);
hi_sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}
9 扩容函数hi_sdsMakeRoomFor,有如下策略,可以从代码里看出
hisds hi_sdsMakeRoomFor(hisds s, size_t addlen) {
void *sh, *newsh;
size_t avail = hi_sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & HI_SDS_TYPE_MASK;
int hdrlen;
// 1)若sds中剩余空闲长度avail大于新增内容的长度addlen,直接返回,无需扩容
if (avail >= addlen) return s;
len = hi_sdslen(s);
sh = (char*)s-hi_sdsHdrSize(oldtype);
newlen = (len+addlen);
// 2)若新增后总长度len+addlen < 1MB,按新长度的2倍扩容;
// 如果len+addlen > 1MB,新长度再加上1MB扩容
if (newlen < HI_SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += HI_SDS_MAX_PREALLOC;
// 3)根据新长度重新选取存储类型,并分配空间
type = hi_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 hi_sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == HI_SDS_TYPE_5) type = HI_SDS_TYPE_8;
hdrlen = hi_sdsHdrSize(type);
if (oldtype==type) {
newsh = hi_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 = hi_s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
hi_s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
hi_sdssetlen(s, len);
}
hi_sdssetalloc(s, newlen);
return s;
}
10 其他API
11 sds有两点注意
- sds暴露给上层的是指向柔性数组buf的指针
- 读操作的复杂度多为O(1),直接读取成员变量;写操作则可能会触发扩容