sds

简介

在redis内部,string数据结构的value主要以int、sds作为结构存储。int用来存放整型,sds用来存放字节、字符串、浮点型数据。

sds结构分析

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

sds一共有5种类型的header(sdshdr5 不再被使用了)。使得不同长度的字符串可以使用不同大小的header,从而节省内存。

接下来我们定义一个通用的sds结构,如下:

struct  sdshdr {
    XXX len; /* used */
    XXX alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
  • len:表示字符串的真正长度(不包含NULL结束符);
  • alloc:表示字符串的最大容量(不包含头部和NULL结束符);
  • flags:占用一个字节,其中的最低3个bit用来表示header的类型,其余5个bit未使用。对应类型如下:
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
  • buf:字符数组,存储实际内容。SDS 遵循 C 字符串以空字符结尾的惯例, 保存NULL字符的 1 字节空间不计算在SDS的len属性、alloc属性里面, 并且为空字符分配额外的 1 字节空间。

sds优势

C 语言使用长度为 N+1 的字符数组来表示长度为 N 的字符串, 并且字符数组的最后一个元素总是NULL字符 '\0' 。这种简单的字符串表示方式, 并不能满足 Redis 对字符串在安全性、效率、以及功能方面的要求。

常数复杂度获取字符串长度

C 字符串并不记录自身的长度信息, 所以为了获取一个 C 字符串的长度, 程序必须遍历整个字符串, 对遇到的每个字符进行计数, 直到遇到代表字符串结尾的空字符为止, 这个操作的复杂度为 O(N);
sds记录字符串长度,可以直接返回len,复杂度为 O(1);

杜绝缓冲区溢出

C 字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出。


image.png

如上图所示,str1="hello",str2="world",str1与str2存储在连续的内存空间,接着对str1拼接“&hello”(c语言提供/strcat 函数可以将 src 字符串中的内容拼接到 dest 字符串的末尾:char *strcat(char *dest, const char *src);),可以看到原先str2="world"被修改了。

sds的空间分配策略完全杜绝了发生缓冲区溢出的可能性: 当sds API 需要对sds进行修改时, API 会先检查sds的空间是否满足修改所需的要求, 如果不满足的话, API 会自动将sds的空间扩展至执行修改所需的大小, 然后才执行实际的修改操作, 所以使用sds既不需要手动修改sds的空间大小, 也不会出现前面所说的缓冲区溢出问题。

减少修改字符串时带来的内存重分配次数

每次增长或者缩短一个 C 字符串, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作。内存重分配涉及复杂的算法, 并且可能需要执行系统调用, 所以它通常是一个比较耗时的操作。

在sds中, buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节, 而这些字节的数量就由 SDS 的 alloc属性 - len属性来决定。
sds通过空间预分配和惰性空间释放两种优化策略来减少修改字符串时带来的内存重分配次数。

空间预分配

空间预分配策略如下:

  • 如果对sds进行修改之后, len < 1 MB , 则alloc = 2 * len,buf长度 = 2*len + 1;
  • 如果对sds进行修改之后, len >= 1 MB , 则alloc = len + 1MB,buf长度 = len + 1MB + 1byte;

通过空间预分配策略, Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。

惰性空间释放

惰性空间释放用于优化sds的字符串缩短操作: 当需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是先预留着,并等待将来使用。

当然, sds也提供了相应的API,让我们可以在有需要时,真正地释放 sds里面的未使用空间, 所以不用担心惰性空间释放策略会造成内存浪费。

二进制安全

对于C语言来说,C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含NULL字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。

sds的 API 都是二进制安全的(binary-safe): 所有sds API 都会以处理二进制的方式来处理sds存放在buf数组里的数据, 程序不会对其中的数据做任何额外的处理,数据在写入时是什么样的,它被读取时就是什么样。也就是说相比C语言字符串,SDS会通过len来限制读取长度,而非“\0”,所以保证了二进制安全。

因此sds既可以保存文本数据, 又能保存二进制数据。

兼容部分 C 字符串函数

通过遵循C字符串以NULL字符结尾的惯例, sds可以使用部分 库中的函数。

你可能感兴趣的:(sds)