简介:本节主要讨论redis五种基本类型中的动态字符串sds类型。分别从应用场景,定义,实现,API主要功能这几个方面介绍。欢迎感兴趣的朋友我们一起讨论学习。
redis支持五种数据类型:
- Strings - 字符串
- Hashes - 哈希值
- Lists - 列表
- Sets - 集合
- 集合排序
下面的五个小节将分别介绍5种数据类型的定义、实现及API操作。
简单的动态字符串SDS
redis中没有直接使用C语言中的字符串表示,而是使用了一个抽象的字符串类型sds(simple dynamic string,SDS)。
SDS应用场景
在《redis设计与实现》一书中有两个例子描述的比较好:
/*
* 类型别名,用于指向 sdshdr 的 buf 属性
*/
typedef char *sds;
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
图2
以上代码我们可以学到以下两点:
typedef的使用技巧:
- 代码typedef char *sds;
比如这样定义一个字符串sds mystr; 展开之后就是char * mystr,把mystr,替换为语句typedef char sds的sds,展开之后还是char mystr。
- struct sdshdr结构定义的技巧:
在struct sdshdr结构中buf变量是一个未分配空间的字符串,联系到typedef char sds的定义,猜测加入定义一个sds对象s,可以通过(void )(s – sizeof(struct sdshdr))找到结构sdshdr的起始地址。继续推测struct sdshdr就相当于C++中的std::string类型,而sds就相当于方法std::string.c_str()的返回值。运气好的是后续的分析刚好印证了我们在此处的猜测。
SDS API
SDS API的功能
接下来我们看一下sds的API。首先我们说明一下sds的API的主要功能:
图3 sds的API的主要功能
说明:此图摘自《redis设计与实现》一书的2.3小节
SDS API的声明
内联函数的定义
下面我们开始对SDS API的分析,首先我们看两个内联函数的定义。
/*
* 返回 sds 实际保存的字符串的长度
*
* T = O(1)
*/
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
/*
* 返回 sds 可用空间的长度
*
* T = O(1)
*/
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
这两个内联函数分别完成以下两个功能:
- sds 实际保存的字符串的长度
- sds 可用空间的长度
从这段代码上我们可以学习到的有以下几点:
- 定义inline函数:
inline用于定义内联函数,inline只是个编译器建议,编译器不一定非得展开Inline函数。inline通常定义在头文件中。原因是:inline定义的函数和宏定义一样,只在本地文件可见。所以建议inline定义的函数放在头文件中。
- sdslen方法和sdsavail方法的时间复杂度都是O(1)。
由于redis是一个数据库,使用字符串类型做为key-value底层存储结构的键。在填充或追加键值的过程中,如果使用C字符串,在执行类似于C函数库中的strcat方法、strcpy方法时。为了缓存区安全,需要计算字符串的长度。而C字符串由于采用空结尾而不保存字符串的长度。拷贝修改字符串长度的过程中,计算字符串的长度的方法的时间复杂度为O(n),必将占用处理键的绝大部分时间。可能正是因为这个原因,redis定义自己的动态字符串类型sds。
- 这两个方法的实现:
struct sdshdr sh = (void)(s-(sizeof(struct sdshdr)))和struct sdshdr sh = (void)(s-(sizeof(struct sdshdr)))印证了2.12小节中我们的猜测。
sds内部API定义
sds sdsnewlen(const void *init, size_t initlen); //根据给定长度,新生出一个sds
sds sdsnew(const char *init); //根据给定的值,生出sds
sds sdsempty(void); //清空sds操作
size_t sdslen(const sds s); //获取sds的长度
sds sdsdup(const sds s); //sds的复制方法
void sdsfree(sds s); //sds的free释放方法
size_t sdsavail(const sds s); //判断sds获取可用空间
sds sdsgrowzero(sds s, size_t len); // 扩展字符串到指定的长度
sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t); //sds连接上char字符
sds sdscatsds(sds s, const sds t); //sds连接上sds
sds sdscpylen(sds s, const char *t, size_t len); //字符串复制相关
sds sdscpy(sds s, const char *t); //字符串复制相关
sds sdscatvprintf(sds s, const char *fmt, va_list ap); //字符串格式化输出,依赖已有的方法sprintf,效率不及下面自己写的
#ifdef __GNUC__
sds sdscatprintf(sds s, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
#else
sds sdscatprintf(sds s, const char *fmt, ...);
#endif
sds sdscatfmt(sds s, char const *fmt, ...); //字符串格式化输出
sds sdstrim(sds s, const char *cset); //字符串缩减
void sdsrange(sds s, int start, int end); //字符串截取函数
void sdsupdatelen(sds s); //更新字符串最新的长度
void sdsclear(sds s); //字符串清空操作
int sdscmp(const sds s1, const sds s2); //sds比较函数
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count); //字符串分割子字符串
void sdsfreesplitres(sds *tokens, int count); //释放子字符串数组
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);
sds *sdssplitargs(const char *line, int *argc); //参数拆分
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen); //字符映射,"ho" "01", h映射为0, o映射为1
sds sdsjoin(char **argv, int argc, char *sep); //以分隔符连接字符串子数组构成新的字符串
图5
由于buf中也按照空’\0’来标识字符串的空,所以sds的很多字符串方法可以借助C的函数库来实现,无需自己重新编写字符串函数库。
sds开放给使用者的API
接下来我们看一下sds给使用者的API:
sds sdsMakeRoomFor(sds s, size_t addlen);
void sdsIncrLen(sds s, int incr);
sds sdsRemoveFreeSpace(sds s);
size_t sdsAllocSize(sds s);
图6
SDS API的声明
我们按照图3中给出的的sds主要API功能的方法依次分析。
首先我们分析一下sdsnew方法和sdsempty方法。
sdsnew方法和sdsempty方法
/*
* 根据给定的初始化字符串 init 和字符串长度 initlen
* 创建一个新的 sds
*
* 参数
* init :初始化字符串指针
* initlen :初始化字符串的长度
*
* 返回值
* sds :创建成功返回 sdshdr 相对应的 sds
* 创建失败返回 NULL
*
* 复杂度
* T = O(N)
*/
/* Create a new sds string with the content specified by the 'init' pointer
* and 'initlen'.
* If NULL is used for 'init' the string is initialized with zero bytes.
*
* The string is always null-termined (all the sds strings are, always) so
* even if you create an sds string with:
*
* mystring = sdsnewlen("abc",3");
*
* You can print the string with printf() as there is an implicit \0 at the
* end of the string. However the string is binary safe and can contain
* \0 characters in the middle, as the length is stored in the sds header. */
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
*
* 返回值
* sds :创建成功返回 sdshdr 相对应的 sds
* 创建失败返回 NULL
*
* 复杂度
* T = O(1)
*/
/* Create an empty (zero length) sds string. Even in this case the string
* always has an implicit null term. */
sds sdsempty(void) {
return sdsnewlen("",0);
}
/*
* 根据给定字符串 init ,创建一个包含同样字符串的 sds
*
* 参数
* init :如果输入为 NULL ,那么创建一个空白 sds
* 否则,新创建的 sds 中包含和 init 内容相同字符串
*
* 返回值
* sds :创建成功返回 sdshdr 相对应的 sds
* 创建失败返回 NULL
*
* 复杂度
* T = O(N)
*/
/* Create a new sds string starting from a null termined C string. */
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
图7
sdsnew方法根据给定字符串init ,创建一个包含同样字符串的sds。如果输入为空,那么创建一个空的sds;否则创建的sds包含于init相同字符串sds.
在这段代码中,我们学会以下几点:
- size_t类型:
size_t 类型定义在cstddef头文件中,该文件是C标准库的头文件stddef.h的C++版。它是一个与机器相关的unsigned类型,其大小足以保证存储内存中对象的大小。一个基本的无符号整数的C / C + +类型,它是sizeof操作符返回的结果类型,该类型的大小是选择。因此,它可以存储在理论上是可能的任何类型的数组的最大大小。
- 给struct sdshdr *sh;分配空间的代码:
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);我们可以看到实际上给sh分配了sizeof(struct sdshdr) +initlen+1的空间。
其中sizeof(struct sdshdr)空间分配给struct sdshdr结构本身;
initlen分配给sdshdr->buf中实际存储的值。不含隐含的’\0’;
1实际分配给空占位’\0’。
- return (char*)sh->buf;返回sh->buf而不是实际的sdshdr*的原因:
函数返回类型sbs,也就是char*;前文说过返回char*类型可以方便的复用C的函数库。
- 根据是否有初始化内容,选择适当的内存分配方式
这段代码也是十分巧妙,如果没有初始化内容,无需清空内存,因为后续代码必然给sdshdr结构初始化值。这样就有效的提升了效率。
sdsfree方法和sdsclear
/*
* 释放给定的 sds
*
* 复杂度
* T = O(N)
*/
/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {
if (s == NULL) return;
zfree(s-sizeof(struct sdshdr));
}
/*
* 在不释放 SDS 的字符串空间的情况下,
* 重置 SDS 所保存的字符串为空字符串。
*
* 复杂度
* T = O(1)
*/
/* Modify an sds string on-place to make it empty (zero length).
* However all the existing buffer is not discarded but set as free space
* so that next append operations will not require allocations up to the
* number of bytes previously available. */
void sdsclear(sds s) {
// 取出 sdshdr
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
// 重新计算属性
sh->free += sh->len;
sh->len = 0;
// 将结束符放到最前面(相当于惰性地删除 buf 中的内容)
sh->buf[0] = '\0';
}
以上代码可以学到以下两点:
通过以上代码可以看到一个巧妙的地方,通过s-sizeof(struct sdshdr)其实释放的是sds所在的struct sdshdr *。
通过sh->free += sh->len;和sh->buf[0] = ‘\0’;实现了惰性删除
sdsdup方法
/*
* 复制给定 sds 的副本
*
* 返回值
* sds :创建成功返回输入 sds 的副本
* 创建失败返回 NULL
*
* 复杂度
* T = O(N)
*/
/* Duplicate an sds string. */
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
图9
sdsMakeRoomFor、sdsRemoveFreeSpace、sdsAllocSize方法没有多少特殊的地方,涉及到了sds的策略,在《redis设计与实现》上说的已经很清楚了,这里我就不再赘述了。
后面剩余一些sds API函数的实现,都是比较细节,没有细看。这里就不做分析了。