壹、sds
(SDS)Simple Dynamic String, 从字面意思上来简单理解就是简单的动态字符,其为具有动态增加空间的能力,扩容不需要使用者担心。
好的咱们来看一下他的数据结构吧
这个版本是黄所注释的3.0版本
typedef char *sds; //注意,sds其实不是一个结构体类型,而是被typedef的char*,好处见下文
struct sdshdr {
unsigned int len; //记录buf中已经使用的长度 等于SDS所保存的字符串的长度
unsigned int free; //buf中未使用的长度
char buf[]; //柔性数组buf
};
sds遵循c字符串以空字符结尾的惯例,但是保存空字符的一字节空间不计算在sds的len属性之中,并且为空字符分配额外的一字节空间,使用这一惯例的好处是可直接重用c字符串函数库里面的额函数,这也符合软件工程的思想。
此处应有图(没图你说个毛)
但是在Redis 3.2 版本中,对数据结构做出了修改,针对不同的长度范围定义了不同的结构,如下,这是目前的结构:
typedef char *sds;
struct __attribute__ ((__packed__)) sdshdr5 { // 对应的字符串长度小于 1<<5
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 { // 对应的字符串长度小于 1<<8
uint8_t len; /* used */ //目前字符串的长度
uint8_t alloc; //已经分配的总长度
unsigned char flags; //flag用3bit来标明类型,类型后续解释,其余5bit目前没有使用
char buf[]; //柔性数组,以'\0'结尾
};
struct __attribute__ ((__packed__)) sdshdr16 { // 对应的字符串长度小于 1<<16
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 { // 对应的字符串长度小于 1<<32
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 { // 对应的字符串长度小于 1<<64
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[];
};
新版带来的好处就是针对长度不同的字符串做了优化,选取不同的数据类型uint8_t或者uint16_t或者uint32_t等来表示长度、一共申请字节的大小等。上面结构体中的 attribute ((packed)) 设置是告诉编译器取消字节对齐,则结构体的大小就是按照结构体成员实际大小相加得到的。(这也是我第一次了解到的) 记得小组每年的纳新面试题,结构体的大小是必考的,试想当年如果能答出来这个的话,会不会刮目呢。
贰、SDS的优劣
typedef char *sds;
e.m.
typedef char *sds;
sds string = sdsnew ("hello world!"); // 生成这样的一个sds。
我们通过sdsnew返回的实际上是一个char * 类型的指针,这个指针指向的是字符串的开始位置,它的头部信息是在字符串前面分配的,这样带来的好处有:
- sds可以传给任何 char * 为参数的函数,复用c 函数。
- string[0], 可以直接访问单个字符,对长度进行检测,杜绝缓冲区溢出。
- 分配的空间地址是连续的(存疑),对高速缓存的命中率更加友好,
- 二进制安全,读完的标志为len - free, 可存二进制数据。
- 获取长度的复杂度为O(1),
除此之外需要重点介绍的就是关于sds的内存分配策略(主要指修改时),
redis 作为数据库,经常被用于速度要求严苛,数据被频繁修改的场合,如果每次修改字符串的时候都去执行一次内存重新分配,那么光是执行内存重新分配都会占去很大一部分时间,频繁发生的话,会对性能造成影响。
为了避免之中缺陷,sds解除了字符串长度与底层数组的关联: 在SDS中,buf的长度不一定是字符数量加1,数组里边可以包含未使用的字节,由SDS中的free来记录。
通过未使用空间,SDS使用了空间预分配和惰性空间释放的两种优化策略。
1.空间预分配
空间预分配用于优化SDS的字符串增长操作:即不仅分配修改所需空间,还会额外分配未使用空间。
其中未使用的空间的数量由一下公式决定
- 进行修改之后,sds的长度(len的属性值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,举个例子,修改之后len将变为12,则程序会另外分配13个未使用的字节,总长为 len = 12+12+1 = 25
(不要忘记额外的一字节用于保存空字符串)。 - 如果大于1MB,会分配1MB的未使用空间。举个例子,sds变为12MB,那么程序会分配1MB的未使用空间,则len = 12MB+1MB+1byte。
通过这种预分配策略,SDS连续增长N次字符串所需要的内存冲分配次数从N次将为最对N次。
2.惰性空间释放
用有优化SDS缩短操作:当字符串缩短时,并不立即使用内存重新分配回收多出来的字符,而是使用free把这些字节的长度记录起来等待将来使用。
可能你会担心惰性空间释放策略会导致内存浪费。Never mind,SDS提供相应的api 让我们在有需要的时候,真正的释放SDS的未使用空间。
要说缺点也有:虽然用这么多手段提高效率,但是我们并不知道内部是否重新分配了空间。
可以预见到的是,如果程序中别的地方使用了这个字符串,一经改变之后,没有更新地址,那么原地址就会失效,所以这一定要注意,索性这些问题redis已经帮我们处理就好了,我们只需要拿来用就可以啦!!!