Redis深入理解-数据结构篇(1)-简单动态字符串SDS

Redis没有直接使用C语言中的字符串,而是自己构建了SDS这样的一种简单动态字符串,并且将他作为Redis中字符串的默认的表示,个人认为,Redis并未完全抛弃C语言字符串,只不过是在C语言字符串的基础上,通过封装其他的属性,构造出一个更加高效的字符串的封装结构,在早些的版本中记录了其长度(实际使用了多少)、剩余空间、以及字符数组,最新的版本3.2.4中,已经对SDS做了一定的改动记录了长度、分配内存大小(除去‘\0’)、标志位(低三位表示类型,其余五位未使用)、以及字符数组。


当你的客户端执行以下的命令的时候:

redis:0>set wy 'wy'
OK
实际上在Redis数据库中,会创建一个新的键值对,其中

1. 键值对的键是一个字符串对象,底层的实现是一个SDS字符串。

2. 键值对的值也是一个字符串对象,他对应着一个封装了‘wy’的字符串对象,其实质是通过SDS进行内部存储的。


此外,SDS在其他的多种对象的底层结构中,都将被用于集成。


Redis中SDS的定义

之前的版本2.9

/*
 * 保存字符串对象的结构
 */
struct sdshdr {
    
    // buf 中已占用空间的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
};

这里展示了一个SDS示例:

Redis深入理解-数据结构篇(1)-简单动态字符串SDS_第1张图片

1. free属性的值0,该SDS没有空闲的未使用空间。

2. len 为5,表示字符串的长度为5

3. buf为字符数组,最后一位为\0标识字符串的结束。

SDS遵循C字符串以空字符结束的惯例,保留空字符的1字节的空间,不计算在SDS的len属性中,并且为空字符分配一个字节的空间,遵循这一惯例使得SDS仍然可以使用部分C语言字符串的一些函数。



3.2.4版本

/* 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[];
};
这里出现了一些变化,长度,分配的内存大小,flags标记还有字符数组

根据自己的理解:

1. 长度应该根据不同的大小确定是多少位的长度值。

2. 分配内存大小alloc:排除了头和空字符,这里引入了header的头信息。

3. flags低三位表示header的类型,目前主要有七种类型,高五位未使用,这里未使用的部分可以对以后的需要进行扩展,后面如果还有其他的类型完全可以不变该结构的情况下进行扩展。

#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
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3

Redis深入理解-数据结构篇(1)-简单动态字符串SDS_第2张图片



SDS与C字符串的区别


Redis对字符串在安全性、效率和功能方面都要求很高,如果直接照搬C语言的字符串,会出现很多问题,而且当获取长度等操作的时候,效率明显很低达到O(N)。


获取字符串长度


1. C字符串不记录自己的长度,当需要获取长度的时候,需要遍历整个字符数组,对遇到的所有的字符进行计数,直到遇到空字符为止,执行时间复杂度为O(N)。

2. SDS字符串保存了自身的长度,当需要获取长度的时候,直接可以获取到,这样把时间复杂度降低到O(1)。图中为2.9版本中实例,如果3.2这里的free是没有的,计算free需要用alloc-len算出。

Redis深入理解-数据结构篇(1)-简单动态字符串SDS_第3张图片


杜绝缓冲区溢出


C语言不记录字符串长度的另一个弊端就是容易造成缓冲区溢出。举个例子,拼接两个字符串:wy和xx


假设前者为s1后者为s2,那么这里在进行字符串拼接的时候,一旦没有为s1从新分配适合的空间的话,那么拼接后的结果会溢出到s2的空间中去。


SDS在进行字符串拼接的时候,会自行检查是不是内存空间是不是 满足要求,如果不满足的话,自动进行分配,而且在进行分配空间的时候,会实行预先分配的策略。


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


C语言字符串在进行字符串的扩充和收缩的时候,都会面临着内存空间的重新分配问题。

1. 字符串拼接会产生字符串的内存空间的扩充,在拼接的过程中,原来的字符串的大小很可能小于拼接后的字符串的大小,那么这样的话,就会导致一旦忘记申请分配空间,就会导致内存的溢出。

2. 字符串在进行收缩的时候,内存空间会相应的收缩,而如果在进行字符串的切割的时候,没有对内存的空间进行一个重新分配,那么这部分多出来的空间就成为了内存泄露。


Redis在内存空间分配的问题上进行了优化,主要分为两个过程。

1. 内存预分配

内存空间进行分配的时候,预先分配一块多余的空间给当前的字符串对象,使得,在下一次字符串比如拼接的时候,尽可能保证其内存空间的足够用,不需要再去分配内存,这样的话,效率将会大大的提升。

额外分配未使用空间数量:

1). 如果修改之后SDS的长度小于1MB,那么程序将会分配和当前字符串len相同的空间给该字符串对象。比如说,wy和xx进行合并,这里是4个字符,大小为4,实际的存储大小为5,但是分配的内存空间大小会采用预分配的方式,那么分配后的内存大小为4+4+1=9个字节。

2). 如果修改之后的SDS的长度大小大于等于1MB的话,程序分配的内存空间将会为1MB,比如说变化后的字符串对象达到了30M,当他在分配空间的时候只分配1MB空间。那么最终空间大小为30M+1MB+1B.


2. 惰性释放

当字符串进行缩短操作的时候,并不立即将空间释放出来,而是,将这部分空间通过free进行标识,本字符串有多少的空余的空间。这样的话,在再次使用时也可以避免分配内存造成的时间开销。

当然,Redis中提供了专门的API,需要的时候,会真正的释放这部分空闲的内存。


二进制安全


C字符串必须符合某种编码,除了字符串的末尾外,不能包含空字符,否则会被误认为是字符串的结尾,导致最终读取的字符串是不完整的。

这些限制导致了字符串不能用于存放图片、音频、视频等二进制数据,只能存放文本数据。


但是在Redis中,不是靠空字符来判断字符串的结束的,而是通过len这个属性。那么,即便是中间出现了空字符对于SDS来说,读取该字符仍然是可以的。

Redis深入理解-数据结构篇(1)-简单动态字符串SDS_第4张图片



你可能感兴趣的:(Redis)