redis 源码系列(2):一看就懂的 string 实现 --- sds

操纵字符串(string)对于大多数的程序,都是一项主要工作。但是 c 语言中没有内置 string 类型,所以 redis 封装了自己的 string 类型 sds。今天我们就看一下 redis 中的 string 是如何实现的。

其对应的代码在 src/sds.h 和 src/sds.c 中。

柔性数组

在分析 redis 的字符串实现前,我们先岔开话题聊一下柔性数组。

柔性数组或者叫做 Arrays of Length Zero 是对标准 c 的一个扩展。其允许我们使用如下的结构体定义:

struct S {
  int a;
  char data[]; // 也可以写为 char data[]; 注意并不是只能用于 char 数组
};
printf("%lu", sizeof(struct S)); // 输出 4 而不是 8

data 往往是需要动态分配的内存(长度不固定),在构造一个如上的结构体时,一般使用如下代码:

int datalen = get_datalen();
struct S *s = malloc(sizeof(*s) + datalen);

分配后内存的布局如下:

|-----s----|---data----|
|<-4bytes->|<-datalen->|

可以看到这个声明就好像 struct S 的 data 是一个 datalen 的字符数组,如果我们不使用柔性数组想达到这个效果,就不得不使用如下的定义:

#define MAX_LENGTH 1024 // max length of data
struct S {
  int a;
  char data[MAX_LENGTH];
};

这会导致内存的浪费。

柔性数组多用于替代类似下面的定义方式:

struct S {
  int a;
  char *data;
};
printf("%lu", sizeof(struct S)); // 8 or 16

其主要的优点有:

  1. 节省内存,没有额外的指针开销(从结构体的 size 可以看出)
  2. 避免通过指针的间接访问,提高访问效率(第二种使用指针的要多寻址一次)

缺点有:

  1. 只能声明在结构体的最后,所以一个结构体内只能使用一个
  2. 在 c++ 的类继承体系中会有问题(使用这种技术的结构体不能作为基类)

sds header

struct sdshdr {
  // 数据占用的 buf 长度,不包括 \0
  int len;
  // buf 内剩余可用空间
  int free;
  // 柔性数组,存放真正的数据
  char buf[];
};

typedef char *sds;

redis 中的 sds 就使用柔性数组来实现。这也是字符串常见的一种实现方式。

构造

sds sdsnewlen(const void *init, size_t initlen) {
  struct sdshdr *sh;
  // 根据 initlen 决定到底需要申请多少内存,注意 +1(用于\0)
  if (init) {
    sh = zmalloc(sizeof(struct sdshdr)+initlen+1)
  } else {
    // 不需要拷贝数据的话,直接将新申请的内存清零
    sh = zcalloc(sizeof(struct sdshdr)+initlen+1)
  }
  
  if (sh == NULL) return NULL;
  // 新构造的字符串没有空闲空间
  // sh->len + sh->free == len of buf
  sh->len = initlen;
  sh->free = 0;
  if (initlen && init) {
    // 如果传入了 init data,进行拷贝
    memcpy(sh->buf, init, initlen);
  }
  // 字符串以 \0 结尾
  sh->buf[initlen] = '\0';
  
  // 返回字符串数组地址
  return (char*)sh->buf;
}

sdsnewlen 是最基础的构造函数,redis 还提供了类似

sds sdsdup(const sds s);
sds sdsnew(const char *init);
sds sdsempty();

等接口,都会转调用 sdsnewlen。

需要注意的是,构造好 sdshdr 结构体以后,我们并没有返回这个结构体,而是返回了 buf 的地址,后续所有的操作都是直接操作这个字符串数组,而不是 sdshdr。

析构

void sdsfree(sds s) {
  if (s == NULL) return;
  // s-sizeof(struct sdshdr) 计算出对应的 sdshdr 的地址
  zfree(s-sizeof(struct sdshdr));
}

因为释放内存的时候,我们需要释放整个 sdshdr ,所以我们需要计算对应 sdshdr 的地址。s - sizeof(struct sdshdr) 在后面的接口实现中会非常常见。这里大家可以想一下,如果 buf 是 char *,这种计算方式还可以 work 吗?如果不能的话,要怎么求 sdshdr 的地址?

sdscat

字符串的拼接是最常见的任务之一。让我们看一下 redis 内的实现

size_t sdsavail(const sds s) {
  struct sdshdr *sh = (void*)(s-sizeof(struct sdshdr));
  return sh->free;
}
size_t sdslen(const sds s) {
  struct sdshdr *sh = (void*)(s-sizeof(struct sdshdr));
  return sh->len;
}

sds sdsMaekRoomFor(sds s, size_t addlen) {
  struct sdshdr *sh, *newsh;
  size_t free, len, newlen;
  free = sdsavail(s);
  // 剩余空闲空间足够,直接返回
  if (free >= addlen) return s;
  // 计算需要的内存大小
  len = sdslen(s);
  sh = (vodi*)(s-sizeof(struct sdshdr));
  newlen = len + sh->addlen;
  if (newlen < SDS_MAX_PREALLOC)
    newlen *= 2;
  else
    newlen += SDS_MAX_PREALLOC;
  // 申请新 hdr
  newsh = zrealloc(sh, sizeof(struct sdshdr) + newlen+1);
  if (newsh == NULL) return NULL;
  newsh->free = newlen - len;
  return (char*)newsh->buf;
}

sds sdscatlen(sds s, const void *t, size_t len) {
  struct sdshdr *sh;
  size_t curlen = sdslen(s);
  // 确保有足够的内存
  s = sdsMakeRoomFor(s, len);
  if (s == NULL) return NULL;
  
  sh = (void*)(s-sizeof(struct sdshdr));
  memcpy(s+curllen, t, len);
  
  // 更新 len、free
  sh->len = curlen+len;
  sh->free -= len;
  return s;
}

sds sdscat(sds s,  const char *t) {
  return sdscatlen(s, t, strlen(t));
}

sds sdscatsds(sds s, const sds t) {
  return sdscatlen(s, t, sdslen(t));
}

拼接的内容也比较直观,如果可用内存不够的话,使用 zrealloc 重新申请内存即可。之后只需要将添加的数据拷贝到 buf 即可。

总结

redis 的字符串相关的接口实现还是比较直观的简单的。比较值得学习的是使用了柔性数组来存储字符串。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值