redis源码分析笔记2- redis的数据类型-动态字符串sds

简介:本节主要讨论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 可用空间的长度

从这段代码上我们可以学习到的有以下几点:

  1. 定义inline函数:

inline用于定义内联函数,inline只是个编译器建议,编译器不一定非得展开Inline函数。inline通常定义在头文件中。原因是:inline定义的函数和宏定义一样,只在本地文件可见。所以建议inline定义的函数放在头文件中。

  1. sdslen方法和sdsavail方法的时间复杂度都是O(1)。

由于redis是一个数据库,使用字符串类型做为key-value底层存储结构的键。在填充或追加键值的过程中,如果使用C字符串,在执行类似于C函数库中的strcat方法、strcpy方法时。为了缓存区安全,需要计算字符串的长度。而C字符串由于采用空结尾而不保存字符串的长度。拷贝修改字符串长度的过程中,计算字符串的长度的方法的时间复杂度为O(n),必将占用处理键的绝大部分时间。可能正是因为这个原因,redis定义自己的动态字符串类型sds。

  1. 这两个方法的实现:

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函数的实现,都是比较细节,没有细看。这里就不做分析了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值