Redis源码分析01——简单动态字符串(sds)
Posted 彼 方
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis源码分析01——简单动态字符串(sds)相关的知识,希望对你有一定的参考价值。
本文对应Redis源代码的 src/sds.c 和 src/sds.h
Redis为了更有效率地管理字符串的内存,自己构建了一种简单动态字符串(simple dynamic string,简称sds),并将sds作为Redis的默认字符串来使用。sds会根据针对不同的长度的数据采用不同的数据结构,以达到节省内存的目的。如下共五种,其中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
这里以SDS_TYPE_8类型为例讲解:
typedef char *sds;
/*
__attribute__ ((packed)) 的作用就是告诉编译器取消结构体在编译过程的优化对齐,按照实际占用字节数进行对齐
*/
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已使用的数据长度 */
uint8_t alloc; /* 去掉头和'\\0'结束符,有效长度+数据长度 */
unsigned char flags; /* 低三位表示类型, 其余五位未使用,小端格式 */
char buf[]; /* 可变长结构 */
};
内存示例如图1-1所示:
Redis为了支持sds的使用了一系列的函数,接下来就来看一下几个重要函数的具体实现:
- 新内存分配(
_sdsnewlen
)- 如果init的值为NULL,则将缓冲区全部初始化为0
- 使用init的值为SDS_NOINIT,则不对缓冲区进行初始化
- 生成的sds字符串总是以’\\0’结尾,这样可以兼容C类型字符串
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
// 获取initlen长度对应的类型
char type = sdsReqType(initlen);
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp;
size_t usable;
assert(initlen + hdrlen + 1 > initlen);
sh = trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable);
if (sh == NULL) return NULL;
// 不初始化缓冲区
if (init==SDS_NOINIT)
init = NULL;
// 将缓冲区全部置零
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
// s指向的是buf,即可变长结构
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
switch(type) {
// SDS_TYPE_5没有只有一个flags和buf,所以其长度是存储在flags的高五位的,这点和其它几个是不一样的
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 = usable;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
}
// 如果符合条件,则将缓冲区初始化为init的内容
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\\0';
return s;
}
- 内存扩容(
sdsMakeRoomFor
)- 当前剩余有效长度>=新增长度,直接返回
- 新增后的内存长度如果小于预分配长度(1024*1024)的话,则扩容一倍;其余情况每次增加预分配长度(1024*1024)
- 判断新旧类型是否一致,如果一致则直接使用realloc,否则使用malloc分配一块新内存,然后free掉旧内存
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;
size_t usable;
// 当前剩余有效长度>=新增长度,直接返回
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
assert(newlen > len);
// 新增后的内存长度小于预分配长度(1024*1024),扩容一倍,SDS_MAX_PREALLOC=1024*1024
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
// 其余情况每次增加预分配长度(1024*1024)
else
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
assert(hdrlen + newlen + 1 > len);
// 新旧类型一致则直接使用remalloc
if (oldtype==type) {
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
// 新旧类型不一致则使用malloc分配一块新内存,然后free掉旧内存
} else {
newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
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);
}
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
sdssetalloc(s, usable);
return s;
}
- 内存缩容(
sdsRemoveFreeSpace
)- 当前剩余有效长度为0(空间已用完),直接返回
- 根据当前数据的长度获取缩容之后的类型
- 判断新旧类型是否一致,如果一致则直接使用realloc进行缩容,否则使用malloc分配一块新的更小的内存,然后free掉旧内存
sds sdsRemoveFreeSpace(sds s) {
void *sh, *newsh;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
size_t len = sdslen(s);
size_t avail = sdsavail(s);
sh = (char*)s-oldhdrlen;
// 当前剩余有效长度为0(空间已用完),直接返回
if (avail == 0) return s;
// 获取当前数据长度对应的SDS类型,后续需拿这个进行和当前实际类型进行比较
type = sdsReqType(len);
hdrlen = sdsHdrSize(type);
// 新旧类型一致则直接使用remalloc进行缩容
if (oldtype==type || type > SDS_TYPE_8) {
newsh = s_realloc(sh, oldhdrlen+len+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+oldhdrlen;
// 新旧类型不一致则使用malloc分配一块新的更小内存,然后free掉旧内存
} else {
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;
}
sds优点如下:
- 可以避免缓冲区溢出(由len和alloc配合实现)
- 减少字符串修改带来的内存频繁重分配次数
- 二进制操作安全,可以保持文本数据,也可以保持任意格式的二进制数据
- 以’\\0’结尾,兼容C类型字符串
- sds是char*的别名,可以理解为分配的是一块连续内存(头部字段+数据),根据局部性原理可以提高访问速度
- 利用C语言内存布局,在sds结构体中使用了一个0长度的数组,既可以达到变长,又能保证内存也是连续的
表1-1给出Redis为sds实现的一些API,当然,因为sds兼容C类型字符串,所以大多数情况下C的那些字符串函数也是可以正常使用的
函数 | 参数 | 作用 |
---|---|---|
sdslen | (const sds s) | 获取给定sds的长度 |
sdsavail | (const sds s) | 获取给定sds的剩余可使用长度 |
sdssetlen | (sds s, size_t newlen) | 将给定sds的长度设置为newlen的值 |
sdsinclen | (sds s, size_t inc) | 将给定sds的长度增加inc |
sdsalloc | (const sds s) | 获取给定sds的总容量 |
sdssetalloc | (sds s, size_t newlen) | 将给定sds的总容量设置为newlen的值 |
sdsnewlen | (const void *init, size_t initlen) | 调用前面讲到的_sdsnewlen,创建一个新的sds对象 |
sdstrynewlen | (const void *init, size_t initlen) | 调用前面讲到的_sdsnewlen,创建一个新的sds对象 |
sdsnew | (const char *init) | 创建一个包含给定C类型字符串的sds |
sdsempty | - | 创建一个不包含任何内容的空sds |
sdsdup | (const sds s) | 创建一个给定sds的副本(深拷贝) |
sdsfree | (sds s) | 释放给定的sds |
sdsgrowzero | (sds s, size_t len) | 用空字符将给定sds扩展至长度为len |
sdscatlen | (sds s, const void *t, size_t len) | 将指定sds扩容到长度为len,然后将t的前len个字符到拼接到指定sds的末尾 |
sdscat | (sds s, const char *t) | 将给定C类型字符串拼接到sds的末尾(底层使用的是sdscatlen函数) |
sdscatsds | (sds s, const sds t) | 将给定sds拼接到另一个sds的末尾(底层使用的是sdscatlen函数) |
sdscpylen | (sds s, const char *t, size_t len) | 将指定sds扩容到长度为len,然后从t中复制前len个字符到指定的sds中 |
sdscpy | (sds s, const char *t) | 将给定C类型字符串复制到sds中(底层使用的是sdscpylen函数) |
sdscatvprintf | (sds s, const char *fmt, va_list ap) | 按指定格式将字符串拼接到指定sds的末尾(与printf系列函数类似) |
sdscatprintf | (sds s, const char *fmt, …) | 按指定格式将字符串拼接到指定sds的末尾(与printf系列函数类似) |
sdscatfmt | (sds s, char const *fmt, …) | 按指定格式将字符串拼接到指定sds的末尾(Redis自己实现的,相比printf系列函数少了很多功能) |
sdstrim | (sds s, const char *cset) | 接受一个sds和一个C类型字符串作为参数,从sds左右两端分别移除所有在C类型字符串中出现过的字符 |
sdsrange | (sds s, ssize_t start, ssize_t end) | 保留sds给定区间内的数据,不在区间内的数据会被覆盖或清除 |
sdsupdatelen | (sds s) | 更新指定sds的长度 |
sdsclear | (sds s) | 清空sds保存的字符串内容 |
sdscmp | (const sds s1, const sds s2) | 对比两个sds是否相同 |
sdssplitlen | (const char *s, ssize_t len, const char *sep, int seplen, int *count) | 使用指定的分割符将字符串s给分割各个子串,子串是sds格式的 |
sdsfreesplitres | (sds *tokens, int count) | 将sdssplitlen生成的结果释放掉 |
sdstolower | (sds s) | 将指定sds转换为小写 |
sdstoupper | (sds s) | 将指定sds转换为大写 |
sdsfromlonglong | (long long value) | 将value(整型)转换为sds |
sdscatrepr | (sds s, const char *p, size_t len) | 将转义字符串的表示形式拼接到指定sds的末尾 |
sdsmapchars | (sds s, const char *from, const char *to, size_t setlen) | 修改字符串,将from字符串中指定的字符集的所有匹配项替换为to字符串中的相应字符 |
sdsjoin | (char **argv, int argc, char *sep) | 使用指定的分隔符sep连接C类型字符串数组,返回结果为sds类型 |
sdsjoinsds | (sds *argv, int argc, const char *sep, size_t seplen) | 同sdsjoin,唯一不同的是sdsjoinsds可以指定分割符的长度,而sdsjoin的分割符必须是C类型字符串 |
sdstemplate | (const char *template, sdstemplate_callback_t cb_func, void *cb_arg) | 执行模板字符串的展开,并将结果作为新分配的sds返回。可以使用花括号指定模板变量,例如{variable}。如果要打印的是花括号,可以使用{{或}}来实现,类似于printf系列函数打印%的方法 |
sdsMakeRoomFor | (sds s, size_t addlen) | 内存扩容,前面有详细介绍 |
sdsIncrLen | (sds s, ssize_t incr) | 将给定sds的长度增加incr,并将增加完后的末尾处置’\\0’ |
sdsRemoveFreeSpace | (sds s) | 内存缩容,前面有详细介绍 |
sdsAllocSize | (sds s) | 获取指定sds的总占用空间大小(包括头部字段和末尾的’\\0’) |
sdsAllocPtr | (sds s) | 获取指定sds的头指针(指向头部字段开始处的指针) |
sds_malloc | (size_t size) | 分配内存 |
sds_realloc | (void *ptr, size_t size) | 扩展或收缩内存 |
sds_free | (void *ptr) | 释放内存 |
以上是关于Redis源码分析01——简单动态字符串(sds)的主要内容,如果未能解决你的问题,请参考以下文章