Redis学习笔记——SDS

Redis定义了一种数据结构动态字符串来表示字符串值,该数据结构的定义在文件/src/sds.h中

/*
 * 保存字符串对象的结构
 */
struct sdshdr {
    
    // buf 中已占用空间的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
}; 

变量名已经清晰的记录了变量的作用。初次之外,还定义了这个结构的一些操作接口。

static inline size_t sdsavail(const sds s);        //返回字符串生于的可用空间的长度,也就是free值
sds sdsnewlen(const void *init, size_t initlen);   //根据init指向的字符串常量,和initlen指定的大小来构造字符串
sds sdsnew(const char *init); //根据init指向的字符串常量来构造字符串,不过是通过sdsnewlen来实现的sds sdsempty(void); // 创建长度为0的空字符串size_t sdslen(const sds s); //返回字符串实际占用空间的长度,也就是len值
sds sdsdup(const sds s); //拷贝一个字符串,也是通过sdsnewlen来实现
sds sdsfree(sds s);   //释放字符串占用的空间
sds sdsgrowzero(sds s, size_t len); //将sds扩充至指定长度,末尾未使用的空间以0填充
sds sdscatlen(sds s, const void *t, size_t len); //将字符串t的前len个字节填充到s的末尾
sds sdscat(sds s, const char *t); //将字符串t填充到s的末尾,动下脑子就能猜到,内部通过sdscatlen实现
sds sdscatsds(sds s, const sds t); //同上,因为typedefchar *sds;
sds sdscpylen(sds s, const char *t, size_t len); //将t的前len个字符拷贝到s上,也就是会覆盖s的内容
sds sdscpy(sds s, const char *t); // 将t的内容拷贝到s上
sds sdscatvprintf(sds s, const char *fmt, va_list ap);//通过fmt指定个格式来格式化字符串
sds sdscatfmt(sds s, char const *fmt, ...); //将格式化后的任意数量个字符串追加到s的末尾,通过sdscatvprintf实现
sds sdstrim(sds s, const char *cset); //对s的左右两端进行裁剪,去掉cset指定的字符
void sdsrange(sds s, int start, int end); //通过索引区间[start,end]来截取字符串
void sdsupdatelen(sds s); //根据字符串所占用空间的长度大小来更新len、free
void sdsclear(sds s); //将字符串的第一个字符串置为'\0',也就是把字符串置为空字符串,但是没有释放空间
int sdscmp(const sds s1, const sds s2); //比较两个sds是否相等
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);
//使用分隔符sep对s进程进行分割,返回一个sds数组,同时count设置为数组的个数
//len和seplen分别是s和sep的长度
void sdsfreesplitres(sds *tokens, int count);//释放数组tokens的count个sds
void sdstolower(sds s); // 将sds的所有字符都转换成小写
void sdstoupper(sds s); // 将sds的所有字符都转换成大写 
sds sdsfromlonglong(long long value); //将长整型数据转成字符串
sds sdscatrepr(sds s, const char *p, size_t len);
//将长度为len的字符串p以带引号的格式追加到s的末尾
//如 s = "abc" , p = "gbdf\n134"; 那么函数的返回结果为 ret = "abc\"gbdf\\n134\""
sds *sdssplitargs(const char *line, int *argc);//将一行文本分割成多个参数,参数的个数存在argc
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
// 将字符串s中,出现存在from中指定的字符,都转换成to中的字符,from与to是有位置关系,
// 假如from = "ckj", to = "345", 那么‘c’就换成‘3’, 'k'就换成‘4’, 以此类推
sds sdsjoin(char **argv, int argc, char *sep); //通过分隔符sep把字符数组argv拼接成一个字符串
sds sdsMakeRoomFor(sds s, size_t addlen); //对字符串进行扩充,使之有addlen+1个长度的剩余空间
void sdsIncrLen(sds s, int incr); //在不重新分配空间的基础上,给字符串增加incr长度
sds sdsRemoveFreeSpace(sds s); //回收sds剩余的空间内容,但是不会修改字符串的内容
size_t sdsAllocSize(sds s); //返回给s分配的内存的字节数

代码中操作的对象是sds,并且多次利用sdshr中buf的偏移地址来获取sdshr的地址,如下

struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));

个人感觉这个可以写成一个宏,像offsetof。之所以可以这样用sizeof(struct sdshdr)是因为,buf是变长数组,因为没有指定长度,所以没有占用空间。所以,sizeof(struct sdshdr) == sizeof(int) + sizeof(int)
1.Redis通过空间预分配来减少修改字符串带来的内存重新分配的开销。分配的算法如下:
A. 如果对 SDS 进行修改之后, SDS 的长度(也即是 len 属性的值)将小于 1 MB , 那么程序分配和 len 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 free 属性的值相同。 举个例子, 如果进行修改之后, SDS 的 len 将变成 13 字节, 那么程序也会分配 13 字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13 + 13 + 1 = 27 字节(额外的一字节用于保存空字符)。
B. 如果对 SDS 进行修改之后, SDS 的长度将大于等于 1 MB , 那么程序会分配 1 MB 的未使用空间。 举个例子, 如果进行修改之后, SDS 的 len 将变成 30 MB , 那么程序会分配 1 MB 的未使用空间, SDS 的 buf 数组的实际长度将为 30 MB + 1 MB + 1 byte

2.Redis缩短字符串时,只把字符串的第一个字符置为’0’,不回收空间。也就是所谓的惰性空间释放

3.Redis字符串是二进制安全的,因为sdshr里的字符串数组 char buf[] 存储字符的二进制数据,通过len来表示大小

最后附上《Redis设计与实现的总结》里指出的C字符串跟SDS的区别
字符串和 SDS 之间的区别
C 字符串 SDS
获取字符串长度的复杂度为 O(N) 。 获取字符串长度的复杂度为 O(1) 。
API 是不安全的,可能会造成缓冲区溢出。 API 是安全的,不会造成缓冲区溢出。
修改字符串长度 N 次必然需要执行 N 次内存重分配。 修改字符串长度 N 次最多需要执行 N 次内存重分配。
只能保存文本数据。 可以保存文本或者二进制数据。
可以使用所有 <string.h> 库中的函数。 可以使用一部分 <string.h> 库中的函数。

参考:《Redis设计与实现》

    原文作者:yoop
    原文地址: https://segmentfault.com/a/1190000008896434
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