简单动态字符串SDS

数据结构

Redis是用C语言实现的,那么在C语言里面,怎么实现一个既能动态扩容,又得二进制安全的字符串呢?

二进制安全: 在C语言中,'\0'是用来表示字符串结束的,但是如果字符串本身的内容就包含这个'\0'的话,原始字符串就会被截断,这就是非二进制安全。

Redis设计了一个struct,既能支持动态扩容,又能保证二进制安全:

struct sds{
  int len; //buf中已经占用的字节数
  int free; //buf中剩余可用的字节数
  char buf[]; //数据空间
}

SDS的结构如下,在64位操作系统中,len和free都是4个字节。


image.png

buf[]是一个柔性数组(flexible array),在结构体中,flexible array只能放在末尾。包含flexible array的结构体,通过malloc函数为柔性数组动态分配内存。
这种结构的好处:

  1. 有len和free变量的存在,所以可以很容易的统计出字符串的长度;
  2. 由于有len变量的存在,所以读取字符串也不依赖于'\0',保证了二进制安全;
  3. buf[]柔性数组支持动态扩容;

但是这种结构也有不好的地方,len和free都固定地各占4个字节,而在实际使用场景中,字符串往往并不长。比如假设一个字符串只有两个字符,可是如果此时len和free都是4个字节都话,它们各自能描述都长度范围为0-2^32-1,可是字符串就两个字符,用这么大的类型去描述有点太浪费空间了。
所以在Redis 5.0中,改变了这种结构,增加了一个flag变量,长度为最小的1字节。这个flag变量的低三位用来表示类型。
这个类型就是len的大小,如果字符串是长度小于31的短字符串,那么可以用小于1字节的类型去描述。Redis 5.0里面,根据字符串的长短,设计了5种类型:

#define SDS_TYPE_5    0  //表示长度小于31的短字符串,用flag的低5位来表示len,也就是2^5-1=31, 0-31的范围。
#define SDS_TYPE_8    1  //表示len需要一个字节,也就是字符串长度已经大于31了,用前面的类型已经不够了。
#define SDS_TYPE_16   2  //表示len需要两个字节
#define SDS_TYPE_32   3  //表示len需要8个字节
#define SDS_TYPE_64   4  //表示len需要16个字节

这就是为什么要用flag的低三位来表示类型,因为低三位能表示的值范围是0-2^3-1,即0-7。而这5种类型的值是0-4。低三位的范围能覆盖这个0-4的值,还能有一定的扩展。

下图是SDS_TYPE是SDS_TYPE_5的结构:


image.png

所以在这种超短型字符串的结构中,只有flag变量和flexible array buf[]。
源代码结构:

struct sdshdr5{
  unsigned char flags;  //低三位存储类型,高5位存储长度
  char buf[];  //存放实际内容
}

而长度大于31的字符串,则len和free单独存放。不过在Redis 5.0中,已经没有free了,是alloc,buf的总长度。SDS_TYPE是SDS_TYPE_8的结构图如下:


image.png

源代码结构:

stuct sdshdr8{
  uint8_t len; //已使用长度,用1个字节存储
  uint8_t alloc;  //总长度,用1个字节存储
  unsigned char flags;  //低三位存储类型,高5位预留
  char buf[];  //存放实际内容
}

SDS_TYPE_16, SDS_TYPE_32, SDS_TYPE_64的结构图和源代码,与SDS_TYPE_8的一致,只有len和alloc的长度的差别。

stuct sdshdr16{
  uint16_t len; //已使用长度,用2个字节存储
  uint16_t alloc;  //总长度,用2个字节存储
  unsigned char flags;  //低三位存储类型,高5位预留
  char buf[];  //存放实际内容
}

stuct sdshdr32{
  uint32_t len; //已使用长度,用4个字节存储
  uint32_t alloc;  //总长度,用4个字节存储
  unsigned char flags;  //低三位存储类型,高5位预留
  char buf[];  //存放实际内容
}

stuct sdshdr64{
  uint64_t len; //已使用长度,用8个字节存储
  uint64_t alloc;  //总长度,用8个字节存储
  unsigned char flags;  //低三位存储类型,高5位预留
  char buf[];  //存放实际内容
}

所以除了sdshdr5,其他几个的结构都是一样的。

你可能感兴趣的:(简单动态字符串SDS)