学习redis,字符串是最基础的结构之一。
redis字符串设计的比较巧妙。由于他是基于c语言的,不像C++字符串类要实现各种构造函数,析构函数等等,整体设计很精炼。但是也有一些要注意的地方。
字符串有长度len,可用长度free等数据。当然最关键的是数据buf。由于数据不是定长的,所以把len,free放在结构体的最前面,最后才放数据:
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
我们创建一个新的字符串可以这样:
sds c = sdsnew("hello world");
sdsnew的定义为:
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
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;
if (initlen && init)
memcpy(sh->buf, init, initlen);
sh->buf[initlen] = '\0'; //最后一位是\0 字符串的结束符
return (char*)sh->buf; //返回char * 指针
}
实际上sds是char*的别名。上面那个函数返回的只是一个char指针,那么怎么知道c的长度呢?难不成我们调用strlen,那样设计字符串就没有意义了。上面我们提到长度等数据在buf数据的前面,所以定位len字段就可以知道了,事实上sdslen就是这么实现的:
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
return sh->len;
}
有个问题需要注意的是,不是所有的char*数据都能这样求长度,只有sdshdr结构的char*数据才能这样做,否则得出的是未知的,因为char*数据前并没有sdshdr结构的字段。
客户端对象redisClient的数据缓冲区字段querybuf类型是char*,我们却可以随时对他求长度,那是因为querybuf是用sdsnewlen()创建出来的一个空字符串,在sdsnewlen()中在堆上分配了空间:
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// 根据是否有初始化内容,选择适当的内存分配方式
// T = O(N)
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 中
// T = O(N)
if (initlen && init)
memcpy(sh->buf, init, initlen);
// 以 \0 结尾
sh->buf[initlen] = '\0';
// 返回 buf 部分,而不是整个 sdshdr
return (char*)sh->buf;
}
要扩展字符串时,调用的是sds sdsMakeRoomFor(sds s, size_t addlen),他分配新的数据。往数据buf写入数据我们调用strcpy,或者提供buf指针即可,所以我们必须有更新buf关联sdshdr结构的操作,sdsIncrLen:
void sdsIncrLen(sds s, int incr) {
struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
// 确保 sds 空间足够
assert(sh->free >= incr);
// 更新属性
sh->len += incr;
sh->free -= incr;
// 这个 assert 其实可以忽略
// 因为前一个 assert 已经确保 sh->free - incr >= 0 了
assert(sh->free >= 0);
// 放置新的结尾符号
s[sh->len] = '\0';
}
清空数据我们可以调用sdsrange,实际上他是移动数据块的操作:
void sdsrange(sds s, int start, int end) {
struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
size_t newlen, len = sdslen(s);
if (len == 0) return;
if (start < 0) {
start = len + start;
if (start < 0) start = 0;
}
if (end < 0) {
end = len + end;
if (end < 0) end = 0;
}
newlen = (start > end) ? 0 : (end - start) + 1;
if (newlen != 0) {
if (start >= (signed)len) {
newlen = 0;
}
else if (end >= (signed)len) {
end = len - 1;
newlen = (start > end) ? 0 : (end - start) + 1;
}
}
else {
start = 0;
}
// 如果有需要,对字符串进行移动
// T = O(N)
if (start && newlen) memmove(sh->buf, sh->buf + start, newlen);
// 添加终结符
sh->buf[newlen] = 0;
// 更新属性
sh->free = sh->free + (sh->len - newlen);
sh->len = newlen;
}
我们调用sdsrange(s, len, -1),就可以清空结构体的数据,实际是让第一个字节为0,长度变为0,free恢复。
以上就是目前遇到的操作字符串的函数。