2.基本数据结构
2.1 简单动态字符串(SDS)
2.1.1数据结构
redis为了节省内存,针对不同长度的数据采用不同的数据结构。如下共五种,但SDS_TYPE_5并不使用,因为该类型不会存放数据长度,每次都需要进行分配和释放:
#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
以type=1为例:
typedef char* sds
/*
__attribute__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法
*/
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 数据长度 */
uint8_t alloc; /* 去掉头和null结束符,有效长度+数据长度*/
unsigned char flags; /* 3 lsb of type, 5 unused bits,小端*/
//变长数据
char buf[];
};
2.1.2 空间扩容
-
当前有效长度>=新增长度,直接返回
-
更新之后,判断新旧类型是否一致:
-
一致使用remalloc,否则使用malloc+free
- a.当前有效长度>=新增长度,直接返回
-
-
增长步长:
- 新增后长度小于预分配长度(1024*1024),扩大一倍;
-
新增后长度大于等于预分配的长度,每次加预分配长度(减少不必要的内存)
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);
//新增后长度小于预分配长度(1024*1024),扩大一倍;SDS_MAX_PREALLOC=1024*1024
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;
// 新老类型一致使用remalloc,否则使用malloc+freea.当前有效长度>=新增长度,直接返回
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.1.3 空间缩容
在trim操作时,采用的是惰性空间释放即:不会立即使用内存重分配来回收缩短的字节,只是进行移动和标记,并修改数据长
sds sdstrim(sds s, const char *cset) {
char *start, *end, *sp, *ep;
size_t len;
sp = start = s;
ep = end = s+sdslen(s)-1;
while(sp <= end && strchr(cset, *sp)) sp++;
while(ep > sp && strchr(cset, *ep)) ep--;
len = (sp > ep) ? 0 : ((ep-sp)+1);
if (s != sp) memmove(s, sp, len);
s[len] = '\0';
sdssetlen(s,len);
return s;
}
- 真正的删除被放在后续操作中见tryObjectEncoding
if (o->encoding == OBJ_ENCODING_RAW &&
sdsavail(s) > len/10)
{
o->ptr = sdsRemoveFreeSpace(o->ptr);
}
2.1.4优点
- 常量获取字符串长度(len)
- 避免缓冲区溢出
- 减少字符串修改带来的内存频繁重分配次数
- 二进制操作安全:可以保持文本数据,也可以保持任意格式的二进制数据(如视频流数据)
- 以’\0’结尾,使其兼容部分C字符串函数
2.1.5其他
- sds是char*的别名,可以理解为分配的是一块连续内存(表头+数据),根据局部性原理可以提高访问速度。
- 数据存储不使用SDS_TYPE_5,因为使用该类型每次新数据时,都需要进行扩充
- 利用C语言内存布局,在sds结构体中使用了一个0长度的数组,既可以达到变长,又能保证内存也是连续的,因此在sds一列操作中,看到使用s[-1]这样的操作搞到惊讶,当然这里的s指向的是buf位置。
/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsalloc(const sds s) {
//主要是看这句,s指向的是数据部分
unsigned char flags = s[-1];
return 0;
}