最近好几次用到redis,但是一直没有时间来好好学习下redis的原理,打算最近花一个月的空余时间来整理学习下redis的源码。redis本身非常简洁,下载源码直接make就ok了,而且似乎没有依赖第三方库什么的。先从基本的数据结构开始把,主要参考了http://www.redisbook.com/en/latest/index.html#, 非常赞的关于redis 的站点,作者huangz1990对redis的分析非常清楚易懂,作者提供的有详细注释的源代码地址:https://github.com/huangz1990/annotated_redis_source。在此基础上我读读源码。好久不搞c语言了,看起来真陌生。先从基本数据结构看起吧,首先分析sds.c文件。
1.sdshdr结构
// sds 类型
typedef char *sds;
// sdshdr 结构
struct sdshdr {
// buf 已占用长度
int len;
// buf 剩余可用长度
int free;
// 实际保存字符串数据的地方
char buf[];
};
看来sdshdr结构还是挺简单的,主要用来管理字符串,之所以另外设计出一个结构,作者自有用意了,管理字符串比原生的字符串方便高效许多。
这样我们如果构建一个字符串“hello world”, 那么在sdshdr结构体中表示起来就是这个样子的了:
struct sdshdr {
len = 11;
free = 0;
buf = "hello world\0"; // buf 的实际长度为 len + 1
};
2.sdsnew构建字符串
构建字符串的函数sdsnew如下:/*
* 根据给定初始化值 init ,创建 sds
* 如果 init 为 NULL ,那么创建一个 buf 内只包含 \0 终结符的 sds
*
* T = O(N)
*/
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
/*
* 创建一个指定长度的 sds
* 如果给定了初始化值 init 的话,那么将 init 复制到 sds 的 buf 当中
*
* T = O(N)
*/
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// 有 init ?
// O(N)
if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
// 内存不足,分配失败
if (sh == NULL) return NULL;
sh->len = initlen;
sh->free = 0;
// 如果给定了 init 且 initlen 不为 0 的话
// 那么将 init 的内容复制至 sds buf
// O(N)
if (initlen && init)
memcpy(sh->buf, init, initlen);
// 加上终结符
sh->buf[initlen] = '\0';
// 返回 buf 而不是整个 sdshdr
return (char*)sh->buf;
}
这里主要是根据字符串长度来分配内存空间。如果指定了init,则调用zmalloc分配内存,否则调用zcalloc分配内存。需要注意的是,redis对内存分配和释放过程做了封装,具体在下一篇文章再单独分析。zmalloc和zcalloc的区别无非是zmalloc不会初始化分配的内存,而zcalloc则会将分配的内存初始化为0。分配内存大小都为initlen+1+sdshdr大小。需要注意的是,返回值为sdshdr结构体的buf。buf是一个柔性数组,在结构体中并没有设定元素数目。所以,结构体sdshdr本身的大小其实是2个int的大小。
3.sdscatsds 字符串扩展
使用sdshdr结构体,在append字符串的时候就非常方便了。如果我们在“hello world”后append“ again!“,则sdshdr会更新为下面这个样子:
struct sdshdr {
len = 18;
free = 18;
buf = "hello world again!\0 "; // 空白的地方为预分配空间,共 18 + 18 + 1 个字节
}
append操作在函数sdscat中实现,而sdscat又是调用sdscatlen中实现。
/*
* 将一个 char 数组拼接到 sds 末尾
*
* T = O(N)
*/
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
sdscatlen首先获取当前sds的长度,然后扩展大小。并将字符串拼接到原来的sds末尾。更新len和free大小,并设定终结符\0。
/*
* 按长度 len 扩展 sds ,并将 t 拼接到 sds 的末尾
*
* T = O(N)
*/
sds sdscatlen(sds s, const void *t, size_t len) {
struct sdshdr *sh;
size_t curlen = sdslen(s);
// O(N)
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
// 复制
// O(N)
memcpy(s+curlen, t, len);
// 更新 len 和 free 属性
// O(1)
sh = (void*) (s-(sizeof(struct sdshdr)));
sh->len = curlen+len;
sh->free = sh->free-len;
// 终结符
// O(1)
s[curlen+len] = '\0';
return s;
}
sds sdsMakeRoomFor(
sds s,
size_t addlen // 需要增加的空间长度
)
{
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s); //根据sds找出sdshdr结构体指针位置,并得到结构体free字段的值
size_t len, newlen;
// 剩余空间可以满足需求,无须扩展
if (free >= addlen) return s;
sh = (void*) (s-(sizeof(struct sdshdr)));
// 目前 buf 长度
len = sdslen(s);
// 新 buf 长度
newlen = (len+addlen);
// 如果新 buf 长度小于 SDS_MAX_PREALLOC 长度
// 那么将 buf 的长度设为新 buf 长度的两倍
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 扩展长度
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
如果free大于要增加的长度,则直接返回。否则,则需要扩展。在2.6版本中,SDS_MAX_PREALLOC大小为1M,即如果newlen大于1M,则增加1M。否则则newlen更新为原来的两倍。并更新free的值,返回buf。
4.sdsclear 清除
清除sds的函数为sdsclear,代码如下:
void sdsclear(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); //获取sdshdr结构体地址
sh->free += sh->len; //free加上字符串长度
sh->len = 0; //len设为0
sh->buf[0] = '\0'; //buf第0个元素设为\0
}
针对前面的“hello world”构建的sds调用sdsclear,则此时sdshdr结构体看起来就如下面一样了:
struct sdshdr {
len = 0;
free = 11;
buf = "\0ello world\0";
};