从零学习redis(14)--- 源码阅读之动态字符串

redis 刘宇帅 5年前 阅读量: 1427

redis 内部字符串没有使用 c 语言传统字符串表示而是用了自己定义的 SDS(simple dynamic string 动态字符串)类型, SDS 相关的定义在 src/sds.h src/sds.c 中,redis 一共定义了以下五种 SDS:
sds.h

struct __attribute__ ((__packed__)) sdshdr5 {
  unsigned char flags; /* 低三位标识类型,高五位存储字符串长度 */
  char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
  uint8_t len; /* 包含字符串的长度 */
  uint8_t alloc; /* 可容纳字符串的长度 */
  unsigned char flags; /* 低三位标识类型,高五位未使用 */
  char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
  uint16_t len; /* 包含字符串的长度 */
  uint16_t alloc; /* 可容纳字符串的长度 */
  unsigned char flags; /* 低三位标识类型,高五位未使用 */
  char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
  uint32_t len; /* used */
  uint32_t alloc; /* excluding the header and null terminator */
  unsigned char flags; /* 低三位标识类型,高五位未使用 */
  char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
  uint64_t len; /* 包含字符串的长度 */
  uint64_t alloc; /* 可容纳字符串的长度 */
  unsigned char flags; /* 低三位标识类型,高五位未使用 */
  char buf[];
};

字段分析:

  • buf 是存储动态字符串的值,这里称为动态也就是说 结构体中 buf 的大小可能很大,但是实际上存储的值可能只占 buf 的前几个字段,所以称为动态,动态数组的实现是为了减少当字符串需要变长或变短时候内存的重新分配的消耗以提高性能。
  • len 就是用来标识当前结构体保存的字符串的真正大小。
  • alloc 是当前 SDS 所能容下的最大字符串长度。buf 保留和 c 语言传统字符串一样的规则在数组末尾保留一个为空的字节,这是为了重用 C 语言内置的字符串函数,所以 alloc 的长度是 buf 长度减 1。
  • flags 用来标识 sds 类型,区分是五种结构体中的哪种

比如:保存值为 ”Redis"的字符串的 SDS 的 len 值为 5,alloc 的最小值是5(具体大小会根据分配规则调整),buf 的最小长度是 6,且前 6 个字符依次是 'R' 'e' 'd' 'i' 's' '\0'。
那么为什么这里定义了五种 sds 呢,我们可以根据 字段 len、alloc 的类型来推断这五种类型是为了保存不同长度的字符串而设计的,因为 len、alloc 的数据类型依次是 uint8_t、uint16_t、uint32_t、uint64_t(sdshdr5不包括这两个字段是因为 sdshrd5 使用 flags 的高五位存储字符串长度,另外字符串本身可容纳长度有限只能最多包含31 个字符,所以 redis 对该结构没有使用动态属性而是直接按需申请)。
使用 sds 的好处:

  • 动态字符串的能力可以减少频繁的内存申请和释放
  • 可以直接获得字符串长度(len字段)
  • 实现 SDS 处理函数,避免 c 语言中常见的未重新申请字符串空间造成内存溢出(字符串拼接等操作)

SDS 相关的处理函数如下,详细代码请看 redis 源码我添加了注释的代码 代码均在 src/sds.csrc/sds.h 两个文件中:


// 返回 SDS len
static inline size_t sdslen(const sds s);

// 返回 SDS 还可容下字符数
static inline size_t sdsavail(const sds s);
// 设置 SDS len
static inline void sdssetlen(sds s, size_t newlen);
// 修改 SDS len 增加 inc
static inline void sdsinclen(sds s, size_t inc);

// 返回 SDS alloc
static inline size_t sdsalloc(const sds s);

// 设置 SDS alloc 值
static inline void sdssetalloc(sds s, size_t newlen);

