版权声明:欢迎关注公众号「Golang来了」交流。文章若有不妥之处请指正,欢迎转载,不过须注明出处~ https://blog.csdn.net/asd1126163471/article/details/54132872
一直对redis源码感兴趣,工作之余,总是抽空阅读,在这里记录下自己的阅读心得。废话不多说,一起来看下redis内部比较简单的数据结构–简单动态字符串SDS,源文件为sds.c和sds.h。
SDS数据结构定义
struct sdshdr {
// 当前字符串的长度(不包括‘\0’)
int len;
// buf 中剩余可用空间的长度
int free;
// 字符数组
char buf[];
};
Redis没有采用C语言中的字符串表示方式(以‘\0’结尾的字符数组),而是构建了SDS这种数据结构作为Redis默认的字符串,相对于C语言传统的字符串的好处在于:可动态地扩展内存、二进制安全和兼容部分C字符串函数。
基本操作函数
1、sds创建函数-sdsnewlen
//类型别名,用于指向 sdshdr 的 buf 属性
typedef char *sds;
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// 根据是否有初始化内容,选择适当的内存分配方式
//+1是因为需要额外的一个字节存放'\0'
if (init) {
// zmalloc 不初始化所分配的内存
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
// zcalloc 将分配的内存全部初始化为 0
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
// 内存分配失败,返回
if (sh == NULL) return NULL;
// 设置初始化长度
sh->len = initlen;
// 新 sds 不预留任何空间
sh->free = 0;
// 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中
if (initlen && init)
memcpy(sh->buf, init, initlen);
sh->buf[initlen] = '\0';
// 返回 buf 部分,而不是整个 sdshdr
return (char*)sh->buf;
}
需要注意的是函数的返回值:sh->buf,sds的很多基本操作都是先
通过sh->buf找到sdshdr结构体,例如接下去说到的sdslen。
2、获取sds中字符串的长度-sdslen
/* 获取字符串长度 */
static inline size_t sdslen(const sds s) {
/* sizeof(struct sdshdr))的长度是8,而s指向sdshdr中的buf字符数组,所以 s-8 的结果就是sdshdr结构体的地址--sh,通过sh->len就可以获得字符串的长度
*/
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
类似的函数操作还有sdsavail–获取sds可用空间的长度等。
3、空间扩展函数-sdsMakeRoomFor
//最大预分配长度
#define SDS_MAX_PREALLOC (1024*1024)
//s 需要扩展空间的sdshdr,addlen 需要扩展的字节数
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
// sdsavail()函数可以获取 s 目前的空余空间长度
size_t free = sdsavail(s);
size_t len, newlen;
// s 中有够用的字节空间,直接返回
if (free >= addlen) return s;
// 获取 s 目前已占用空间的长度
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
// s 最少需要的长度
newlen = (len+addlen);
// 根据新长度,为 s 分配新空间所需的大小
if (newlen < SDS_MAX_PREALLOC)
// 如果新长度小于 1M
newlen *= 2;
else
// 否则,多分配1M的空间
newlen += SDS_MAX_PREALLOC;
newsh = realloc(sh, sizeof(struct sdshdr)+newlen+1);
// 内存不足,分配失败,返回
if (newsh == NULL) return NULL;
// 更新 sds 的空余长度
newsh->free = newlen - len;
// 返回 sds
return newsh->buf;
}
空间预分配策略可以减少修改字符串带来的内存重分配次数,同时可以避免缓存区溢出:当字符串(sds)操作函数需要对sds进行修改时,函数会先检查sds的空间是否满足修改之后的需要,如果不满足的话,函数会将sds的空间扩展至修改后所需要的空间大小,之后才会执行修改操作,避免缓存区溢出。
缓存区溢出:例如在使用strcat()函数时,如果不事先分配所需要的内存空间时,就会容易发生缓存区溢出。
4、其他操作函数
sds还提供了许多操作函数,这里就不一一详细做源码分析了,只说明函数的用途
sds sdscat(sds s, const char *t); //字符串连接函数
sds sdsempty(void); //清空sds
sds sdscpy(sds s, const char *t); // 字符串的复制
sds sdscatfmt(sds s, char const *fmt, ...); //字符串格式化输出
sds sdstrim(sds s, const char *cset); //字符串缩减
sds sdsRemoveFreeSpace(sds s); //回收sds空余空间
总结
第一次写博客,如果有错误的地方或者不妥之处,楼主非常欢迎大家可以回帖讨论,定及时回复。希望通过对这几个函数的解析,起到抛砖引玉的作用,读者能够更深入地了解其他函数的作用以及实现方式,体会作者的编程思想,同时,这也是一个能够提高自己C语言的方式。
很感谢黄健宏老师的《Redis设计与实现》,这本书给了我很大的帮助,支持!