sds sdsnewlen(const void *init, size_t initlen);
// 根据一个字符串指针创建一个 SDS
sds sdsnew(const char *init);
// 申请一个值为空的 SDS
sds sdsempty(void);
// 复制 SDS
sds sdsdup(const sds s);
// 释放一个 SDS
void sdsfree(sds s);
// 为 SDS 申请空间并设置新增空间值为 0
sds sdsgrowzero(sds s, size_t len);
// 复制字符串 t 的前 len 字符并拼接到 SDS s 后边
sds sdscatlen(sds s, const void *t, size_t len);
// 把字符串 t 拼接到 SDS s 后边
sds sdscat(sds s, const char *t);
// 把 SDS t 拼接到 s 后边
sds sdscatsds(sds s, const sds t);
// 复制 t 前 len 个字符到 s
sds sdscpylen(sds s, const char *t, size_t len);
// 复制字符串 t 到 s
sds sdscpy(sds s, const char *t);

// 使用 字符串格式化 fmt 和参数列表 ap 解析成新的字符串并拼接到 s 后面
sds sdscatvprintf(sds s, const char *fmt, va_list ap);
#ifdef __GNUC__
// 使用参数列表把格式化字符串拼接到 s 后面
sds sdscatprintf(sds s, const char *fmt, ...)
    __attribute__((format(printf, 2, 3)));
#else
// 使用参数列表把格式化字符串拼接到 s 后面
sds sdscatprintf(sds s, const char *fmt, ...);
#endif
// 自定义的格式化字符串并拼接到 s 后边
sds sdscatfmt(sds s, char const *fmt, ...);
// 根据字符数组删除字符串前后的相应字符
sds sdstrim(sds s, const char *cset);
// 根据起始位置 start end 截取 SDS
void sdsrange(sds s, ssize_t start, ssize_t end);
// 使用 strlen(s) 更新 s 所对应的 SDS.len,这个redis 源码注释说用于 hack 更新
// SDS 的长度(当手动修改字符串中字符时),目前感觉这个方法没啥用还会存在潜在的风险
// redis 当前源码中还没有地方调用这个函数
void sdsupdatelen(sds s);
// 设置 SDS 为空,但是没有释放已经申请的字符空间,可供后续使用
void sdsclear(sds s);
// 一个字节一个字节的比较两个 SDS 的值
int sdscmp(const sds s1, const sds s2);
// 查找 字符串 sep 的前 seplen 的字符串对字符串 s 的 前 len 个字符组成的字符串进行分割,并返回分割子串列表(SDS 格式)
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count);
// 释放 sds split 返回的结果
void sdsfreesplitres(sds *tokens, int count);
// SDS 各个字母转化为小写字母
void sdstolower(sds s);
// SDS 各个字母转化为大写字母
void sdstoupper(sds s);
sds sdsfromlonglong(long long value);
// 把字符串 p 拼接到 s 后边,对 p 中转义字符转化为字面表达,如字符 '\n' 替换成 "\n" 字符串
sds sdscatrepr(sds s, const char *p, size_t len);
// 解析 sdscatrepr 生成的字符串,并返回多个子串
sds *sdssplitargs(const char *line, int *argc);
// SDS 字符替换
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
// 用 sep 连接多个 字符串,返回 SDS
sds sdsjoin(char **argv, int argc, char *sep);
// 用 sep 连接多个 SDS,返回 SDS
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);

// 为 SDS 申请空间以保证可以再容纳 addlen 个字符
sds sdsMakeRoomFor(sds s, size_t addlen);
// 用于手动指定 SDS 的长度,适用于手动修改 SDS 中字符串数组的值,和 sdsupdatelen 场景差不多,
void sdsIncrLen(sds s, ssize_t incr);
// 删除 SDS 中 buf 包含的多余空间
sds sdsRemoveFreeSpace(sds s);
// 返回 SDS 占用的所有空间
size_t sdsAllocSize(sds s);
// 返回 SDS 结构的指针(s实际上是 buf 的指针)
void *sdsAllocPtr(sds s);

// 封装 SDS 内存申请
void *sds_malloc(size_t size);
// 封装 SDS 内存重新申请
void *sds_realloc(void *ptr, size_t size);
// 封装 SDS 内存释放
void sds_free(void *ptr);

SDS 这部分代码是比较独立的,只有结构相关的代码,唯一有依赖的就是内存申请这块,SDS 没有用 C 语言的中的 malloc realloc free 而是封装了一层 s_malloc s_realloc s_free,为了更好地理解 SDS 我下面准备先阅读内存分配相关的代码。

提示

功能待开通!


暂无评论~